├── .editorconfig ├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── OWL2OAS.sln ├── OWL2OAS ├── DotNetRdfExtensions.cs ├── OASDocument.cs ├── OWL2OAS.csproj ├── OntologyRestriction.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Relationship.cs ├── VocabularyHelper.cs └── examples │ ├── rec-core-v3.0.rdf │ └── rec-core-v3.0.yaml ├── README.md └── docs ├── _config.yml └── index.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | # IDE0017: Simplify object initialization 4 | dotnet_style_object_initializer = false:suggestion 5 | 6 | # CA1062: Do not validate 'this' parameter in extension methods 7 | dotnet_diagnostic.CA1062.exclude_extension_method_this_parameter = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | .DS_Store 263 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/OWL2OAS/bin/Debug/netcoreapp3.0/OWL2OAS.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/OWL2OAS", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/OWL2OAS/OWL2OAS.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/OWL2OAS/OWL2OAS.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/OWL2OAS/OWL2OAS.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /OWL2OAS.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29215.179 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OWL2OAS", "OWL2OAS\OWL2OAS.csproj", "{2AE0F52D-31B3-4C9A-994D-937EC35AE4F3}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2F6511A7-CEFE-4B93-BEEF-63F58496FCDE}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {2AE0F52D-31B3-4C9A-994D-937EC35AE4F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2AE0F52D-31B3-4C9A-994D-937EC35AE4F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2AE0F52D-31B3-4C9A-994D-937EC35AE4F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2AE0F52D-31B3-4C9A-994D-937EC35AE4F3}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {1CFCE062-E420-43BC-A2AC-CA64FA13CCFB} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /OWL2OAS/DotNetRdfExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using VDS.RDF; 7 | using VDS.RDF.Ontology; 8 | using VDS.RDF.Parsing; 9 | 10 | namespace OWL2OAS 11 | { 12 | /// 13 | /// Various extensions to DotNetRdf, particularly relating to the VDS.RDF.Ontology functionality. 14 | /// 15 | public static class DotNetRdfExtensions 16 | { 17 | 18 | // Used in string handling etc 19 | private static readonly CultureInfo invariantCulture = CultureInfo.InvariantCulture; 20 | 21 | #region Shared 22 | /// 23 | /// Custom comparer for OntologyResource objects, that simply 24 | /// defers to comparison of nested INodes. 25 | /// 26 | class OntologyResourceComparer : IEqualityComparer 27 | { 28 | public bool Equals(OntologyResource x, OntologyResource y) 29 | { 30 | return x.Resource == y.Resource; 31 | } 32 | 33 | public int GetHashCode(OntologyResource obj) 34 | { 35 | return obj.Resource.GetHashCode(); 36 | } 37 | } 38 | #endregion 39 | 40 | #region INode/ILiteralNode/IUriNode extensions 41 | public static bool IsLiteral(this INode node) 42 | { 43 | return node.NodeType.Equals(NodeType.Literal); 44 | } 45 | 46 | public static bool IsUri(this INode node) 47 | { 48 | return node.NodeType.Equals(NodeType.Uri); 49 | } 50 | 51 | public static IUriNode AsUriNode(this INode node) 52 | { 53 | if (!node.IsUri()) 54 | { 55 | throw new RdfException($"Node {node} is not an URI node."); 56 | } 57 | return node as IUriNode; 58 | } 59 | 60 | public static bool IsEnglish(this ILiteralNode node) 61 | { 62 | return node.Language.Equals("en", StringComparison.Ordinal) || node.Language.StartsWith("en-", StringComparison.Ordinal); 63 | } 64 | 65 | public static bool HasLanguage(this ILiteralNode node) 66 | { 67 | return !string.IsNullOrEmpty(node.Language); 68 | } 69 | 70 | public static bool IsInteger(this ILiteralNode node) 71 | { 72 | string datatype = node.DataType.ToString(); 73 | return datatype.StartsWith(XmlSpecsHelper.NamespaceXmlSchema, StringComparison.Ordinal) 74 | && (datatype.EndsWith("Integer", StringComparison.Ordinal) || datatype.EndsWith("Int", StringComparison.Ordinal)); 75 | } 76 | 77 | 78 | public static bool IsDataProperty(this INode propertyNode) 79 | { 80 | IGraph graph = propertyNode.Graph; 81 | IUriNode rdfType = graph.CreateUriNode(new Uri(RdfSpecsHelper.RdfType)); 82 | IUriNode dataProperty = graph.CreateUriNode(new Uri(OntologyHelper.OwlDatatypeProperty)); 83 | return graph.ContainsTriple(new Triple(propertyNode, rdfType, dataProperty)); 84 | } 85 | 86 | public static bool IsObjectProperty(this INode propertyNode) 87 | { 88 | IGraph graph = propertyNode.Graph; 89 | IUriNode rdfType = graph.CreateUriNode(new Uri(RdfSpecsHelper.RdfType)); 90 | IUriNode objectProperty = graph.CreateUriNode(new Uri(OntologyHelper.OwlObjectProperty)); 91 | return graph.ContainsTriple(new Triple(propertyNode, rdfType, objectProperty)); 92 | } 93 | 94 | public static bool IsAnnotationProperty(this INode propertyNode) 95 | { 96 | IGraph graph = propertyNode.Graph; 97 | IUriNode rdfType = graph.CreateUriNode(new Uri(RdfSpecsHelper.RdfType)); 98 | IUriNode annotationProperty = graph.CreateUriNode(new Uri(OntologyHelper.OwlAnnotationProperty)); 99 | return graph.ContainsTriple(new Triple(propertyNode, rdfType, annotationProperty)); 100 | } 101 | 102 | public static bool IsOntologyProperty(this INode node) 103 | { 104 | return node.IsAnnotationProperty() || node.IsDataProperty() || node.IsObjectProperty(); 105 | } 106 | 107 | public static string GetLocalName(this IUriNode node) 108 | { 109 | if (node.Uri.Fragment.Length > 0) 110 | { 111 | return node.Uri.Fragment.Trim('#'); 112 | } 113 | return Path.GetFileName(node.Uri.AbsolutePath); 114 | } 115 | 116 | public static Uri GetNamespace(this IUriNode node) 117 | { 118 | if (node.Uri.Fragment.Length > 0) 119 | { 120 | return new Uri(node.Uri.GetLeftPart(UriPartial.Path) + "#"); 121 | } 122 | string nodeUriPath = node.Uri.GetLeftPart(UriPartial.Path); 123 | if (nodeUriPath.Count(x => x == '/') >= 3) 124 | { 125 | string nodeUriBase = nodeUriPath.Substring(0, nodeUriPath.LastIndexOf("/", StringComparison.Ordinal) + 1); 126 | return new Uri(nodeUriBase); 127 | } 128 | throw new UriFormatException($"The Uri {node.Uri} doesn't contain a namespace/local name separator."); 129 | } 130 | #endregion 131 | 132 | #region OntologyGraph extensions 133 | public static bool HasBaseUriOntology(this OntologyGraph graph) 134 | { 135 | IUriNode baseUriNode = graph.CreateUriNode(graph.BaseUri); 136 | IUriNode rdfType = graph.CreateUriNode(new Uri(RdfSpecsHelper.RdfType)); 137 | IUriNode owlOntology = graph.CreateUriNode(new Uri(OntologyHelper.OwlOntology)); 138 | return graph.ContainsTriple(new Triple(baseUriNode, rdfType, owlOntology)); 139 | } 140 | 141 | public static Ontology GetBaseUriOntology(this OntologyGraph graph) 142 | { 143 | IUriNode ontologyUriNode = graph.CreateUriNode(graph.BaseUri); 144 | return new Ontology(ontologyUriNode, graph); 145 | } 146 | 147 | /// 148 | /// Gets all owl:Ontology nodes declared in the graph, packaged as Ontology objects. 149 | /// 150 | /// 151 | /// 152 | public static IEnumerable GetOntologies(this OntologyGraph graph) 153 | { 154 | IUriNode rdfType = graph.CreateUriNode(new Uri(RdfSpecsHelper.RdfType)); 155 | IUriNode owlOntology = graph.CreateUriNode(new Uri(OntologyHelper.OwlOntology)); 156 | IEnumerable ontologyNodes = graph.GetTriplesWithPredicateObject(rdfType, owlOntology) 157 | .Select(triple => triple.Subject) 158 | .UriNodes(); 159 | return ontologyNodes.Select(node => new Ontology(node, graph)); 160 | } 161 | 162 | /// 163 | /// Returns the "main" owl:Ontology declared in the the graph. Will 164 | /// return the owl:Ontology whose identifier matches the RDF graph base 165 | /// URI; if no such owl:Ontology is present, will return the first declared 166 | /// owl:Ontology; if there are none, will throw an RdfException. 167 | /// 168 | /// 169 | /// 170 | public static Ontology GetOntology(this OntologyGraph graph) 171 | { 172 | if (graph.HasBaseUriOntology()) 173 | { 174 | return graph.GetBaseUriOntology(); 175 | } 176 | IEnumerable graphOntologies = graph.GetOntologies(); 177 | if (graphOntologies.Any()) 178 | { 179 | return graphOntologies.First(); 180 | } 181 | throw new RdfException($"The graph {graph} doesn't contain any owl:Ontology declarations."); 182 | } 183 | #endregion 184 | 185 | #region OntologyResource extensions 186 | public static IEnumerable GetNodesViaPredicate(this OntologyResource resource, INode predicate) 187 | { 188 | return resource.Graph.GetTriplesWithSubjectPredicate(resource.Resource, predicate).Select(triple => triple.Object); 189 | } 190 | 191 | public static bool IsNamed(this OntologyResource ontResource) 192 | { 193 | return ontResource.Resource.IsUri(); 194 | } 195 | 196 | public static IUriNode GetUriNode(this OntologyResource ontResource) 197 | { 198 | if (!ontResource.IsNamed()) 199 | { 200 | throw new RdfException($"Ontology resource {ontResource} does not have an IRI."); 201 | } 202 | return ontResource.Resource.AsUriNode(); 203 | } 204 | 205 | public static Uri GetIri(this OntologyResource ontResource) 206 | { 207 | return ontResource.GetUriNode().Uri; 208 | } 209 | 210 | public static string GetLocalName(this OntologyResource ontResource) 211 | { 212 | return ontResource.GetUriNode().GetLocalName(); 213 | } 214 | 215 | public static Uri GetNamespace(this OntologyResource ontResource) 216 | { 217 | return ontResource.GetUriNode().GetNamespace(); 218 | } 219 | 220 | public static IEnumerable GetNodesViaProperty(this OntologyResource resource, INode property) 221 | { 222 | return resource.Graph.GetTriplesWithSubjectPredicate(resource.Resource, property).Select(triple => triple.Object); 223 | } 224 | 225 | public static bool IsDeprecated(this OntologyResource resource) 226 | { 227 | IUriNode deprecated = resource.Graph.CreateUriNode(VocabularyHelper.OWL.deprecated); 228 | return resource.GetNodesViaProperty(deprecated).LiteralNodes().Any(node => node.Value == "true"); 229 | } 230 | 231 | public static OntologyGraph OntologyGraph(this OntologyResource resource) 232 | { 233 | return resource.Graph as OntologyGraph; 234 | } 235 | #endregion 236 | 237 | #region Ontology extensions 238 | public static bool HasVersionIri(this Ontology ontology) 239 | { 240 | IUriNode versionIri = ontology.Graph.CreateUriNode(VocabularyHelper.OWL.versionIRI); 241 | return ontology.GetNodesViaProperty(versionIri).UriNodes().Any(); 242 | } 243 | 244 | public static Uri GetVersionIri(this Ontology ontology) 245 | { 246 | if (!ontology.HasVersionIri()) 247 | { 248 | throw new RdfException($"Ontology {ontology} does not have an owl:versionIRI annotation"); 249 | } 250 | 251 | IUriNode versionIri = ontology.Graph.CreateUriNode(VocabularyHelper.OWL.versionIRI); 252 | return ontology.GetNodesViaProperty(versionIri).UriNodes().First().Uri; 253 | } 254 | 255 | /// 256 | /// Gets the version IRI of the ontology, if it is defined, or the ontology 257 | /// IRI if it is not. If neither is defined (i.e. the ontology is anonymous), 258 | /// throws an exception. 259 | /// 260 | /// 261 | /// 262 | public static Uri GetVersionOrOntologyIri(this Ontology ontology) 263 | { 264 | if (ontology.HasVersionIri()) 265 | { 266 | return ontology.GetVersionIri(); 267 | } 268 | return ontology.GetIri(); 269 | } 270 | 271 | /// 272 | /// Gets a short name representation for an ontology, based on the last segment 273 | /// of the ontology IRI or (in the case of anonymous ontologies) the ontology hash. 274 | /// Useful for qname prefixes. 275 | /// 276 | /// 277 | /// 278 | public static string GetShortName(this Ontology ontology) 279 | { 280 | // Fallback way of getting a persistent short identifier in the 281 | // (unlikely?) case that we are dealing w/ an anonymous ontology 282 | if (!ontology.IsNamed()) 283 | { 284 | return ontology.GetHashCode().ToString(invariantCulture); 285 | } 286 | 287 | // This is a simple string handling thing 288 | string ontologyUriString = ontology.GetIri().ToString(); 289 | 290 | // Trim any occurences of entity separation characters 291 | if (ontologyUriString.EndsWith("/", StringComparison.Ordinal) || ontologyUriString.EndsWith("#", StringComparison.Ordinal)) 292 | { 293 | char[] trimChars = { '/', '#' }; 294 | ontologyUriString = ontologyUriString.Trim(trimChars); 295 | } 296 | 297 | // Get the last bit of the string 298 | ontologyUriString = ontologyUriString.Substring(ontologyUriString.LastIndexOf('/') + 1); 299 | 300 | // If the string contains dots, treat them as file ending delimiter and get rid of them 301 | // one at a time 302 | while (ontologyUriString.Contains('.', StringComparison.Ordinal)) 303 | { 304 | ontologyUriString = ontologyUriString.Substring(0, ontologyUriString.LastIndexOf('.')); 305 | } 306 | 307 | return ontologyUriString; 308 | } 309 | #endregion 310 | 311 | #region OntologyClass extensions 312 | public static Uri GetUri(this OntologyResource ontResource) 313 | { 314 | return ontResource.GetUriNode().Uri; 315 | } 316 | 317 | public static bool IsOwlThing(this OntologyClass oClass) 318 | { 319 | return oClass.IsNamed() && oClass.GetUri().AbsoluteUri.Equals(VocabularyHelper.OWL.Thing.AbsoluteUri); 320 | } 321 | 322 | public static bool IsRdfsLiteral(this OntologyClass oClass) 323 | { 324 | return oClass.IsNamed() && oClass.GetUri().AbsoluteUri.Equals(VocabularyHelper.OWL.Thing.AbsoluteUri); 325 | } 326 | 327 | 328 | public static bool IsRdfsDatatype(this OntologyClass oClass) 329 | { 330 | return oClass.Types.UriNodes().Any(classType => classType.Uri.AbsoluteUri.Equals(VocabularyHelper.RDFS.Datatype.AbsoluteUri)); 331 | } 332 | 333 | public static bool IsRestriction(this OntologyClass oClass) 334 | { 335 | return oClass.Types.UriNodes().Any(classType => classType.Uri.AbsoluteUri.Equals(VocabularyHelper.OWL.Restriction.AbsoluteUri)); 336 | } 337 | 338 | public static bool IsDatatype(this OntologyClass oClass) 339 | { 340 | return oClass.IsXsdDatatype() || oClass.Types.UriNodes().Any(classType => classType.Uri.AbsoluteUri.Equals(VocabularyHelper.RDFS.Datatype.AbsoluteUri)); 341 | } 342 | 343 | public static bool IsEnumerationDatatype(this OntologyClass oClass) 344 | { 345 | INode oneOf = oClass.Graph.CreateUriNode(VocabularyHelper.OWL.oneOf); 346 | if (oClass.IsDatatype()) 347 | { 348 | if (oClass.EquivalentClasses.Count() == 1) 349 | { 350 | return oClass.EquivalentClasses.Single().GetNodesViaPredicate(oneOf).Count() == 1; 351 | } 352 | else 353 | { 354 | return oClass.GetNodesViaPredicate(oneOf).Count() == 1; 355 | } 356 | } 357 | 358 | return false; 359 | } 360 | 361 | public static bool IsSimpleXsdWrapper(this OntologyClass oClass) 362 | { 363 | if (oClass.IsDatatype() && oClass.EquivalentClasses.Count() == 1) 364 | { 365 | return oClass.EquivalentClasses.Single().IsXsdDatatype(); 366 | } 367 | return false; 368 | } 369 | 370 | public static bool HasRestrictionProperty(this OntologyClass oClass) 371 | { 372 | IUriNode onProperty = oClass.Graph.CreateUriNode(VocabularyHelper.OWL.onProperty); 373 | return oClass.GetNodesViaProperty(onProperty).UriNodes().Any(node => node.IsOntologyProperty()); 374 | } 375 | 376 | public static IUriNode GetRestrictionProperty(this OntologyClass oClass) 377 | { 378 | if (!oClass.HasRestrictionProperty()) 379 | { 380 | throw new RdfException($"Ontology class {oClass} does not have a restriction property."); 381 | } 382 | IUriNode onProperty = oClass.Graph.CreateUriNode(VocabularyHelper.OWL.onProperty); 383 | return oClass.GetNodesViaProperty(onProperty).UriNodes().First(node => node.IsOntologyProperty()); 384 | } 385 | 386 | public static bool IsXsdDatatype(this OntologyClass oClass) 387 | { 388 | if (oClass.IsNamed()) 389 | { 390 | return oClass.GetIri().ToString().StartsWith(XmlSpecsHelper.NamespaceXmlSchema, StringComparison.Ordinal); 391 | } 392 | return false; 393 | } 394 | 395 | public static IEnumerable IsScopedDomainOf(this OntologyClass cls) 396 | { 397 | OntologyGraph graph = cls.Graph as OntologyGraph; 398 | IUriNode onProperty = graph.CreateUriNode(new Uri("http://www.w3.org/2002/07/owl#onProperty")); 399 | IEnumerable propertyNodes = cls.DirectSuperClasses.Where(superClass => superClass.IsRestriction()) 400 | .SelectMany(restriction => restriction.GetNodesViaProperty(onProperty)).UriNodes(); 401 | return propertyNodes.Select(node => graph.CreateOntologyProperty(node)); 402 | } 403 | 404 | public static IEnumerable IsExhaustiveDomainOf(this OntologyClass oClass) 405 | { 406 | IEnumerable indirectScopedDomainProperties = oClass.SuperClasses.SelectMany(cls => cls.IsScopedDomainOf()); 407 | IEnumerable scopedDomainProperties = oClass.IsScopedDomainOf(); 408 | IEnumerable indirectDomainProperties = oClass.SuperClasses.SelectMany(cls => cls.IsDomainOf); 409 | IEnumerable directDomainProperties = oClass.IsDomainOf; 410 | // The order is intentional -- the restrictions come before rdfs:domains, and more generic ones go first 411 | IEnumerable allDomainProperties = indirectScopedDomainProperties.Union(scopedDomainProperties).Union(indirectDomainProperties).Union(directDomainProperties); 412 | return allDomainProperties.Distinct(new OntologyResourceComparer()).Select(ontResource => ontResource as OntologyProperty); 413 | } 414 | 415 | public static IEnumerable IsExhaustiveDomainOfUniques(this OntologyClass oClass) 416 | { 417 | IEnumerable allProperties = oClass.IsExhaustiveDomainOf(); 418 | IEnumerable allUniqueProperties = allProperties.GroupBy(property => property.GetIri().ToString()).Select(group => group.First()); 419 | return allUniqueProperties; 420 | } 421 | 422 | public static IEnumerable GetRelationships(this OntologyClass cls) 423 | { 424 | List relationships = new List(); 425 | 426 | // Start w/ rdfs:domain declarations. At this time we only consider no-range (i.e., 427 | // range is owl:Thing) or named singleton ranges 428 | IEnumerable rdfsDomainProperties = cls.IsDomainOf.Where( 429 | property => 430 | property.Ranges.Count() == 0 || 431 | (property.Ranges.Count() == 1 && (property.Ranges.First().IsNamed() || property.Ranges.First().IsDatatype()))); 432 | foreach (OntologyProperty property in rdfsDomainProperties) 433 | { 434 | Relationship newRelationship; 435 | if (property.Ranges.Count() == 0) 436 | { 437 | OntologyGraph oGraph = cls.Graph as OntologyGraph; 438 | OntologyClass target; 439 | if (property.IsObjectProperty()) 440 | { 441 | target = oGraph.CreateOntologyClass(VocabularyHelper.OWL.Thing); 442 | } 443 | else 444 | { 445 | target = oGraph.CreateOntologyClass(VocabularyHelper.RDFS.Literal); 446 | } 447 | newRelationship = new Relationship(property, target); 448 | } 449 | else 450 | { 451 | OntologyClass range = property.Ranges.First(); 452 | newRelationship = new Relationship(property, range); 453 | } 454 | 455 | if (property.IsFunctional()) 456 | { 457 | newRelationship.ExactCount = 1; 458 | } 459 | 460 | relationships.Add(newRelationship); 461 | } 462 | 463 | // Continue w/ OWL restrictions on the class 464 | IEnumerable ontologyRestrictions = cls.DirectSuperClasses 465 | .Where(superClass => superClass.IsRestriction()) 466 | .Select(superClass => new OntologyRestriction(superClass)); 467 | foreach (OntologyRestriction ontologyRestriction in ontologyRestrictions) 468 | { 469 | 470 | 471 | OntologyProperty restrictionProperty = ontologyRestriction.OnProperty; 472 | OntologyClass restrictionClass = ontologyRestriction.OnClass; 473 | if (restrictionProperty.IsNamed() && (restrictionClass.IsNamed() || restrictionClass.IsDatatype())) 474 | { 475 | Relationship newRelationship = new Relationship(restrictionProperty, restrictionClass); 476 | 477 | int min = ontologyRestriction.MinimumCardinality; 478 | int exactly = ontologyRestriction.ExactCardinality; 479 | int max = ontologyRestriction.MaximumCardinality; 480 | 481 | if (min != 0) 482 | newRelationship.MinimumCount = min; 483 | if (exactly != 0) 484 | newRelationship.ExactCount = exactly; 485 | if (max != 0) 486 | newRelationship.MaximumCount = max; 487 | 488 | relationships.Add(newRelationship); 489 | } 490 | } 491 | 492 | // Iterate over the gathered list of Relationships and narrow down to the most specific ones, using a Dictionary for lookup and the MergeWith() method for in-place narrowing 493 | Dictionary relationshipsDict = new Dictionary(new OntologyResourceComparer()); 494 | foreach (Relationship relationship in relationships) 495 | { 496 | OntologyProperty property = relationship.Property; 497 | OntologyResource target = relationship.Target; 498 | // If we already have this property listed in the dictionary, first narrow down the relationship by combining it with the old copy 499 | if (relationshipsDict.ContainsKey(property)) 500 | { 501 | Relationship oldRelationship = relationshipsDict[property]; 502 | relationship.MergeWith(oldRelationship); 503 | } 504 | // Put relationship in the dictionary 505 | relationshipsDict[property] = relationship; 506 | } 507 | 508 | // Return the values 509 | return relationshipsDict.Values; 510 | } 511 | 512 | public static IEnumerable SuperClassesWithOwlThing(this OntologyClass cls) 513 | { 514 | IGraph graph = cls.Graph; 515 | IUriNode owlThing = graph.CreateUriNode(VocabularyHelper.OWL.Thing); 516 | OntologyClass owlThingClass = new OntologyClass(owlThing, graph); 517 | return cls.SuperClasses.Append(owlThingClass); 518 | } 519 | #endregion 520 | 521 | #region OntologyProperty extensions 522 | public static bool IsObjectProperty(this OntologyProperty property) 523 | { 524 | return property.Types.UriNodes().Any(propertyType => propertyType.Uri.ToString().Equals(OntologyHelper.OwlObjectProperty, StringComparison.Ordinal)); 525 | } 526 | 527 | public static bool IsDataProperty(this OntologyProperty property) 528 | { 529 | return property.Types.UriNodes().Any(propertyType => propertyType.Uri.ToString().Equals(OntologyHelper.OwlDatatypeProperty, StringComparison.Ordinal)); 530 | } 531 | 532 | public static bool IsAnnotationProperty(this OntologyProperty property) 533 | { 534 | return property.Types.UriNodes().Any(propertyType => propertyType.Uri.ToString().Equals(OntologyHelper.OwlAnnotationProperty, StringComparison.Ordinal)); 535 | } 536 | 537 | public static bool IsFunctional(this OntologyProperty property) 538 | { 539 | // Note the toString-based comparison; because .NET Uri class does not differentiate by Uri fragment! 540 | return property.Types.UriNodes().Any(propertyType => propertyType.Uri.ToString().Equals(VocabularyHelper.OWL.FunctionalProperty.ToString())); 541 | } 542 | 543 | public static IEnumerable DataProperties(this IEnumerable properties) 544 | { 545 | return properties.Where(property => property.IsDataProperty()); 546 | } 547 | 548 | public static IEnumerable ObjectProperties(this IEnumerable properties) 549 | { 550 | return properties.Where(property => property.IsObjectProperty()); 551 | } 552 | #endregion 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /OWL2OAS/OASDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using YamlDotNet.Core; 4 | using YamlDotNet.Serialization; 5 | 6 | namespace OWL2OAS 7 | { 8 | class OASDocument 9 | { 10 | public readonly string openapi = "3.0.2"; 11 | public Info info; 12 | public Components components; 13 | 14 | public Dictionary paths; 15 | public List> servers; 16 | 17 | public class Info 18 | { 19 | [YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted, Alias = "version")] 20 | public string Version { get; set; } 21 | public string title; 22 | public License license; 23 | public string description; 24 | } 25 | 26 | public class License 27 | { 28 | public string name; 29 | public string url; 30 | } 31 | 32 | public class Parameter 33 | { 34 | private string _referenceTo; 35 | [YamlMember(Alias = "$ref")] 36 | public string ReferenceTo 37 | { 38 | get 39 | { 40 | return _referenceTo; 41 | } 42 | set 43 | { 44 | _referenceTo = "#/components/parameters/" + value; 45 | } 46 | } 47 | 48 | public enum InFieldValues 49 | { 50 | path = 1, 51 | query = 2, 52 | header = 3, 53 | cookie = 4 54 | } 55 | 56 | public string name; 57 | [YamlMember(Alias = "in")] 58 | public InFieldValues InField { get; set; } 59 | public string description; 60 | public bool required; 61 | public Schema schema; 62 | public string style; 63 | } 64 | 65 | public class Components 66 | { 67 | public Dictionary parameters = new Dictionary { 68 | { "pageParam", new Parameter 69 | { 70 | name = "page", 71 | description = "If the result set is large, pagination across the results can be employed; in that case, this parameter defines the page number (zero-indexed) that is requested by the client. The number of items included in each page is defined by the 'size' parameter. Default is 0, i.e., the first results page is returned.", 72 | InField = Parameter.InFieldValues.query, 73 | required = false, 74 | schema = new PrimitiveSchema { 75 | type = "integer", 76 | format = "int32", 77 | minimum = "0", 78 | DefaultValue = "0" 79 | } 80 | } 81 | }, 82 | { 83 | "sizeParam", new Parameter 84 | { 85 | name = "size", 86 | description = "The number of items to display on a returned results page (see the page parameter).", 87 | InField = Parameter.InFieldValues.query, 88 | required = false, 89 | schema = new PrimitiveSchema { 90 | type = "integer", 91 | format = "int32", 92 | minimum = "0", 93 | maximum = 100, 94 | DefaultValue = "20" 95 | } 96 | } 97 | }, 98 | { 99 | "sortParam", new Parameter 100 | { 101 | name = "sort", 102 | description = "The field and direction to sort results on.", 103 | InField = Parameter.InFieldValues.query, 104 | required = false, 105 | schema = new ReferenceSchema("SortingSchema"), 106 | style = "deepObject" 107 | } 108 | } 109 | }; 110 | public Dictionary schemas = new Dictionary 111 | { 112 | // Add Hydra Colletions wrapper schema 113 | {"HydraCollectionWrapper", 114 | new ComplexSchema 115 | { 116 | required = new List 117 | { 118 | "@context", 119 | "@type", 120 | "hydra:member" 121 | }, 122 | properties = new Dictionary 123 | { 124 | {"@context", new ReferenceSchema("Context") }, 125 | {"@type", new PrimitiveSchema { type="string", DefaultValue="hydra:Collection"} }, 126 | {"hydra:totalItems", new PrimitiveSchema { type="integer" } }, 127 | {"hydra:view", new ComplexSchema 128 | { 129 | properties = new Dictionary 130 | { 131 | {"@id", new PrimitiveSchema { type="string", format="uri"} }, 132 | {"@type", new PrimitiveSchema { type="string", DefaultValue="hydra:PartialCollectionView"} }, 133 | {"hydra:first", new PrimitiveSchema { type="string"} }, 134 | {"hydra:previous", new PrimitiveSchema { type="string"} }, 135 | {"hydra:next", new PrimitiveSchema { type="string"} }, 136 | {"hydra:last", new PrimitiveSchema { type="string"} }, 137 | } 138 | } } 139 | } 140 | } 141 | }, 142 | // Add the default query operator filter schemas 143 | {"IntegerFilter", new ComplexSchema { 144 | properties = new Dictionary 145 | { 146 | {"eq", new PrimitiveSchema {type="integer"} }, 147 | {"lt", new PrimitiveSchema {type="integer"} }, 148 | {"lte", new PrimitiveSchema {type="integer"} }, 149 | {"gt", new PrimitiveSchema {type="integer"} }, 150 | {"gte", new PrimitiveSchema {type="integer"} } 151 | } 152 | }}, 153 | {"NumberFilter", new ComplexSchema { 154 | properties = new Dictionary 155 | { 156 | {"eq", new PrimitiveSchema {type="number"} }, 157 | {"lt", new PrimitiveSchema {type="number"} }, 158 | {"lte", new PrimitiveSchema {type="number"} }, 159 | {"gt", new PrimitiveSchema {type="number"} }, 160 | {"gte", new PrimitiveSchema {type="number"} } 161 | } 162 | }}, 163 | {"StringFilter", new ComplexSchema { 164 | properties = new Dictionary 165 | { 166 | {"eq", new PrimitiveSchema {type="string"} }, 167 | {"contains", new PrimitiveSchema {type="string"} }, 168 | {"regex", new PrimitiveSchema {type="string"} } 169 | } 170 | }}, 171 | {"DateTimeFilter", new ComplexSchema { 172 | properties = new Dictionary 173 | { 174 | {"eq", new PrimitiveSchema {type="string", format="date-time"} }, 175 | {"starting", new PrimitiveSchema {type="string", format="date-time"} }, 176 | {"ending", new PrimitiveSchema {type="string", format="date-time"} }, 177 | {"before", new PrimitiveSchema {type="string", format="date-time"} }, 178 | {"after", new PrimitiveSchema {type="string", format="date-time"} }, 179 | {"latest", new PrimitiveSchema {type="boolean" } } 180 | } 181 | }}, 182 | // And the sort operators schema 183 | {"SortingSchema", new ComplexSchema { 184 | properties = new Dictionary 185 | { 186 | {"asc", new PrimitiveSchema {type="string"} }, 187 | {"desc", new PrimitiveSchema {type="string"} } 188 | } 189 | }} 190 | }; 191 | } 192 | 193 | public class Schema 194 | { 195 | 196 | } 197 | 198 | public class ComplexSchema: Schema 199 | { 200 | public readonly string type = "object"; 201 | public List required; 202 | public Dictionary properties; 203 | public int maxProperties; 204 | public int minProperties; 205 | } 206 | 207 | public class PrimitiveSchema: Schema 208 | { 209 | public string type; 210 | public string format; 211 | public string minimum; 212 | public int maximum; 213 | [YamlMember(Alias = "default")] 214 | public string DefaultValue { get; set; } 215 | [YamlMember(Alias = "enum")] 216 | public string[] Enumeration { get; set; } 217 | } 218 | 219 | public class ReferenceSchema: Schema 220 | { 221 | public ReferenceSchema(string referenceType) 222 | { 223 | Reference = "#/components/schemas/" + referenceType.Replace(":", "_", StringComparison.Ordinal); 224 | } 225 | [YamlMember(Alias = "$ref")] 226 | public string Reference { get; set; } 227 | } 228 | 229 | public class AllOfSchema: Schema 230 | { 231 | public Schema[] allOf; 232 | } 233 | 234 | public class ArraySchema: Schema 235 | { 236 | public readonly string type = "array"; 237 | public Schema items; 238 | public int maxItems; 239 | public int minItems; 240 | } 241 | 242 | public class Path 243 | { 244 | public Operation get; 245 | public Operation put; 246 | public Operation patch; 247 | public Operation post; 248 | public Operation delete; 249 | } 250 | 251 | public class Operation 252 | { 253 | public string summary; 254 | public List parameters = new List(); 255 | public RequestBody requestBody; 256 | public Dictionary responses = new Dictionary(); 257 | public List tags = new List(); 258 | } 259 | 260 | public class RequestBody 261 | { 262 | public string description; 263 | public bool required; 264 | public Dictionary content; 265 | } 266 | 267 | public class Response 268 | { 269 | public string description; 270 | public Dictionary content; 271 | } 272 | 273 | public class Content 274 | { 275 | public Schema schema; 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /OWL2OAS/OWL2OAS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | 7 | 8 | 9 | Project 10 | -c DefaultExclude -u https://w3id.org/rec/full/3.3/ -o /Users/karl/Scratch/GitHub/RealEstateCore/rec/api/REST/rec-swagger-api.yaml 11 | true 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OWL2OAS/OntologyRestriction.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using System.Linq; 4 | using VDS.RDF; 5 | using VDS.RDF.Ontology; 6 | 7 | namespace OWL2OAS 8 | { 9 | /// 10 | /// (Partial) representation of an owl:Restriction. 11 | /// 12 | public class OntologyRestriction 13 | { 14 | public OntologyProperty OnProperty 15 | { 16 | get 17 | { 18 | IUriNode onProperty = _graph.CreateUriNode(VocabularyHelper.OWL.onProperty); 19 | IEnumerable properties = _wrappedClass.GetNodesViaPredicate(onProperty).UriNodes(); 20 | if (properties.Count() != 1) 21 | { 22 | throw new RdfException("A restriction must be on exactly one property."); 23 | } 24 | return _graph.CreateOntologyProperty(properties.First()); 25 | } 26 | } 27 | 28 | public OntologyClass OnClass 29 | { 30 | get 31 | { 32 | IUriNode onClass = _graph.CreateUriNode(VocabularyHelper.OWL.onClass); 33 | IUriNode allValuesFrom = _graph.CreateUriNode(VocabularyHelper.OWL.allValuesFrom); 34 | IUriNode someValuesFrom = _graph.CreateUriNode(VocabularyHelper.OWL.someValuesFrom); 35 | IEnumerable classes = _wrappedClass.GetNodesViaPredicate(onClass) 36 | .Union(_wrappedClass.GetNodesViaPredicate(someValuesFrom)) 37 | .Union(_wrappedClass.GetNodesViaPredicate(allValuesFrom)); 38 | if (!classes.Any()) 39 | { 40 | if (OnProperty.IsObjectProperty()) 41 | { 42 | return _graph.CreateOntologyClass(VocabularyHelper.OWL.Thing); 43 | } 44 | else 45 | { 46 | return _graph.CreateOntologyClass(VocabularyHelper.RDFS.Literal); 47 | } 48 | } 49 | else if (classes.Count() == 1) 50 | { 51 | return _graph.CreateOntologyClass(classes.First()); 52 | } 53 | throw new RdfException("A restriction must be on at most one class."); 54 | } 55 | } 56 | 57 | public int MinimumCardinality 58 | { 59 | get 60 | { 61 | IUriNode someValuesFrom = _graph.CreateUriNode(VocabularyHelper.OWL.someValuesFrom); 62 | IUriNode minCardinality = _graph.CreateUriNode(VocabularyHelper.OWL.minCardinality); 63 | IUriNode minQualifiedCardinality = _graph.CreateUriNode(VocabularyHelper.OWL.minQualifiedCardinality); 64 | 65 | IEnumerable minCardinalities = _wrappedClass.GetNodesViaPredicate(minCardinality).Union(_wrappedClass.GetNodesViaPredicate(minQualifiedCardinality)); 66 | if (minCardinalities.LiteralNodes().Count() == 1 && 67 | minCardinalities.LiteralNodes().First().IsInteger()) 68 | { 69 | return int.Parse(minCardinalities.LiteralNodes().First().Value, CultureInfo.InvariantCulture); 70 | } 71 | 72 | if (_wrappedClass.GetNodesViaPredicate(someValuesFrom).Count() == 1) 73 | { 74 | return 1; 75 | } 76 | return 0; 77 | } 78 | } 79 | 80 | public int ExactCardinality 81 | { 82 | get 83 | { 84 | IUriNode cardinality = _graph.CreateUriNode(VocabularyHelper.OWL.cardinality); 85 | IUriNode qualifiedCardinality = _graph.CreateUriNode(VocabularyHelper.OWL.qualifiedCardinality); 86 | 87 | IEnumerable exactCardinalities = _wrappedClass.GetNodesViaPredicate(cardinality).Union(_wrappedClass.GetNodesViaPredicate(qualifiedCardinality)); 88 | if (exactCardinalities.LiteralNodes().Count() == 1 && 89 | exactCardinalities.LiteralNodes().First().IsInteger()) 90 | { 91 | return int.Parse(exactCardinalities.LiteralNodes().First().Value, CultureInfo.InvariantCulture); 92 | } 93 | return 0; 94 | } 95 | } 96 | 97 | public int MaximumCardinality 98 | { 99 | get 100 | { 101 | IUriNode maxCardinality = _graph.CreateUriNode(VocabularyHelper.OWL.maxCardinality); 102 | IUriNode maxQualifiedCardinality = _graph.CreateUriNode(VocabularyHelper.OWL.maxQualifiedCardinality); 103 | 104 | IEnumerable maxCardinalities = _wrappedClass.GetNodesViaPredicate(maxCardinality).Union(_wrappedClass.GetNodesViaPredicate(maxQualifiedCardinality)); 105 | if (maxCardinalities.LiteralNodes().Count() == 1 && 106 | maxCardinalities.LiteralNodes().First().IsInteger()) 107 | { 108 | return int.Parse(maxCardinalities.LiteralNodes().First().Value, CultureInfo.InvariantCulture); 109 | } 110 | return 0; 111 | } 112 | } 113 | 114 | public readonly OntologyGraph _graph; 115 | public readonly OntologyClass _wrappedClass; 116 | 117 | /// 118 | /// Wrapper around an ontology class that exposes some methods particular to ontology restrictions. 119 | /// 120 | /// 121 | public OntologyRestriction(OntologyClass wrappedClass) 122 | { 123 | _wrappedClass = wrappedClass; 124 | _graph = wrappedClass.Graph as OntologyGraph; 125 | } 126 | 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /OWL2OAS/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using CommandLine; 8 | using VDS.RDF; 9 | using VDS.RDF.Nodes; 10 | using VDS.RDF.Ontology; 11 | using VDS.RDF.Parsing; 12 | using YamlDotNet.Serialization; 13 | 14 | namespace OWL2OAS 15 | { 16 | class Program 17 | { 18 | public enum EntityInclusionPolicy 19 | { 20 | DefaultInclude, 21 | DefaultExclude 22 | } 23 | 24 | public class Options 25 | { 26 | [Option('c', "ClassInclusionPolicy", Default = EntityInclusionPolicy.DefaultInclude, HelpText = "Whether to include all classes by default (overridden by o2o:included annotation). Valid options: DefaultInclude or DefaultExclude.")] 27 | public EntityInclusionPolicy ClassInclusionPolicy { get; set; } 28 | [Option('p', "PropertyInclusionPolicy", Default = EntityInclusionPolicy.DefaultInclude, HelpText = "Whether to include all properties by default (overridden by o2o:included annotation). Valid options: DefaultInclude or DefaultExclude.")] 29 | public EntityInclusionPolicy PropertyInclusionPolicy { get; set; } 30 | [Option('n', "no-imports", Required = false, HelpText = "Sets program to not follow owl:Imports declarations.")] 31 | public bool NoImports { get; set; } 32 | [Option('s', "server", Default = "http://localhost:8080/", Required = false, HelpText = "The server URL (where presumably an API implementation is running).")] 33 | public string Server { get; set; } 34 | [Option('f', "file-path", Required = true, HelpText = "The path to the on-disk root ontology file to translate.", SetName = "fileOntology")] 35 | public string FilePath { get; set; } 36 | [Option('u', "uri-path", Required = true, HelpText = "The URI of the root ontology file to translate.", SetName = "uriOntology")] 37 | public string UriPath { get; set; } 38 | [Option('o', "outputPath", Required = true, HelpText = "The path at which to put the generated OAS file.")] 39 | public string OutputPath { get; set; } 40 | } 41 | 42 | /// 43 | /// Custom comparer for Ontology objects, based on W3C OWL2 specification for version IRIs. 44 | /// See https://www.w3.org/TR/owl2-syntax/#Ontology_IRI_and_Version_IRI 45 | /// 46 | class OntologyComparer : IEqualityComparer 47 | { 48 | 49 | public bool Equals(Ontology x, Ontology y) 50 | { 51 | return 52 | !x.HasVersionIri() && !y.HasVersionIri() && (x.GetIri() == y.GetIri()) || 53 | x.HasVersionIri() && y.HasVersionIri() && (x.GetIri() == y.GetIri()) && (x.GetVersionIri() == y.GetVersionIri()); 54 | } 55 | 56 | // Method borrowed from https://stackoverflow.com/a/263416 57 | public int GetHashCode(Ontology x) 58 | { 59 | // Generate partial hashes from identify-carrying fields, i.e., ontology IRI 60 | // and version IRI; if no version IRI exists, default to partial hash of 0. 61 | int oidHash = x.GetIri().GetHashCode(); 62 | int vidHash = x.HasVersionIri() ? x.GetVersionIri().GetHashCode() : 0; 63 | 64 | // 65 | int hash = 23; 66 | hash = hash * 37 + oidHash; 67 | hash = hash * 37 + vidHash; 68 | return hash; 69 | } 70 | } 71 | 72 | /// 73 | /// The ontology being parsed. 74 | /// 75 | private static Ontology rootOntology; 76 | 77 | /// 78 | /// The joint ontology graph into which all imported ontologies are merged 79 | /// and upon which this tool subsequently operates. 80 | /// 81 | private static readonly OntologyGraph _ontologyGraph = new OntologyGraph(); 82 | 83 | /// 84 | /// The generated output OAS document. 85 | /// 86 | private static OASDocument _document; 87 | 88 | /// 89 | /// Set of transitively imported child ontologies. 90 | /// 91 | private static readonly HashSet importedOntologies = new HashSet(new OntologyComparer()); 92 | 93 | private static readonly Dictionary> requiredPropertiesForEachClass = new Dictionary>(); 94 | 95 | private static readonly Dictionary namespacePrefixes = new Dictionary(); 96 | 97 | // Used in string handling etc 98 | private static readonly CultureInfo invariantCulture = CultureInfo.InvariantCulture; 99 | 100 | // Various configuration fields 101 | private static string _server; 102 | private static bool _noImports; 103 | private static bool _localOntology; 104 | private static string _ontologyPath; 105 | private static bool _defaultIncludeClasses; 106 | private static bool _defaultIncludeProperties; 107 | private static string _outputPath; 108 | 109 | /// 110 | /// Dictionary mapping some common XSD data types to corresponding OSA data types and formats, see 111 | /// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypeFormat 112 | /// 113 | static readonly Dictionary xsdOsaMappings = new Dictionary 114 | { 115 | {"boolean",("boolean","") }, 116 | {"byte",("string","byte") }, 117 | {"base64Binary",("string","byte") }, 118 | {"dateTime",("string","date-time") }, 119 | {"dateTimeStamp",("string","date-time") }, 120 | {"double",("number","double") }, 121 | {"float",("number","float") }, 122 | {"int",("integer","int32") }, 123 | {"integer",("integer","int32") }, 124 | {"long",("integer","int64") }, 125 | {"string",("string","") } 126 | }; 127 | 128 | /// 129 | /// A struct representing cardinality constraints on a property. 130 | /// 131 | public struct PropertyCardinalityConstraints 132 | { 133 | public int min; 134 | public int max; 135 | public int exactly; 136 | public bool AllowsMultiple() 137 | { 138 | return !MaxOne(); 139 | } 140 | public bool MaxOne() 141 | { 142 | return (exactly == 1 || max == 1); 143 | } 144 | public bool IsRequired() 145 | { 146 | return (min == 1 || exactly == 1); 147 | } 148 | } 149 | 150 | /// 151 | /// Checks whether an ontology class or property should be included in the output specification, based on a) the run time 152 | /// "ClassInclusionPolicy" and "PropertyInclusionPolicy" options, and b) any explicit annotations, if present, on the resource 153 | /// in question using the https://karlhammar.com/owl2oas/o2o.owl#included annotation property. 154 | /// 155 | /// 156 | /// True iff the resource is annotated with included=true OR the relevant entity inclusion policy is DefaultInclude AND there is no included=false annotation on the entity. 157 | private static bool IsIncluded(OntologyResource resource) 158 | { 159 | // Do not include deprecated entities 160 | if (resource.IsDeprecated()) 161 | { 162 | return false; 163 | } 164 | 165 | // Check which inclusion policy applies; also, if resource is not class or property, throw exception right away 166 | bool includeByDefault; 167 | switch (resource) 168 | { 169 | case OntologyClass oClass: 170 | includeByDefault = _defaultIncludeClasses; 171 | break; 172 | case OntologyProperty oProp: 173 | includeByDefault = _defaultIncludeProperties; 174 | break; 175 | default: 176 | throw new RdfException($"Resource {resource} is neither an OntologyClass nor OntologyResource."); 177 | } 178 | 179 | // Check for existence of an o2o:included annotation; if so return based on this 180 | IUriNode includeAnnotationProperty = rootOntology.OntologyGraph().CreateUriNode(VocabularyHelper.O2O.included); 181 | IEnumerable includeAnnotationValues = resource.GetNodesViaProperty(includeAnnotationProperty).LiteralNodes(); 182 | if (includeAnnotationValues.Count() == 1) 183 | { 184 | bool resourceIncluded = includeAnnotationValues.First().AsValuedNode().AsBoolean(); 185 | return resourceIncluded; 186 | } 187 | 188 | // If no annotation, go by the default policy 189 | return includeByDefault; 190 | } 191 | 192 | static void Main(string[] args) 193 | { 194 | Parser.Default.ParseArguments(args) 195 | .WithParsed(o => 196 | { 197 | _noImports = o.NoImports; 198 | _server = o.Server; 199 | _outputPath = o.OutputPath; 200 | if (o.FilePath != null) 201 | { 202 | _localOntology = true; 203 | _ontologyPath = o.FilePath; 204 | } 205 | else 206 | { 207 | _localOntology = false; 208 | _ontologyPath = o.UriPath; 209 | } 210 | _defaultIncludeClasses = (o.ClassInclusionPolicy == EntityInclusionPolicy.DefaultInclude ? true : false); 211 | _defaultIncludeProperties = (o.PropertyInclusionPolicy == EntityInclusionPolicy.DefaultInclude ? true : false); 212 | }) 213 | .WithNotParsed((errs) => 214 | { 215 | Environment.Exit(1); 216 | }); 217 | 218 | // Clear cache from any prior runs 219 | UriLoader.Cache.Clear(); 220 | 221 | // Load ontology graph from local or remote path 222 | if (_localOntology) 223 | { 224 | FileLoader.Load(_ontologyGraph, _ontologyPath); 225 | } 226 | else 227 | { 228 | UriLoader.Load(_ontologyGraph, new Uri(_ontologyPath)); 229 | } 230 | 231 | // Get the main ontology defined in the graph. 232 | rootOntology = _ontologyGraph.GetOntology(); 233 | 234 | // If configured for it, parse owl:Imports transitively 235 | if (!_noImports) 236 | { 237 | foreach (Ontology import in rootOntology.Imports) 238 | { 239 | LoadImport(import); 240 | } 241 | } 242 | 243 | // Create OAS object, create OAS info header, server block, (empty) components/schemas structure, and LoadedOntologies endpoint 244 | _document = new OASDocument 245 | { 246 | info = GenerateDocumentInfo(), 247 | servers = new List> { new Dictionary { { "url", _server } } }, 248 | components = new OASDocument.Components(), 249 | paths = new Dictionary 250 | { 251 | { "/LoadedOntologies", GenerateLoadedOntologiesPath() } 252 | } 253 | }; 254 | 255 | // Parse OWL classes.For each class, create a schema and a path 256 | GenerateAtomicClassSchemas(); 257 | GenerateClassPaths(); 258 | 259 | // Generate and add the Context schema 260 | // This is done after class schemas are generated, in case the former causes new prefixes to be added 261 | // to the context 262 | _document.components.schemas.Add("Context", GenerateContextSchema()); 263 | 264 | // Dispose all open graphs 265 | _ontologyGraph.Dispose(); 266 | 267 | // Dump output as YAML 268 | var stringBuilder = new StringBuilder(); 269 | var serializerBuilder = new SerializerBuilder().DisableAliases(); 270 | var serializer = serializerBuilder.Build(); 271 | stringBuilder.AppendLine(serializer.Serialize(_document)); 272 | File.WriteAllText(_outputPath, stringBuilder.ToString()); 273 | } 274 | 275 | private static OASDocument.Path GenerateLoadedOntologiesPath() 276 | { 277 | OASDocument.Path loadedOntologiesPath = new OASDocument.Path 278 | { 279 | get = new OASDocument.Operation 280 | { 281 | summary = "Get the set of ontologies that were imported by the root ontology when the API was generated.", 282 | responses = new Dictionary 283 | { 284 | { "200", new OASDocument.Response 285 | { 286 | description = "A list of ontologies used to generate this API. Note that while the prefix names used here correspond with the ones given in the JSON-LD @context for the supported data types, the prefix mapping in the API " + 287 | "is based on the Ontology IRIs given in those @context blocks, which may differ from the values given here (which give priority to version IRIs).", 288 | content = new Dictionary 289 | { 290 | { "application/json", new OASDocument.Content 291 | { 292 | schema = GenerateLoadedOntologiesSchema() 293 | } 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | }; 301 | return loadedOntologiesPath; 302 | } 303 | 304 | private static OASDocument.Schema GenerateLoadedOntologiesSchema() 305 | { 306 | OASDocument.ComplexSchema loadedOntologiesSchema = new OASDocument.ComplexSchema 307 | { 308 | properties = new Dictionary(), 309 | required = new List() 310 | }; 311 | 312 | OASDocument.PrimitiveSchema rootOntologySchema = new OASDocument.PrimitiveSchema 313 | { 314 | type = "string", 315 | format = "uri", 316 | Enumeration = new string[] { rootOntology.GetVersionOrOntologyIri().ToString() } 317 | }; 318 | loadedOntologiesSchema.properties.Add("", rootOntologySchema); 319 | loadedOntologiesSchema.required.Add(""); 320 | 321 | // Add each imported ontology to the loaded ontologies schema 322 | foreach (Ontology importedOntology in importedOntologies) 323 | { 324 | OASDocument.PrimitiveSchema importedOntologySchema = new OASDocument.PrimitiveSchema 325 | { 326 | type = "string", 327 | format = "uri", 328 | Enumeration = new string[] { importedOntology.GetVersionOrOntologyIri().ToString() } 329 | }; 330 | 331 | // Fetch shortname as generated at import load time 332 | string ontologyShortname = namespacePrefixes[importedOntology.GetIri()]; 333 | loadedOntologiesSchema.properties.Add(ontologyShortname, importedOntologySchema); 334 | loadedOntologiesSchema.required.Add(ontologyShortname); 335 | } 336 | 337 | return loadedOntologiesSchema; 338 | } 339 | 340 | private static OASDocument.Schema GenerateContextSchema() 341 | { 342 | // Set @context/@vocab based on the ontology IRI 343 | OASDocument.PrimitiveSchema vocabularySchema = new OASDocument.PrimitiveSchema 344 | { 345 | type = "string", 346 | format = "uri", 347 | DefaultValue = rootOntology.GetIri().ToString() 348 | }; 349 | // Set @context/@base (default data namespace) 350 | OASDocument.PrimitiveSchema baseNamespaceSchema = new OASDocument.PrimitiveSchema 351 | { 352 | type = "string", 353 | format = "uri" 354 | }; 355 | // Hardcoded Hydra 356 | OASDocument.PrimitiveSchema hydraSchema = new OASDocument.PrimitiveSchema 357 | { 358 | type = "string", 359 | format = "uri", 360 | DefaultValue = "http://www.w3.org/ns/hydra/core#" 361 | }; 362 | // Hardcoded rdfs:label for @context 363 | OASDocument.PrimitiveSchema labelContextSchema = new OASDocument.PrimitiveSchema 364 | { 365 | type = "string", 366 | format = "uri", 367 | DefaultValue = VocabularyHelper.RDFS.label.ToString() 368 | }; 369 | // Mash it all together into a @context block 370 | OASDocument.ComplexSchema contextSchema = new OASDocument.ComplexSchema 371 | { 372 | required = new List { "@vocab", "@base", "hydra" }, 373 | properties = new Dictionary { 374 | { "@vocab", vocabularySchema }, 375 | { "@base", baseNamespaceSchema }, 376 | { "hydra", hydraSchema }, 377 | { "label", labelContextSchema } 378 | } 379 | }; 380 | // Add each prefix to the @context (sorting by shortname, e.g., dictionary value, for usability) 381 | List> prefixMappingsList = namespacePrefixes.ToList(); 382 | prefixMappingsList.Sort((pair1, pair2) => string.CompareOrdinal(pair1.Value, pair2.Value)); 383 | foreach (KeyValuePair prefixMapping in prefixMappingsList) 384 | { 385 | OASDocument.PrimitiveSchema importedVocabularySchema = new OASDocument.PrimitiveSchema 386 | { 387 | type = "string", 388 | format = "uri", 389 | DefaultValue = prefixMapping.Key.ToString() 390 | }; 391 | contextSchema.properties.Add(prefixMapping.Value, importedVocabularySchema); 392 | contextSchema.required.Add(prefixMapping.Value); 393 | } 394 | 395 | return contextSchema; 396 | } 397 | 398 | private static OASDocument.Info GenerateDocumentInfo() 399 | { 400 | OASDocument.Info docInfo = new OASDocument.Info(); 401 | 402 | OntologyGraph rootOntologyGraph = rootOntology.OntologyGraph(); 403 | 404 | // Check for mandatory components (dc:title, version info, cc:license). 405 | IUriNode dcTitle = rootOntologyGraph.CreateUriNode(VocabularyHelper.DC.title); 406 | if (!rootOntology.GetNodesViaProperty(dcTitle).LiteralNodes().Any()) 407 | { 408 | throw new RdfException($"Ontology <{rootOntology}> does not have an annotation."); 409 | } 410 | if (!rootOntology.VersionInfo.Any()) 411 | { 412 | throw new RdfException($"Ontology <{rootOntology}> does not have an annotation."); 413 | } 414 | IUriNode ccLicense = rootOntologyGraph.CreateUriNode(VocabularyHelper.CC.license); 415 | if (!rootOntology.GetNodesViaProperty(ccLicense).Any(objNode => objNode.IsLiteral() || objNode.IsUri())) 416 | { 417 | throw new RdfException($"Ontology <{rootOntology}> does not have an annotation that is a URI or literal."); 418 | } 419 | docInfo.title = rootOntology.GetNodesViaProperty(dcTitle).LiteralNodes().OrderBy(title => title.HasLanguage()).First().Value; 420 | docInfo.Version = rootOntology.VersionInfo.OrderBy(versionInfo => versionInfo.HasLanguage()).First().Value; 421 | docInfo.license = new OASDocument.License(); 422 | INode licenseNode = rootOntology.GetNodesViaProperty(ccLicense).OrderBy(node => node.NodeType).First(); 423 | if (licenseNode.IsUri()) 424 | { 425 | docInfo.license.name = ((UriNode)licenseNode).GetLocalName(); 426 | docInfo.license.url = ((UriNode)licenseNode).Uri.ToString(); 427 | } 428 | else 429 | { 430 | docInfo.license.name = ((LiteralNode)licenseNode).Value; 431 | } 432 | 433 | // Non-mandatory info components, e.g., rdfs:comment 434 | IUriNode dcDescription = rootOntologyGraph.CreateUriNode(VocabularyHelper.DC.description); 435 | if (rootOntology.GetNodesViaProperty(dcDescription).LiteralNodes().Any()) 436 | { 437 | ILiteralNode ontologyDescriptionLiteral = rootOntology.GetNodesViaProperty(dcDescription).LiteralNodes().OrderBy(description => description.HasLanguage()).First(); 438 | string ontologyDescription = ontologyDescriptionLiteral.Value.Trim().Replace("\r\n", "\n", StringComparison.Ordinal).Replace("\n", "
", StringComparison.Ordinal); 439 | docInfo.description = $"The documentation below is automatically extracted from a annotation on the ontology {rootOntology}:

