Skip to content

Commit 9fb2ae1

Browse files
committed
Schema parsing improvements and additions
* Add support for oasLiterals * Configure jschon for custom metaschemas * Only add ParsedStructure nodes if using line/column number sourcemaps
1 parent ecc6d3a commit 9fb2ae1

File tree

7 files changed

+320
-64
lines changed

7 files changed

+320
-64
lines changed

oascomply/__init__.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,34 @@
55
code paths. Wherever possible, library initialization should be
66
placed near the library's first use.
77
"""
8-
8+
import pathlib
99
import jschon
10+
import jschon.catalog
1011

1112
__all__ = ['schema_catalog']
1213

1314
schema_catalog = jschon.create_catalog('2020-12')
1415
"""The default shared ``jschon`` schema loader and cache"""
16+
17+
schema_catalog.add_uri_source(
18+
jschon.URI(
19+
'https://spec.openapis.org/compliance/schemas/dialect/2023-06/'
20+
),
21+
jschon.catalog.LocalSource(
22+
(
23+
pathlib.Path(__file__) / '..' / '..' / 'schemas' / 'dialect'
24+
).resolve(),
25+
suffix='.json',
26+
),
27+
)
28+
schema_catalog.add_uri_source(
29+
jschon.URI(
30+
'https://spec.openapis.org/compliance/schemas/meta/2023-06/'
31+
),
32+
jschon.catalog.LocalSource(
33+
(
34+
pathlib.Path(__file__) / '..' / '..' / 'schemas' / 'meta'
35+
).resolve(),
36+
suffix='.json',
37+
),
38+
)

oascomply/apidescription.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,12 @@ def get(self, uri: str) -> Optional[Any]:
144144
return None, None
145145

146146
def validate(self, resource_uri=None, oastype='OpenAPI'):
147-
sp = SchemaParser.get_parser(
148-
{},
149-
annotations=('oasType', 'oasChildren', 'oasReferences')
150-
)
147+
sp = SchemaParser.get_parser({}, annotations=(
148+
'oasType',
149+
'oasChildren',
150+
'oasReferences',
151+
'oasLiterals',
152+
))
151153
if resource_uri is None:
152154
assert oastype == 'OpenAPI'
153155
resource_uri = self._primary_uri

oascomply/oasgraph.py

+139-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import json
2+
import re
3+
from functools import cached_property
24
from pathlib import Path
35
from uuid import uuid4
46
from typing import Any, Optional
@@ -10,6 +12,11 @@
1012
from rdflib.namespace import RDF
1113
import yaml
1214

15+
from oascomply.pointers import (
16+
RelativeJSONPointerTemplate,
17+
RelativeJSONPointerTemplateError,
18+
)
19+
1320
__all__ = [
1421
'OasGraph',
1522
]
@@ -49,17 +56,41 @@
4956

5057

5158
class OasGraph:
52-
def __init__(self, version):
59+
"""
60+
Graph representing an OAS API description
61+
62+
:param version: The X.Y OAS version string for the description
63+
"""
64+
def __init__(self, version: str):
5365
if version not in ('3.0', '3.1'):
5466
raise ValueError(f'OAS v{version} is not supported.')
5567
if version == '3.1':
5668
raise ValueError(f'OAS v3.1 support TBD.')
69+
self._version = version
5770

5871
self._g = rdflib.Graph()
59-
self._oas = rdflib.Namespace(
60-
f'https://spec.openapis.org/oas/v{version}/ontology#'
72+
self._oas_unversioned = rdflib.Namespace(
73+
'https://spec.openapis.org/compliance/ontology#'
6174
)
62-
self._g.bind('oas3.0', self._oas)
75+
self._oas_versions = {
76+
'3.0': rdflib.Namespace(
77+
'https://spec.openapis.org/compliance/ontology#3.0-'
78+
),
79+
'3.1': rdflib.Namespace(
80+
'https://spec.openapis.org/compliance/ontology#3.1-'
81+
),
82+
}
83+
self._g.bind('oas', self._oas_unversioned)
84+
self._g.bind('oas3.0', self._oas_versions['3.0'])
85+
self._g.bind('oas3.1', self._oas_versions['3.1'])
86+
87+
@cached_property
88+
def oas(self):
89+
return self._oas_unversioned
90+
91+
@cached_property
92+
def oas_v(self):
93+
return self._oas_versions[self._version]
6394

6495
def serialize(self, *args, base=None, output_format=None, **kwargs):
6596
"""Serialize the graph using the given output format."""
@@ -77,12 +108,17 @@ def add_resource(self, location, iri):
77108
rdf_node = rdflib.URIRef(iri)
78109
self._g.add((
79110
rdf_node,
80-
self._oas['locatedAt'],
111+
self.oas['locatedAt'],
81112
rdflib.URIRef(
82113
location.resolve().as_uri() if isinstance(location, Path)
83114
else location,
84115
),
85116
))
117+
self._g.add((
118+
rdf_node,
119+
self.oas['root'],
120+
rdf_node + '#',
121+
))
86122
filename = None
87123
if isinstance(location, Path):
88124
filename = location.name
@@ -94,7 +130,7 @@ def add_resource(self, location, iri):
94130
if filename:
95131
self._g.add((
96132
rdf_node,
97-
self._oas['filename'],
133+
self.oas['filename'],
98134
rdflib.Literal(filename),
99135
))
100136

@@ -104,14 +140,14 @@ def add_oastype(self, annotation, instance, sourcemap):
104140
self._g.add((
105141
instance_uri,
106142
RDF.type,
107-
self._oas[annotation.value],
108-
))
109-
self._g.add((
110-
instance_uri,
111-
RDF.type,
112-
self._oas['ParsedStructure'],
143+
self.oas_v[annotation.value],
113144
))
114145
if sourcemap:
146+
self._g.add((
147+
instance_uri,
148+
RDF.type,
149+
self.oas['ParsedStructure'],
150+
))
115151
self.add_sourcemap(
116152
instance_uri,
117153
annotation.location.instance_ptr,
@@ -126,28 +162,55 @@ def add_sourcemap(self, instance_rdf_uri, instance_ptr, sourcemap):
126162
entry = sourcemap[map_key]
127163
self._g.add((
128164
instance_rdf_uri,
129-
self._oas['line'],
165+
self.oas['line'],
130166
rdflib.Literal(entry.value_start.line),
131167
))
132168
self._g.add((
133169
instance_rdf_uri,
134-
self._oas['column'],
170+
self.oas['column'],
135171
rdflib.Literal(entry.value_start.column),
136172
))
137173

174+
def _resolve_child_template(
175+
self,
176+
annotation,
177+
instance,
178+
value_processor=None,
179+
):
180+
parent_uri = rdflib.URIRef(str(annotation.location.instance_uri))
181+
parent_obj = annotation.location.instance_ptr.evaluate(instance)
182+
for child_template, ann_value in annotation.value.items():
183+
# Yield back relname unchanged?
184+
# Take modifier funciton?
185+
# double generator of some sort?
186+
rdf_name = ann_value.value # unwrap jschon.JSON
187+
relptr = None
188+
if re.match(r'\d', rdf_name):
189+
relptr = jschon.RelativeJSONPointer(rdf_name)
190+
rdf_name = None
191+
192+
yield from (
193+
(
194+
result,
195+
rdf_name if rdf_name
196+
else relptr.evaluate(result.data),
197+
)
198+
for result in RelativeJSONPointerTemplate(
199+
child_template,
200+
).evaluate(parent_obj)
201+
)
202+
138203
def add_oaschildren(self, annotation, instance, sourcemap):
139204
location = annotation.location
140205
# to_rdf()
141206
parent_uri = rdflib.URIRef(str(location.instance_uri))
142-
for child in annotation.value:
143-
child = child.value
144-
if '{' in child:
145-
continue
146-
147-
child_ptr = jschon.RelativeJSONPointer(child)
148-
parent_obj = location.instance_ptr.evaluate(instance)
149-
try:
150-
child_obj = child_ptr.evaluate(parent_obj)
207+
parent_obj = location.instance_ptr.evaluate(instance)
208+
try:
209+
for result, relname in self._resolve_child_template(
210+
annotation,
211+
instance,
212+
):
213+
child_obj = result.data
151214
child_path = child_obj.path
152215
iu = location.instance_uri
153216
# replace fragment; to_rdf
@@ -156,12 +219,12 @@ def add_oaschildren(self, annotation, instance, sourcemap):
156219
)))
157220
self._g.add((
158221
parent_uri,
159-
self._oas[child_ptr.path[0]],
222+
self.oas[relname],
160223
child_uri,
161224
))
162225
self._g.add((
163226
child_uri,
164-
self._oas['parent'],
227+
self.oas['parent'],
165228
parent_uri,
166229
))
167230
if sourcemap:
@@ -170,20 +233,52 @@ def add_oaschildren(self, annotation, instance, sourcemap):
170233
child_path,
171234
sourcemap,
172235
)
173-
except jschon.RelativeJSONPointerError as e:
174-
pass
236+
except (
237+
jschon.RelativeJSONPointerError,
238+
RelativeJSONPointerTemplateError,
239+
) as e:
240+
# TODO: actual error handling
241+
raise
242+
243+
def add_oasliterals(self, annotation, instance, sourcemap):
244+
location = annotation.location
245+
# to_rdf()
246+
parent_uri = rdflib.URIRef(str(location.instance_uri))
247+
parent_obj = location.instance_ptr.evaluate(instance)
248+
try:
249+
for result, relname in self._resolve_child_template(
250+
annotation,
251+
instance,
252+
):
253+
literal = result.data
254+
literal_path = literal.path
255+
iu = location.instance_uri
256+
# replace fragment; to_rdf
257+
literal_node = rdflib.Literal(literal.value)
258+
self._g.add((
259+
parent_uri,
260+
self.oas[relname],
261+
literal_node,
262+
))
263+
# TODO: Sourcemap for literals? might need
264+
# intermediate node as literals cannot
265+
# be subjects in triples.
266+
except (
267+
jschon.RelativeJSONPointerError,
268+
RelativeJSONPointerTemplateError,
269+
) as e:
270+
# TODO: actual error handling
271+
raise
175272

176273
def add_oasreferences(self, annotation, instance, sourcemap):
177274
location = annotation.location
178275
remote_resources = []
179-
for refloc, reftype in annotation.value.items():
180-
reftype = reftype.value
181-
# if '{' in refloc:
182-
# continue
183-
try:
184-
ref_ptr = jschon.RelativeJSONPointer(refloc)
185-
parent_obj = location.instance_ptr.evaluate(instance)
186-
ref_obj = ref_ptr.evaluate(parent_obj)
276+
try:
277+
for template_result, reftype in self._resolve_child_template(
278+
annotation,
279+
instance,
280+
):
281+
ref_obj = template_result.data
187282
ref_source_path = ref_obj.path
188283
iu = location.instance_uri
189284
# replace fragment; to_rdf
@@ -195,7 +290,7 @@ def add_oasreferences(self, annotation, instance, sourcemap):
195290
))
196291
self._g.add((
197292
ref_src_uri,
198-
self._oas['references'],
293+
self.oas['references'],
199294
ref_target_uri,
200295
))
201296
# TODO: elide the reference with a new edge
@@ -213,6 +308,12 @@ def add_oasreferences(self, annotation, instance, sourcemap):
213308
ref_source_path,
214309
sourcemap,
215310
)
216-
except (ValueError, jschon.RelativeJSONPointerError) as e:
217-
pass
311+
except (
312+
ValueError,
313+
jschon.RelativeJSONPointerError,
314+
RelativeJSONPointerTemplateError,
315+
) as e:
316+
# TODO: Actual error handling
317+
pass
318+
218319
return remote_resources

0 commit comments

Comments
 (0)