*{ontologyDescription}*"; 440 | } 441 | 442 | return docInfo; 443 | } 444 | 445 | private static string GetKeyNameForResource(OntologyResource resource) 446 | { 447 | if (!resource.IsNamed()) 448 | { 449 | throw new RdfException($"Could not generate key name for OntologyResource '{resource}'; resource is anonymous."); 450 | } 451 | 452 | Uri resourceNamespace = resource.GetNamespace(); 453 | 454 | // If the concept is defined in the root ontology (which is default @vocab), return the local identifier 455 | if (resourceNamespace.Equals(rootOntology.GetIri())) 456 | { 457 | return resource.GetLocalName(); 458 | } 459 | 460 | // If the resource is in an ontology that has been parsed and thus added to known namespace prefixes, return prefixed qname 461 | if (namespacePrefixes.ContainsKey(resourceNamespace)) 462 | { 463 | return $"{namespacePrefixes[resourceNamespace]}:{resource.GetLocalName()}"; 464 | } 465 | 466 | // Fallback option -- add this thing to the namespace mapper and return it 467 | char[] trimChars = { '#', '/' }; 468 | string namespaceShortName = resourceNamespace.ToString().Trim(trimChars).Split('/').Last(); 469 | namespacePrefixes[resourceNamespace] = namespaceShortName; 470 | return $"{namespacePrefixes[resourceNamespace]}:{resource.GetLocalName()}"; 471 | } 472 | 473 | private static string GetEndpointName(OntologyResource cls) 474 | { 475 | IUriNode endpoint = _ontologyGraph.CreateUriNode(VocabularyHelper.O2O.endpoint); 476 | IEnumerable endpoints = cls.GetNodesViaProperty(endpoint).LiteralNodes(); 477 | if (endpoints.Any()) 478 | { 479 | return endpoints.First().AsValuedNode().AsString(); 480 | } 481 | return GetKeyNameForResource(cls); 482 | } 483 | 484 | private static void GenerateAtomicClassSchemas() 485 | { 486 | // Iterate over all non-deprecated classes that are either explicitly included, or that have subclasses that are included 487 | // The latter is to ensure that schema subsumption using allOf works 488 | foreach (OntologyClass oClass in _ontologyGraph.OwlClasses.Where(oClass => oClass.IsNamed() && 489 | !oClass.IsDeprecated() && 490 | (IsIncluded(oClass) || oClass.SubClasses.Any(subClass => IsIncluded(subClass))))) 491 | { 492 | // Get key name for API 493 | string classLabel = GetKeyNameForResource(oClass); 494 | 495 | // Create schema for class and corresponding properties dict 496 | OASDocument.ComplexSchema schema = new OASDocument.ComplexSchema(); 497 | schema.properties = new Dictionary(); 498 | 499 | // Set up the required properties set, used in subsequently generating HTTP operations 500 | requiredPropertiesForEachClass.Add(classLabel, new HashSet()); 501 | 502 | // Add @id for all entries 503 | OASDocument.PrimitiveSchema idSchema = new OASDocument.PrimitiveSchema(); 504 | idSchema.type = "string"; 505 | schema.properties.Add("@id", idSchema); 506 | 507 | // Add @type for all entries 508 | OASDocument.PrimitiveSchema typeSchema = new OASDocument.PrimitiveSchema 509 | { 510 | type = "string", 511 | DefaultValue = GetKeyNameForResource(oClass) 512 | }; 513 | schema.properties.Add("@type", typeSchema); 514 | 515 | // @context is mandatory 516 | schema.required = new List() { "@context" }; 517 | 518 | // Label is an option for all entries 519 | OASDocument.PrimitiveSchema labelSchema = new OASDocument.PrimitiveSchema(); 520 | labelSchema.type = "string"; 521 | schema.properties.Add("label", labelSchema); 522 | 523 | // Iterate over all (local) relationships and add them as properties 524 | foreach (Relationship relationship in oClass.GetRelationships().Where(relationship => 525 | IsIncluded(relationship.Property) && 526 | (relationship.Property.IsObjectProperty() || relationship.Property.IsDataProperty()) && 527 | !relationship.Property.IsDeprecated() && 528 | !relationship.Target.IsDeprecated() && 529 | !relationship.Target.IsOwlThing() && 530 | !relationship.Target.IsRdfsLiteral() 531 | )) 532 | { 533 | OntologyClass range = relationship.Target;// property.Ranges.First(); 534 | OntologyProperty property = relationship.Property; 535 | 536 | // Used for lookups against constraints dict 537 | UriNode propertyNode = ((UriNode)property.Resource); 538 | 539 | // Used to allocate property to schema.properties dictionary 540 | string propertyLabel = GetKeyNameForResource(property); 541 | 542 | // The return value: a property block to be added to the output document 543 | OASDocument.Schema outputSchema; 544 | 545 | // Check if multiple values are allowed for this property. By default they are. 546 | //bool propertyAllowsMultipleValues = !property.IsFunctional(); 547 | 548 | // If this is a data property 549 | if (property.IsDataProperty()) 550 | { 551 | // Set up the (possibly later on nested) property block 552 | OASDocument.PrimitiveSchema dataPropertySchema = new OASDocument.PrimitiveSchema(); 553 | 554 | // Fall back to string representation for unknown types 555 | dataPropertySchema.type = "string"; 556 | 557 | // Check that range is an XSD type that can be parsed into 558 | // an OAS type and format (note: not all XSD types are covered) 559 | if (range.IsXsdDatatype() || range.IsSimpleXsdWrapper()) { 560 | string rangeXsdType = ""; 561 | if (range.IsXsdDatatype()) { 562 | rangeXsdType = ((UriNode)range.Resource).GetLocalName(); 563 | } 564 | else { 565 | rangeXsdType = range.EquivalentClasses.First().GetUriNode().GetLocalName(); 566 | } 567 | if (xsdOsaMappings.ContainsKey(rangeXsdType)) 568 | { 569 | dataPropertySchema.type = xsdOsaMappings[rangeXsdType].Item1; 570 | string format = xsdOsaMappings[rangeXsdType].Item2; 571 | if (format.Length > 0) 572 | { 573 | dataPropertySchema.format = format; 574 | } 575 | } 576 | } 577 | 578 | // Assign return value 579 | outputSchema = dataPropertySchema; 580 | } 581 | else 582 | { 583 | // This is an Object property 584 | // Set up the (possibly later on nested) property block 585 | OASDocument.Schema uriPropertySchema; 586 | 587 | // Set the type of the property; locally defined named classes can be either URI or full schema representation 588 | uriPropertySchema = new OASDocument.ComplexSchema 589 | { 590 | properties = new Dictionary { 591 | { "@id", new OASDocument.PrimitiveSchema { type = "string" } }, 592 | { "@type", new OASDocument.PrimitiveSchema { type = "string", DefaultValue = GetKeyNameForResource(range) } } 593 | }, 594 | required = new List { "@id" } 595 | }; 596 | 597 | outputSchema = uriPropertySchema; 598 | } 599 | 600 | if (relationship.MinimumCount > 0) { 601 | requiredPropertiesForEachClass[classLabel].Add(propertyLabel); 602 | } 603 | 604 | 605 | if (property.IsFunctional() || relationship.ExactCount == 1) 606 | { 607 | schema.properties[propertyLabel] = outputSchema; 608 | } 609 | else 610 | { 611 | OASDocument.ArraySchema propertyArraySchema = new OASDocument.ArraySchema(); 612 | propertyArraySchema.items = outputSchema; 613 | if (relationship.MinimumCount.HasValue) 614 | { 615 | propertyArraySchema.minItems = relationship.MinimumCount.Value; 616 | } 617 | if (relationship.MaximumCount.HasValue) 618 | { 619 | propertyArraySchema.maxItems = relationship.MaximumCount.Value; 620 | } 621 | schema.properties[propertyLabel] = propertyArraySchema; 622 | 623 | } 624 | } 625 | 626 | IUriNode rdfsSubClassOf = _ontologyGraph.CreateUriNode(VocabularyHelper.RDFS.subClassOf); 627 | IEnumerable namedSuperClasses = oClass.DirectSuperClasses.Where(superClass => 628 | superClass.IsNamed() && 629 | !superClass.IsOwlThing() && 630 | !superClass.IsDeprecated() && 631 | !PropertyAssertionIsDeprecated(oClass.GetUriNode(), rdfsSubClassOf, superClass.GetUriNode()) 632 | ); 633 | if (namedSuperClasses.Any()) 634 | { 635 | OASDocument.AllOfSchema inheritanceSchema = new OASDocument.AllOfSchema(); 636 | OASDocument.Schema[] superClassSchemaReferences = namedSuperClasses.Select(superClass => new OASDocument.ReferenceSchema(GetKeyNameForResource(superClass))).ToArray(); 637 | inheritanceSchema.allOf = new OASDocument.Schema[namedSuperClasses.Count() + 1]; 638 | for (int i = 0; i < namedSuperClasses.Count(); i++) 639 | { 640 | inheritanceSchema.allOf[i] = superClassSchemaReferences[i]; 641 | } 642 | inheritanceSchema.allOf[namedSuperClasses.Count()] = schema; 643 | _document.components.schemas.Add(classLabel.Replace(":", "_", StringComparison.Ordinal), inheritanceSchema); 644 | } 645 | else 646 | { 647 | _document.components.schemas.Add(classLabel.Replace(":", "_", StringComparison.Ordinal), schema); 648 | } 649 | } 650 | } 651 | 652 | // TODO: move this into the DotNetRdfExtensions class 653 | private static bool PropertyAssertionIsDeprecated(INode subj, IUriNode pred, INode obj) 654 | { 655 | IUriNode owlAnnotatedSource = _ontologyGraph.CreateUriNode(VocabularyHelper.OWL.annotatedSource); 656 | IUriNode owlAnnotatedProperty = _ontologyGraph.CreateUriNode(VocabularyHelper.OWL.annotatedProperty); 657 | IUriNode owlAnnotatedTarget = _ontologyGraph.CreateUriNode(VocabularyHelper.OWL.annotatedTarget); 658 | IUriNode owlDeprecated = _ontologyGraph.CreateUriNode(VocabularyHelper.OWL.deprecated); 659 | 660 | IEnumerable axiomAnnotations = _ontologyGraph.Nodes 661 | .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedSource, subj))) 662 | .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedProperty, pred))) 663 | .Where(node => _ontologyGraph.ContainsTriple(new Triple(node, owlAnnotatedTarget, obj))); 664 | 665 | foreach (INode axiomAnnotation in axiomAnnotations) 666 | { 667 | foreach (Triple deprecationAssertion in _ontologyGraph.GetTriplesWithSubjectPredicate(axiomAnnotation, owlDeprecated).Where(trip => trip.Object.NodeType == NodeType.Literal)) 668 | { 669 | IValuedNode deprecationValue = deprecationAssertion.Object.AsValuedNode(); 670 | try 671 | { 672 | if (deprecationValue.AsBoolean()) 673 | { 674 | return true; 675 | } 676 | } 677 | catch 678 | { 679 | 680 | } 681 | } 682 | } 683 | return false; 684 | } 685 | 686 | private static void GenerateClassPaths() 687 | { 688 | // Iterate over all classes 689 | foreach (OntologyClass oClass in _ontologyGraph.OwlClasses.Where(oClass => oClass.IsNamed() && IsIncluded(oClass))) 690 | { 691 | // Get key name for API 692 | string classLabel = GetKeyNameForResource(oClass); 693 | string endpointName = GetEndpointName(oClass); 694 | 695 | // Create paths and corresponding operations for class 696 | _document.paths.Add($"/{endpointName}", new OASDocument.Path 697 | { 698 | get = GenerateGetEntitiesOperation(endpointName, classLabel, oClass), 699 | post = GeneratePostEntityOperation(endpointName, classLabel) 700 | }); 701 | _document.paths.Add($"/{endpointName}/{{id}}", new OASDocument.Path 702 | { 703 | get = GenerateGetEntityByIdOperation(endpointName, classLabel), 704 | patch = GeneratePatchToIdOperation(endpointName, classLabel), 705 | put = GeneratePutToIdOperation(endpointName, classLabel), 706 | delete = GenerateDeleteByIdOperation(endpointName, classLabel) 707 | }); 708 | } 709 | } 710 | 711 | private static OASDocument.Operation GenerateDeleteByIdOperation(string endpointName, string classLabel) 712 | { 713 | OASDocument.Operation deleteOperation = new OASDocument.Operation(); 714 | deleteOperation.summary = $"Delete a '{classLabel}' object."; 715 | deleteOperation.tags.Add(endpointName); 716 | 717 | // Add the ID parameter 718 | OASDocument.Parameter idParameter = new OASDocument.Parameter 719 | { 720 | name = "id", 721 | description = $"Id of '{classLabel}' to delete.", 722 | InField = OASDocument.Parameter.InFieldValues.path, 723 | required = true, 724 | schema = new OASDocument.PrimitiveSchema 725 | { 726 | type = "string" 727 | } 728 | }; 729 | deleteOperation.parameters.Add(idParameter); 730 | 731 | // Create each of the HTTP response types 732 | OASDocument.Response response404 = new OASDocument.Response(); 733 | response404.description = $"An object of type '{classLabel}' with the specified ID was not found."; 734 | deleteOperation.responses.Add("404", response404); 735 | 736 | OASDocument.Response response500 = new OASDocument.Response(); 737 | response500.description = "Internal Server Error"; 738 | deleteOperation.responses.Add("500", response500); 739 | 740 | OASDocument.Response response200 = new OASDocument.Response(); 741 | response200.description = $"'{classLabel}' entity was successfully deleted."; 742 | deleteOperation.responses.Add("200", response200); 743 | 744 | return deleteOperation; 745 | } 746 | 747 | private static OASDocument.Operation GeneratePostEntityOperation(string endpointName, string classLabel) 748 | { 749 | OASDocument.Operation postOperation = new OASDocument.Operation(); 750 | postOperation.summary = $"Create a new '{classLabel}' object."; 751 | postOperation.tags.Add(endpointName); 752 | 753 | // Create request body 754 | OASDocument.RequestBody body = new OASDocument.RequestBody 755 | { 756 | description = $"New '{classLabel}' entity that is to be added.", 757 | required = true, 758 | content = new Dictionary 759 | { 760 | { 761 | "application/ld+json", new OASDocument.Content 762 | { 763 | schema = MergeAtomicSchemaWithContext(classLabel) 764 | } 765 | } 766 | } 767 | }; 768 | postOperation.requestBody = body; 769 | 770 | // Create each of the HTTP response types 771 | OASDocument.Response response500 = new OASDocument.Response(); 772 | response500.description = "Internal Server Error"; 773 | postOperation.responses.Add("500", response500); 774 | 775 | OASDocument.Response response400 = new OASDocument.Response(); 776 | response400.description = "Bad Request"; 777 | postOperation.responses.Add("400", response400); 778 | 779 | OASDocument.Response response201 = new OASDocument.Response(); 780 | response201.description = "Entity was successfully created (new representation returned)."; 781 | postOperation.responses.Add("201", response201); 782 | 783 | response201.content = new Dictionary(); 784 | OASDocument.Content content201 = new OASDocument.Content(); 785 | response201.content.Add("application/ld+json", content201); 786 | 787 | // Response is per previously defined schema 788 | content201.schema = MergeAtomicSchemaWithContextAndRequiredProperties(classLabel); 789 | 790 | return postOperation; 791 | } 792 | 793 | private static OASDocument.Operation GenerateGetEntityByIdOperation(string endpointName, string classLabel) 794 | { 795 | OASDocument.Operation getOperation = new OASDocument.Operation(); 796 | getOperation.summary = $"Get a specific '{classLabel}' object."; 797 | getOperation.tags.Add(endpointName); 798 | 799 | // Add the ID parameter 800 | OASDocument.Parameter idParameter = new OASDocument.Parameter 801 | { 802 | name = "id", 803 | description = $"Id of '{classLabel}' to return.", 804 | InField = OASDocument.Parameter.InFieldValues.path, 805 | required = true, 806 | schema = new OASDocument.PrimitiveSchema 807 | { 808 | type = "string" 809 | } 810 | }; 811 | getOperation.parameters.Add(idParameter); 812 | 813 | // Create each of the HTTP response types 814 | OASDocument.Response response404 = new OASDocument.Response(); 815 | response404.description = $"An object of type '{classLabel}' with the specified ID was not found."; 816 | getOperation.responses.Add("404", response404); 817 | 818 | OASDocument.Response response500 = new OASDocument.Response(); 819 | response500.description = "Internal Server Error"; 820 | getOperation.responses.Add("500", response500); 821 | 822 | OASDocument.Response response200 = new OASDocument.Response(); 823 | response200.description = $"A '{classLabel}' object."; 824 | getOperation.responses.Add("200", response200); 825 | 826 | response200.content = new Dictionary(); 827 | OASDocument.Content content200 = new OASDocument.Content(); 828 | response200.content.Add("application/ld+json", content200); 829 | 830 | // Response is per previously defined schema 831 | content200.schema = MergeAtomicSchemaWithContextAndRequiredProperties(classLabel); 832 | 833 | return getOperation; 834 | } 835 | 836 | private static OASDocument.Operation GenerateGetEntitiesOperation(string endpointName, string classLabel, OntologyClass oClass) 837 | { 838 | 839 | // Create Get 840 | OASDocument.Operation getOperation = new OASDocument.Operation(); 841 | getOperation.summary = "Get '" + classLabel + "' entities."; 842 | getOperation.tags.Add(endpointName); 843 | 844 | // Add pagination parameters 845 | getOperation.parameters.Add(new OASDocument.Parameter { ReferenceTo = "pageParam" }); 846 | getOperation.parameters.Add(new OASDocument.Parameter { ReferenceTo = "sizeParam" }); 847 | 848 | // Add sort param 849 | getOperation.parameters.Add(new OASDocument.Parameter { ReferenceTo = "sortParam" }); 850 | 851 | // Add parameters for each property field that can be expressed on this class 852 | foreach (OntologyProperty property in oClass.IsExhaustiveDomainOfUniques() 853 | .Where(property => property.IsDataProperty() || property.IsObjectProperty()) 854 | .Where(property => property.Ranges.Count() == 1) 855 | .Where(property => IsIncluded(property))) 856 | { 857 | string propertyLabel = GetKeyNameForResource(property); 858 | 859 | // Fall back to string representation and no format for object properties 860 | // abd data properties w/ unknown types 861 | string propertyType = "string"; 862 | string propertyFormat = ""; 863 | 864 | // Check that range is an XSD type that can be parsed into 865 | // an OAS type and format (note: not all XSD types are covered) 866 | OntologyClass range = property.Ranges.First(); 867 | if (range.IsNamed()) { 868 | if (range.IsXsdDatatype() || range.IsSimpleXsdWrapper()) { 869 | string rangeXsdType = ""; 870 | if (range.IsXsdDatatype()) { 871 | rangeXsdType = ((UriNode)range.Resource).GetLocalName(); 872 | } 873 | else { 874 | rangeXsdType = range.EquivalentClasses.First().GetUriNode().GetLocalName(); 875 | } 876 | if (xsdOsaMappings.ContainsKey(rangeXsdType)) 877 | { 878 | propertyType = xsdOsaMappings[rangeXsdType].Item1; 879 | string format = xsdOsaMappings[rangeXsdType].Item2; 880 | if (format.Length > 0) 881 | { 882 | propertyFormat = format; 883 | } 884 | } 885 | } 886 | } 887 | 888 | // Select a filter schema to use for parameter formats where it is applicable 889 | string filterSchema = ""; 890 | switch (propertyType) 891 | { 892 | case "string": 893 | filterSchema = propertyFormat switch 894 | { 895 | "date-time" => "DateTimeFilter", 896 | _ => "StringFilter", 897 | }; 898 | break; 899 | 900 | case "integer": 901 | filterSchema = "IntegerFilter"; 902 | break; 903 | 904 | case "number": 905 | filterSchema = "NumberFilter"; 906 | break; 907 | } 908 | 909 | // Base the property schema on the filter, if one was selected above 910 | // Otherwise, just do a simple type-based schema, possibly with format if one was found 911 | OASDocument.Schema propertySchema; 912 | if (filterSchema.Length > 0) 913 | { 914 | propertySchema = new OASDocument.ReferenceSchema(filterSchema); 915 | } 916 | else 917 | { 918 | if (propertyFormat.Length > 0) 919 | { 920 | propertySchema = new OASDocument.PrimitiveSchema 921 | { 922 | type = propertyType, 923 | format = propertyFormat 924 | }; 925 | } 926 | else 927 | { 928 | propertySchema = new OASDocument.PrimitiveSchema 929 | { 930 | type = propertyType 931 | }; 932 | } 933 | } 934 | 935 | OASDocument.Parameter parameter = new OASDocument.Parameter 936 | { 937 | name = propertyLabel, 938 | description = $"Filter value on property '{propertyLabel}'.", 939 | required = false, 940 | schema = propertySchema, 941 | InField = OASDocument.Parameter.InFieldValues.query 942 | }; 943 | 944 | if (filterSchema.Length > 0) 945 | { 946 | parameter.style = "deepObject"; 947 | } 948 | 949 | getOperation.parameters.Add(parameter); 950 | } 951 | 952 | // Create each of the HTTP response types 953 | OASDocument.Response response400 = new OASDocument.Response(); 954 | response400.description = "Bad Request"; 955 | getOperation.responses.Add("400", response400); 956 | 957 | OASDocument.Response response500 = new OASDocument.Response(); 958 | response500.description = "Internal Server Error"; 959 | getOperation.responses.Add("500", response500); 960 | 961 | OASDocument.Response response200 = new OASDocument.Response(); 962 | response200.description = "An array of '" + classLabel + "' objects."; 963 | getOperation.responses.Add("200", response200); 964 | 965 | response200.content = new Dictionary(); 966 | OASDocument.Content content200 = new OASDocument.Content(); 967 | response200.content.Add("application/ld+json", content200); 968 | 969 | // Generate schema with required fields propped on via allOf (if any required fields exist) 970 | OASDocument.Schema classSchemaWithRequiredProperties = MergeAtomicSchemaWithRequiredProperties(classLabel); 971 | 972 | // Generate wrapper Hydra schema (https://www.hydra-cg.com/spec/latest/core/) 973 | OASDocument.Schema hydraSchema = new OASDocument.AllOfSchema 974 | { 975 | allOf = new OASDocument.Schema[] 976 | { 977 | new OASDocument.ReferenceSchema("HydraCollectionWrapper"), 978 | new OASDocument.ComplexSchema 979 | { 980 | properties = new Dictionary 981 | { 982 | {"hydra:member", new OASDocument.ArraySchema { 983 | items = classSchemaWithRequiredProperties 984 | } } 985 | } 986 | } 987 | } 988 | }; 989 | 990 | // Wrap responses in array 991 | content200.schema = hydraSchema; 992 | 993 | // Return 994 | return getOperation; 995 | } 996 | 997 | private static OASDocument.Schema MergeAtomicSchemaWithRequiredProperties(string classLabel) 998 | { 999 | OASDocument.Schema itemSchema; 1000 | if (requiredPropertiesForEachClass[classLabel].Count == 0) 1001 | { 1002 | itemSchema = new OASDocument.ReferenceSchema(classLabel); 1003 | } 1004 | else 1005 | { 1006 | itemSchema = new OASDocument.AllOfSchema 1007 | { 1008 | allOf = new OASDocument.Schema[] { 1009 | new OASDocument.ReferenceSchema(classLabel), 1010 | new OASDocument.ComplexSchema { 1011 | required = requiredPropertiesForEachClass[classLabel].ToList() 1012 | } 1013 | } 1014 | }; 1015 | } 1016 | return itemSchema; 1017 | } 1018 | 1019 | private static OASDocument.Schema MergeAtomicSchemaWithContext(string classLabel) 1020 | { 1021 | OASDocument.AllOfSchema itemSchema = new OASDocument.AllOfSchema(); 1022 | OASDocument.ReferenceSchema classSchema = new OASDocument.ReferenceSchema(classLabel); 1023 | OASDocument.ReferenceSchema contextReferenceSchema = new OASDocument.ReferenceSchema("Context"); 1024 | OASDocument.ComplexSchema contextPropertySchema = new OASDocument.ComplexSchema 1025 | { 1026 | required = new List { "@context" }, 1027 | properties = new Dictionary() { { "@context", contextReferenceSchema } } 1028 | }; 1029 | 1030 | // Otherwise merge only with context 1031 | itemSchema.allOf = new OASDocument.Schema[] 1032 | { 1033 | contextPropertySchema, 1034 | classSchema 1035 | }; 1036 | return itemSchema; 1037 | } 1038 | 1039 | private static OASDocument.Schema MergeAtomicSchemaWithContextAndRequiredProperties(string classLabel) 1040 | { 1041 | OASDocument.AllOfSchema itemSchema = new OASDocument.AllOfSchema(); 1042 | OASDocument.ReferenceSchema classSchema = new OASDocument.ReferenceSchema(classLabel); 1043 | OASDocument.ReferenceSchema contextReferenceSchema = new OASDocument.ReferenceSchema("Context"); 1044 | OASDocument.ComplexSchema contextPropertySchema = new OASDocument.ComplexSchema 1045 | { 1046 | required = new List { "@context" }, 1047 | properties = new Dictionary() { { "@context", contextReferenceSchema } } 1048 | }; 1049 | 1050 | // If there are required properties, merge them also 1051 | if (requiredPropertiesForEachClass[classLabel].Count != 0) 1052 | { 1053 | OASDocument.ComplexSchema requiredPropertiesSchema = new OASDocument.ComplexSchema { required = requiredPropertiesForEachClass[classLabel].ToList() }; 1054 | itemSchema.allOf = new OASDocument.Schema[] 1055 | { 1056 | contextPropertySchema, 1057 | classSchema, 1058 | requiredPropertiesSchema 1059 | }; 1060 | return itemSchema; 1061 | } 1062 | 1063 | // Otherwise merge only with context 1064 | itemSchema.allOf = new OASDocument.Schema[] 1065 | { 1066 | contextPropertySchema, 1067 | classSchema 1068 | }; 1069 | return itemSchema; 1070 | } 1071 | 1072 | private static OASDocument.Operation GeneratePatchToIdOperation(string endpointName, string classLabel) 1073 | { 1074 | OASDocument.Operation patchOperation = new OASDocument.Operation(); 1075 | patchOperation.summary = $"Update a single property on a specific '{classLabel}' object."; 1076 | patchOperation.tags.Add(endpointName); 1077 | 1078 | // Add the ID parameter 1079 | OASDocument.Parameter idParameter = new OASDocument.Parameter 1080 | { 1081 | name = "id", 1082 | description = $"Id of '{classLabel}' to update.", 1083 | InField = OASDocument.Parameter.InFieldValues.path, 1084 | required = true, 1085 | schema = new OASDocument.PrimitiveSchema 1086 | { 1087 | type = "string" 1088 | } 1089 | }; 1090 | patchOperation.parameters.Add(idParameter); 1091 | 1092 | // Create patch schema 1093 | OASDocument.ReferenceSchema contextReferenceSchema = new OASDocument.ReferenceSchema("Context"); 1094 | OASDocument.ComplexSchema contextPropertySchema = new OASDocument.ComplexSchema 1095 | { 1096 | required = new List { "@context" }, 1097 | properties = new Dictionary() { { "@context", contextReferenceSchema } } 1098 | }; 1099 | 1100 | OASDocument.Schema patchSchema = new OASDocument.AllOfSchema 1101 | { 1102 | allOf = new OASDocument.Schema[] { 1103 | contextPropertySchema, 1104 | new OASDocument.ReferenceSchema(classLabel), 1105 | new OASDocument.ComplexSchema { 1106 | minProperties = 2, 1107 | maxProperties = 2 1108 | } 1109 | } 1110 | }; 1111 | 1112 | // Add request body 1113 | OASDocument.RequestBody body = new OASDocument.RequestBody 1114 | { 1115 | description = "A single JSON key-value pair (plus @context), indicating the property to update and its new value. Note that the Swagger UI does not properly show the size constraint on this parameter; but the underlying OpenAPI Specification document does.", 1116 | required = true, 1117 | content = new Dictionary 1118 | { 1119 | { 1120 | "application/ld+json", new OASDocument.Content 1121 | { 1122 | schema = patchSchema 1123 | } 1124 | } 1125 | } 1126 | }; 1127 | patchOperation.requestBody = body; 1128 | 1129 | // Create each of the HTTP response types 1130 | OASDocument.Response response400 = new OASDocument.Response(); 1131 | response400.description = "Bad Request"; 1132 | patchOperation.responses.Add("400", response400); 1133 | 1134 | OASDocument.Response response404 = new OASDocument.Response(); 1135 | response404.description = $"An object of type '{classLabel}' with the specified ID was not found."; 1136 | patchOperation.responses.Add("404", response404); 1137 | 1138 | OASDocument.Response response500 = new OASDocument.Response(); 1139 | response500.description = "Internal Server Error"; 1140 | patchOperation.responses.Add("500", response500); 1141 | 1142 | OASDocument.Response response200 = new OASDocument.Response(); 1143 | response200.description = "Entity was updated successfully (new representation returned)."; 1144 | patchOperation.responses.Add("200", response200); 1145 | 1146 | response200.content = new Dictionary(); 1147 | OASDocument.Content content200 = new OASDocument.Content(); 1148 | response200.content.Add("application/ld+json", content200); 1149 | 1150 | // Response is per previously defined schema 1151 | content200.schema = MergeAtomicSchemaWithContextAndRequiredProperties(classLabel); 1152 | 1153 | return patchOperation; 1154 | } 1155 | 1156 | private static OASDocument.Operation GeneratePutToIdOperation(string endpointName, string classLabel) 1157 | { 1158 | OASDocument.Operation putOperation = new OASDocument.Operation(); 1159 | putOperation.summary = $"Update an existing '{classLabel}' entity."; 1160 | putOperation.tags.Add(endpointName); 1161 | 1162 | // Add the ID parameter 1163 | OASDocument.Parameter idParameter = new OASDocument.Parameter 1164 | { 1165 | name = "id", 1166 | description = $"Id of '{classLabel}' to update.", 1167 | InField = OASDocument.Parameter.InFieldValues.path, 1168 | required = true, 1169 | schema = new OASDocument.PrimitiveSchema 1170 | { 1171 | type = "string" 1172 | } 1173 | }; 1174 | putOperation.parameters.Add(idParameter); 1175 | 1176 | // Add request body 1177 | OASDocument.RequestBody body = new OASDocument.RequestBody 1178 | { 1179 | description = $"Updated data for '{classLabel}' entity.", 1180 | required = true, 1181 | content = new Dictionary 1182 | { 1183 | { 1184 | "application/ld+json", new OASDocument.Content 1185 | { 1186 | schema = MergeAtomicSchemaWithContext(classLabel) 1187 | } 1188 | } 1189 | } 1190 | }; 1191 | putOperation.requestBody = body; 1192 | 1193 | // Create each of the HTTP response types 1194 | OASDocument.Response response400 = new OASDocument.Response(); 1195 | response400.description = "Bad Request"; 1196 | putOperation.responses.Add("400", response400); 1197 | 1198 | OASDocument.Response response404 = new OASDocument.Response(); 1199 | response404.description = $"An object of type '{classLabel}' with the specified ID was not found."; 1200 | putOperation.responses.Add("404", response404); 1201 | 1202 | OASDocument.Response response500 = new OASDocument.Response(); 1203 | response500.description = "Internal Server Error"; 1204 | putOperation.responses.Add("500", response500); 1205 | 1206 | OASDocument.Response response200 = new OASDocument.Response(); 1207 | response200.description = "Entity was updated successfully (new representation returned)."; 1208 | putOperation.responses.Add("200", response200); 1209 | 1210 | response200.content = new Dictionary(); 1211 | OASDocument.Content content200 = new OASDocument.Content(); 1212 | response200.content.Add("application/ld+json", content200); 1213 | 1214 | // Response is per previously defined schema 1215 | content200.schema = MergeAtomicSchemaWithContextAndRequiredProperties(classLabel); 1216 | 1217 | return putOperation; 1218 | } 1219 | 1220 | private static int GetMinCardinality(OntologyClass restriction) 1221 | { 1222 | OntologyGraph graph = restriction.Graph as OntologyGraph; 1223 | IUriNode someValuesFrom = graph.CreateUriNode(VocabularyHelper.OWL.someValuesFrom); 1224 | IUriNode minCardinality = graph.CreateUriNode(VocabularyHelper.OWL.minCardinality); 1225 | IUriNode minQualifiedCardinality = graph.CreateUriNode(VocabularyHelper.OWL.minQualifiedCardinality); 1226 | 1227 | IEnumerable minCardinalities = restriction.GetNodesViaProperty(minCardinality).Union(restriction.GetNodesViaProperty(minQualifiedCardinality)); 1228 | if (minCardinalities.LiteralNodes().Count() == 1 && 1229 | minCardinalities.LiteralNodes().First().IsInteger()) 1230 | { 1231 | return int.Parse(minCardinalities.LiteralNodes().First().Value, invariantCulture); 1232 | } 1233 | 1234 | if (restriction.GetNodesViaProperty(someValuesFrom).Count() == 1) 1235 | { 1236 | return 1; 1237 | } 1238 | 1239 | return 0; 1240 | } 1241 | 1242 | private static int GetExactCardinality(OntologyClass restriction) 1243 | { 1244 | OntologyGraph graph = restriction.Graph as OntologyGraph; 1245 | IUriNode cardinality = graph.CreateUriNode(VocabularyHelper.OWL.cardinality); 1246 | IUriNode qualifiedCardinality = graph.CreateUriNode(VocabularyHelper.OWL.qualifiedCardinality); 1247 | IUriNode onClass = graph.CreateUriNode(VocabularyHelper.OWL.onClass); 1248 | 1249 | IEnumerable exactCardinalities = restriction.GetNodesViaProperty(cardinality); 1250 | if (exactCardinalities.LiteralNodes().Count() == 1 && 1251 | exactCardinalities.LiteralNodes().First().IsInteger()) 1252 | { 1253 | return int.Parse(exactCardinalities.LiteralNodes().First().Value, invariantCulture); 1254 | } 1255 | 1256 | IEnumerable exactQualifiedCardinalities = restriction.GetNodesViaProperty(qualifiedCardinality); 1257 | if (exactQualifiedCardinalities.LiteralNodes().Count() == 1 && 1258 | exactQualifiedCardinalities.LiteralNodes().First().IsInteger()) 1259 | { 1260 | IEnumerable qualifierClasses = restriction.GetNodesViaProperty(onClass).UriNodes(); 1261 | if (qualifierClasses.Count() == 1 && qualifierClasses.First().Uri.Equals(VocabularyHelper.OWL.Thing)) 1262 | { 1263 | return int.Parse(exactQualifiedCardinalities.LiteralNodes().First().Value, invariantCulture); 1264 | } 1265 | } 1266 | 1267 | return 0; 1268 | } 1269 | 1270 | private static int GetMaxCardinality(OntologyClass restriction) 1271 | { 1272 | OntologyGraph graph = restriction.Graph as OntologyGraph; 1273 | IUriNode maxCardinality = graph.CreateUriNode(VocabularyHelper.OWL.maxCardinality); 1274 | IUriNode maxQualifiedCardinality = graph.CreateUriNode(VocabularyHelper.OWL.maxQualifiedCardinality); 1275 | IUriNode onClass = graph.CreateUriNode(VocabularyHelper.OWL.onClass); 1276 | 1277 | IEnumerable maxCardinalities = restriction.GetNodesViaProperty(maxCardinality); 1278 | if (maxCardinalities.LiteralNodes().Count() == 1 && 1279 | maxCardinalities.LiteralNodes().First().IsInteger()) 1280 | { 1281 | return int.Parse(maxCardinalities.LiteralNodes().First().Value, invariantCulture); 1282 | } 1283 | 1284 | IEnumerable maxQualifiedCardinalities = restriction.GetNodesViaProperty(maxQualifiedCardinality); 1285 | if (maxQualifiedCardinalities.LiteralNodes().Count() == 1 && 1286 | maxQualifiedCardinalities.LiteralNodes().First().IsInteger()) 1287 | { 1288 | IEnumerable qualifierClasses = restriction.GetNodesViaProperty(onClass).UriNodes(); 1289 | if (qualifierClasses.Count() == 1 && qualifierClasses.First().Uri.Equals(VocabularyHelper.OWL.Thing)) 1290 | { 1291 | return int.Parse(maxQualifiedCardinalities.LiteralNodes().First().Value, invariantCulture); 1292 | } 1293 | } 1294 | 1295 | return 0; 1296 | } 1297 | 1298 | /// 1299 | /// Loads imported ontologies transitively. Each imported ontology is added 1300 | /// to the static set importedOntologies. 1301 | /// 1302 | /// The ontology to import. 1303 | private static void LoadImport(Ontology importedOntology) 1304 | { 1305 | // We only deal with named ontologies 1306 | if (importedOntology.IsNamed()) 1307 | { 1308 | 1309 | // Parse and load ontology from the stated import URI 1310 | Uri importUri = importedOntology.GetIri(); 1311 | 1312 | //Uri importedOntologyUri = ((IUriNode)importedOntology.Resource).Uri; 1313 | OntologyGraph fetchedOntologyGraph = new OntologyGraph(); 1314 | 1315 | try 1316 | { 1317 | UriLoader.Load(fetchedOntologyGraph, importUri); 1318 | } 1319 | catch (RdfParseException e) 1320 | { 1321 | Console.Write(e.Message); 1322 | Console.Write(e.StackTrace); 1323 | } 1324 | 1325 | // Set up a new ontology metadata object from the retrieved ontology graph. 1326 | // This is needed since this ontology's self-defined IRI or version IRI often 1327 | // differs from the IRI through which it was imported (i.e., importedOntology in 1328 | // this method's signature), due to .htaccess redirects, version URIs, etc. 1329 | Ontology importedOntologyFromFetchedGraph = fetchedOntologyGraph.GetOntology(); 1330 | 1331 | // Only proceed if the retrieved ontology has an IRI 1332 | if (importedOntologyFromFetchedGraph.IsNamed()) 1333 | { 1334 | // Only proceed if we have not seen this fetched ontology before, otherwise we risk 1335 | // unecessary fetches and computation, and possibly import loops. 1336 | // Note that importedOntologies uses a custom comparer from DotNetRdfExtensions, 1337 | // since the Ontology class does not implement IComparable 1338 | if (!importedOntologies.Contains(importedOntologyFromFetchedGraph)) 1339 | { 1340 | // Add the fetched ontology to the namespace prefix index 1341 | // (tacking on _1, _2, etc. to the shortname if it exists since before, 1342 | // since we need all prefix names to be unique). 1343 | string importedOntologyShortname = importedOntologyFromFetchedGraph.GetShortName(); 1344 | int i = 1; 1345 | while (namespacePrefixes.ContainsValue(importedOntologyShortname)) 1346 | { 1347 | importedOntologyShortname = importedOntologyShortname.Split('_')[0] + "_" + i; 1348 | i++; 1349 | } 1350 | namespacePrefixes.Add(importedOntologyFromFetchedGraph.GetIri(), importedOntologyShortname); 1351 | 1352 | // Merge the fetch graph with the joint ontology graph the tool operates on 1353 | _ontologyGraph.Merge(fetchedOntologyGraph); 1354 | 1355 | // Add imported ontology to the global imports collection and traverse its 1356 | // import hierarchy transitively 1357 | importedOntologies.Add(importedOntologyFromFetchedGraph); 1358 | foreach (Ontology subImport in importedOntologyFromFetchedGraph.Imports) 1359 | { 1360 | LoadImport(subImport); 1361 | } 1362 | } 1363 | } 1364 | 1365 | // Dispose graph before returning 1366 | fetchedOntologyGraph.Dispose(); 1367 | } 1368 | } 1369 | } 1370 | } -------------------------------------------------------------------------------- /OWL2OAS/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "OWL2OAS": { 4 | "commandName": "Project", 5 | "commandLineArgs": "-c DefaultExclude -u https://w3id.org/rec/full/3.1.1/ > %HOMEPATH%/OWL2OAS_OUTPUT.yaml" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /OWL2OAS/Relationship.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using VDS.RDF; 5 | using VDS.RDF.Ontology; 6 | 7 | namespace OWL2OAS 8 | { 9 | /// 10 | /// A representation of the relationships (data properties or object properties) that can be expressed on an OWL class. 11 | /// TODO Here be gremlins! Thid code is ugly and written in a hurry. It should probably be signficantly refactored and cleaned up, 12 | /// if not removed entirely. The relationship merge logic is particularly hairy. 13 | /// 14 | public class Relationship 15 | { 16 | public OntologyProperty Property 17 | { get; } 18 | 19 | public OntologyClass Target 20 | { get; set; } 21 | 22 | public int? MinimumCount 23 | { get; set; } 24 | 25 | public int? MaximumCount 26 | { get; set; } 27 | 28 | public int? ExactCount 29 | { 30 | get 31 | { 32 | if (MinimumCount.HasValue && MaximumCount.HasValue && MinimumCount == MaximumCount) 33 | { 34 | return MinimumCount; 35 | } 36 | return null; 37 | } 38 | set 39 | { 40 | MinimumCount = value; 41 | MaximumCount = value; 42 | } 43 | } 44 | 45 | public Relationship(OntologyProperty property, OntologyClass target) 46 | { 47 | if (!property.IsNamed() || !(target.IsNamed() || target.IsDatatype())) 48 | { 49 | throw new ArgumentException("Only named properties and named or datatype targets allowed."); 50 | } 51 | Property = property; 52 | Target = target; 53 | } 54 | 55 | /// 56 | /// Merge another Relationship into this one. 57 | /// This only works when the properties of the relationships are identical; else an exception is thrown. 58 | /// If the two relationships are to different target classes of which one subsumes the other, then the 59 | /// most specific relationship is kept. If the classes do not subsume one another, OWL thing is set as target. 60 | /// If the relationship targets are the same, then the narrower cardinality restrictions are kept. 61 | /// 62 | /// Another Relationship 63 | public void MergeWith(Relationship other) 64 | { 65 | if (!other.Property.Resource.Equals(this.Property.Resource)) 66 | { 67 | throw new Exception("Properties are not identical"); 68 | } 69 | 70 | // If target classes are not the same, then we need to keep the most specific one 71 | if (!other.Target.Resource.Equals(this.Target.Resource)) 72 | { 73 | // If both target classes are both datatypes, 74 | if (this.Target.IsDatatype() && other.Target.IsDatatype()) 75 | { 76 | // If the two targets are of same type but not the same, fall back to rdf:Literal 77 | if ( 78 | (this.Target.IsEnumerationDatatype() && other.Target.IsEnumerationDatatype()) || 79 | (this.Target.IsSimpleXsdWrapper() && other.Target.IsSimpleXsdWrapper()) || 80 | (this.Target.IsXsdDatatype() && other.Target.IsXsdDatatype())) 81 | { 82 | IGraph targetsGraph = this.Target.Graph; 83 | IUriNode rdfLiteral = targetsGraph.CreateUriNode(VocabularyHelper.RDFS.Literal); 84 | this.Target = new OntologyClass(rdfLiteral, targetsGraph); 85 | this.MinimumCount = null; 86 | this.MaximumCount = null; 87 | return; 88 | } 89 | else 90 | { 91 | // Preference order is enumeration, custom xsd wrapper type, built-in xsd type 92 | List relationshipCandidates = new List() { this, other }; 93 | relationshipCandidates.OrderBy(candidate => !candidate.Target.IsEnumerationDatatype()) 94 | .ThenBy(candidate => !candidate.Target.IsSimpleXsdWrapper()) 95 | .ThenBy(candidate => !candidate.Target.IsXsdDatatype()); 96 | if (relationshipCandidates.First() == this) 97 | { 98 | return; 99 | } 100 | else 101 | { 102 | this.Target = other.Target; 103 | this.MinimumCount = other.MinimumCount; 104 | this.MaximumCount = other.MaximumCount; 105 | return; 106 | } 107 | } 108 | } 109 | 110 | // If this relationship has the more specific target class, keep it as-is, i.e., return 111 | if (this.Target.SuperClassesWithOwlThing().Contains(other.Target)) 112 | { 113 | return; 114 | } 115 | // If the other relationhip has the more specific target class, keep it instead and return 116 | else if (other.Target.SuperClassesWithOwlThing().Contains(this.Target)) 117 | { 118 | this.Target = other.Target; 119 | this.MinimumCount = other.MinimumCount; 120 | this.MaximumCount = other.MaximumCount; 121 | return; 122 | } 123 | // The classes do not subsume one another; fall back to OWL:Thing as target class 124 | else 125 | { 126 | IGraph targetsGraph = this.Target.Graph; 127 | IUriNode owlThing = targetsGraph.CreateUriNode(VocabularyHelper.OWL.Thing); 128 | this.Target = new OntologyClass(owlThing, targetsGraph); 129 | this.MinimumCount = null; 130 | this.MaximumCount = null; 131 | return; 132 | } 133 | } 134 | 135 | // If either restriction has an exact count, then that is the most specific restriction possible and min/max need not be inspected 136 | if (ExactCount.HasValue || other.ExactCount.HasValue) 137 | { 138 | // If both restrictions have exact values they either diverge (e.g., caused by an inconsistent ontology) or they converge (no change is required) 139 | if (ExactCount.HasValue && other.ExactCount.HasValue) 140 | { 141 | if (ExactCount.Value != other.ExactCount.Value) 142 | { 143 | throw new Exception("Conflicting ExactCounts"); 144 | } 145 | // The exact value is identical; simply return 146 | return; 147 | } 148 | // Assign exact count from other only if our own is null, else keep our old exactcount 149 | ExactCount ??= other.ExactCount.Value; 150 | return; 151 | } 152 | 153 | int? newMinimum = new int?(); 154 | if (MinimumCount.HasValue || other.MinimumCount.HasValue) 155 | { 156 | // If both have minimum counts, keep the larger one 157 | if (MinimumCount.HasValue && other.MinimumCount.HasValue) 158 | { 159 | newMinimum = MinimumCount > other.MinimumCount ? MinimumCount : other.MinimumCount; 160 | } 161 | else 162 | { 163 | // Else, keep whichever is non-null 164 | newMinimum = MinimumCount.HasValue ? MinimumCount : other.MinimumCount; 165 | } 166 | } 167 | 168 | int? newMaximum = new int?(); 169 | if (MaximumCount.HasValue || other.MaximumCount.HasValue) 170 | { 171 | // If both have maximum counts, keep the smaller one 172 | if (MaximumCount.HasValue && other.MaximumCount.HasValue) 173 | { 174 | newMaximum = MaximumCount < other.MaximumCount ? MaximumCount : other.MaximumCount; 175 | } 176 | else 177 | { 178 | // Else, keep whichever is non-null 179 | newMaximum = MaximumCount.HasValue ? MaximumCount : other.MaximumCount; 180 | } 181 | } 182 | 183 | // At this point newMinimum is maximized or null and newMaximum is minimized or null 184 | // If they are inconsistent, i.e., newMinimum is larger than new Maximum, the model is inconsistent; 185 | // throw an error; else store 186 | if (newMinimum.HasValue && newMaximum.HasValue && newMinimum > newMaximum) 187 | { 188 | throw new Exception("Resulting min > resulting max"); 189 | } 190 | MinimumCount = newMinimum; 191 | MaximumCount = newMaximum; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /OWL2OAS/VocabularyHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OWL2OAS.VocabularyHelper 4 | { 5 | public static class O2O 6 | { 7 | public static readonly Uri included = new Uri("https://karlhammar.com/owl2oas/o2o.owl#included"); 8 | public static readonly Uri endpoint = new Uri("https://karlhammar.com/owl2oas/o2o.owl#endpoint"); 9 | } 10 | 11 | public static class DC 12 | { 13 | public static readonly Uri title = new Uri("http://purl.org/dc/elements/1.1/title"); 14 | public static readonly Uri description = new Uri("http://purl.org/dc/elements/1.1/description"); 15 | } 16 | 17 | public static class CC 18 | { 19 | public static readonly Uri license = new Uri("http://creativecommons.org/ns#license"); 20 | } 21 | 22 | public static class RDFS 23 | { 24 | public static readonly Uri label = new Uri("http://www.w3.org/2000/01/rdf-schema#label"); 25 | public static readonly Uri subClassOf = new Uri("http://www.w3.org/2000/01/rdf-schema#subClassOf"); 26 | public static readonly Uri Datatype = new Uri("http://www.w3.org/2000/01/rdf-schema#Datatype"); 27 | public static readonly Uri Literal = new Uri("http://www.w3.org/2000/01/rdf-schema#Literal"); 28 | } 29 | 30 | public static class OWL 31 | { 32 | public static readonly Uri Thing = new Uri("http://www.w3.org/2002/07/owl#Thing"); 33 | public static readonly Uri Restriction = new Uri("http://www.w3.org/2002/07/owl#Restriction"); 34 | public static readonly Uri FunctionalProperty = new Uri("http://www.w3.org/2002/07/owl#FunctionalProperty"); 35 | public static readonly Uri versionIRI = new Uri("http://www.w3.org/2002/07/owl#versionIRI"); 36 | public static readonly Uri deprecated = new Uri("http://www.w3.org/2002/07/owl#deprecated"); 37 | public static readonly Uri oneOf = new Uri("http://www.w3.org/2002/07/owl#oneOf"); 38 | public static readonly Uri annotatedSource = new Uri("http://www.w3.org/2002/07/owl#annotatedSource"); 39 | public static readonly Uri annotatedProperty = new Uri("http://www.w3.org/2002/07/owl#annotatedProperty"); 40 | public static readonly Uri annotatedTarget = new Uri("http://www.w3.org/2002/07/owl#annotatedTarget"); 41 | 42 | #region Restrictions 43 | public static readonly Uri onProperty = new Uri("http://www.w3.org/2002/07/owl#onProperty"); 44 | public static readonly Uri onClass = new Uri("http://www.w3.org/2002/07/owl#onClass"); 45 | public static readonly Uri cardinality = new Uri("http://www.w3.org/2002/07/owl#cardinality"); 46 | public static readonly Uri qualifiedCardinality = new Uri("http://www.w3.org/2002/07/owl#qualifiedCardinality"); 47 | public static readonly Uri allValuesFrom = new Uri("http://www.w3.org/2002/07/owl#allValuesFrom"); 48 | public static readonly Uri someValuesFrom = new Uri("http://www.w3.org/2002/07/owl#someValuesFrom"); 49 | public static readonly Uri minCardinality = new Uri("http://www.w3.org/2002/07/owl#minCardinality"); 50 | public static readonly Uri minQualifiedCardinality = new Uri("http://www.w3.org/2002/07/owl#minQualifiedCardinality"); 51 | public static readonly Uri maxCardinality = new Uri("http://www.w3.org/2002/07/owl#maxCardinality"); 52 | public static readonly Uri maxQualifiedCardinality = new Uri("http://www.w3.org/2002/07/owl#maxQualifiedCardinality"); 53 | #endregion 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # The OWL2OAS Converter 3 | 4 | **Author:** [Karl Hammar](https://karlhammar.com) 5 | 6 | This is a converter for translating [OWL ontologies](https://www.w3.org/TR/owl2-overview/) into 7 | [OpenAPI Specification](https://swagger.io/specification/) documents. OWL is a formal language for expressing concepts and their 8 | relations on the web, based on description logic. OpenAPI Specification is a standard for describing REST endpoints and operations. 9 | This tool generates REST endpoints for the classes (i.e., concepts) declared in an ontology, and generates [JSON-LD](https://json-ld.org) 10 | schemas for those classes based on the object and data properties that they are linked to in the ontology (either via 11 | rdfs:domain/rdfs:range, or via property restrictions). 12 | 13 | ## Usage 14 | 15 | The generated OAS document is printed to stdout. 16 | 17 | To translate a local file, use the `-f`option, as follows: 18 | 19 | ``` 20 | ./OWL2OAS -f /path/to/ontology.rdf 21 | ``` 22 | 23 | To translate a remote file, use the `-u` option, as follows: 24 | ``` 25 | ./OWL2OAS -u http://example.com/path/to/ontology.rdf 26 | ``` 27 | 28 | ## Options 29 | 30 | ``` 31 | -c, --ClassInclusionPolicy (Default: DefaultInclude) Whether to include 32 | all classes by default (overridden by 33 | o2o:included annotation). Valid options: 34 | DefaultInclude or DefaultExclude. 35 | 36 | -p, --PropertyInclusionPolicy (Default: DefaultInclude) Whether to include 37 | all properties by default (overridden by 38 | o2o:included annotation). Valid options: 39 | DefaultInclude or DefaultExclude. 40 | 41 | -n, --no-imports Sets program to not follow owl:Imports 42 | declarations. 43 | 44 | -s, --server (Default: http://localhost:8080/) The server 45 | URL (where presumably an API implementation 46 | is running). 47 | 48 | -f, --file-path Required. The path to the on-disk root 49 | ontology file to translate. 50 | 51 | -u, --uri-path Required. The URI of the root ontology file 52 | to translate. 53 | 54 | --help Display this help screen. 55 | 56 | --version Display version information. 57 | ``` 58 | 59 | ## Entity inclusion policy 60 | 61 | By default we include all classes and properties found in the parsed ontologies. If you want to specifically exclude some entity from the generated output, apply the `https://karlhammar.com/owl2oas/o2o.owl#included` annotation property on it, with the boolean value `false`. If you want to reverse this inclusion policy, for classes or properties, use the command line options `--ClassInclusionPolicy` or `--PropertyInclusionPolicy` with the value `DefaultExclude`; then, *only* the entities of that kind which have been tagged with `https://karlhammar.com/owl2oas/o2o.owl#included` and the value `true` will be included. 62 | 63 | ## OWL Entity Equivalence 64 | 65 | We do not support equivalence between classes or between properties. Strange things will probably happen if you run this tool over ontologies that depend on equivalence axioms. Feel free to try it out though! 66 | 67 | ## Examples 68 | 69 | See the [examples directory](https://github.com/hammar/OWL2OAS/tree/master/OWL2OAS/examples). 70 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # OWL2OAS Documentation 2 | 3 | This is a placeholder. 4 | --------------------------------------------------------------------------------