Skip to content

Commit 9835d62

Browse files
committed
Better handle traversing into subresources while resolving pointers.
This is .. ugly and duplicative. But more correct. When traversing a JSON Pointer, we need to know when and if we're entering a subresource, and this differs by spec (and has to do with whether we've just entered a known keyword). This will likely get less duplicative when #24 is done.
1 parent b66b330 commit 9835d62

File tree

4 files changed

+138
-7
lines changed

4 files changed

+138
-7
lines changed

referencing/_core.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Iterable, Iterator, Sequence
4-
from typing import Any, Callable, ClassVar, Generic
4+
from typing import Any, Callable, ClassVar, Generic, Protocol
55
from urllib.parse import unquote, urldefrag, urljoin
66

77
from attrs import evolve, field
@@ -13,6 +13,16 @@
1313
from referencing.typing import URI, Anchor as AnchorType, D, Mapping
1414

1515

16+
class _MaybeInSubresource(Protocol[D]):
17+
def __call__(
18+
self,
19+
segments: Sequence[int | str],
20+
resolver: Resolver[D],
21+
subresource: Resource[D],
22+
) -> Resolver[D]:
23+
...
24+
25+
1626
@frozen
1727
class Specification(Generic[D]):
1828
"""
@@ -32,6 +42,10 @@ class Specification(Generic[D]):
3242
#: the subresources themselves).
3343
subresources_of: Callable[[D], Iterable[D]]
3444

45+
#: While resolving a JSON pointer, conditionally enter a subresource
46+
#: (if e.g. we have just entered a keyword whose value is a subresource)
47+
maybe_in_subresource: _MaybeInSubresource[D]
48+
3549
#: Retrieve the anchors contained in the given document.
3650
_anchors_in: Callable[
3751
[Specification[D], D],
@@ -63,6 +77,7 @@ def create_resource(self, contents: D) -> Resource[D]:
6377
id_of=lambda contents: None,
6478
subresources_of=lambda contents: [],
6579
anchors_in=lambda specification, contents: [],
80+
maybe_in_subresource=lambda segments, resolver, subresource: resolver,
6681
)
6782

6883

@@ -156,6 +171,7 @@ def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]:
156171
if the pointer points to a location not present in the document
157172
"""
158173
contents = self.contents
174+
segments: list[int | str] = []
159175
for segment in unquote(pointer[1:]).split("/"):
160176
if isinstance(contents, Sequence):
161177
segment = int(segment)
@@ -166,11 +182,12 @@ def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]:
166182
except LookupError:
167183
raise exceptions.PointerToNowhere(ref=pointer, resource=self)
168184

169-
# FIXME: this is slightly wrong, we need to know that we are
170-
# entering a subresource specifically, not just any mapping
171-
if isinstance(contents, Mapping):
172-
subresource = self._specification.create_resource(contents) # type: ignore[reportUnknownArgumentType] # noqa: E501
173-
resolver = resolver.in_subresource(subresource)
185+
segments.append(segment)
186+
resolver = self._specification.maybe_in_subresource(
187+
segments=segments,
188+
resolver=resolver,
189+
subresource=self._specification.create_resource(contents), # type: ignore[reportUnknownArgumentType] # noqa: E501
190+
)
174191
return Resolved(contents=contents, resolver=resolver) # type: ignore[reportUnknownArgumentType] # noqa: E501
175192

176193

referencing/jsonschema.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,29 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
224224
return subresources_of
225225

226226

227+
def _maybe_in_subresource(
228+
in_value: Set[str] = frozenset(),
229+
in_subvalues: Set[str] = frozenset(),
230+
in_subarray: Set[str] = frozenset(),
231+
):
232+
def maybe_in_subresource(
233+
segments: Sequence[int | str],
234+
resolver: _Resolver[Any],
235+
subresource: Resource[Any],
236+
) -> _Resolver[Any]:
237+
_segments = iter(segments)
238+
for segment in _segments:
239+
if segment in in_value:
240+
continue
241+
elif segment in in_subarray or segment in in_subvalues:
242+
if next(_segments, None) is not None:
243+
continue
244+
return resolver
245+
return resolver.in_subresource(subresource)
246+
247+
return maybe_in_subresource
248+
249+
227250
DRAFT202012 = Specification(
228251
name="draft2020-12",
229252
id_of=_dollar_id,
@@ -250,6 +273,28 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
250273
},
251274
),
252275
anchors_in=_anchor,
276+
maybe_in_subresource=_maybe_in_subresource(
277+
in_value={
278+
"additionalProperties",
279+
"contains",
280+
"contentSchema",
281+
"else",
282+
"if",
283+
"items",
284+
"not",
285+
"propertyNames",
286+
"then",
287+
"unevaluatedItems",
288+
"unevaluatedProperties",
289+
},
290+
in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"},
291+
in_subvalues={
292+
"$defs",
293+
"dependentSchemas",
294+
"patternProperties",
295+
"properties",
296+
},
297+
),
253298
)
254299
DRAFT201909 = Specification(
255300
name="draft2019-09",
@@ -277,6 +322,28 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
277322
},
278323
),
279324
anchors_in=_anchor_2019,
325+
maybe_in_subresource=_maybe_in_subresource(
326+
in_value={
327+
"additionalItems",
328+
"additionalProperties",
329+
"contains",
330+
"contentSchema",
331+
"else",
332+
"if",
333+
"not",
334+
"propertyNames",
335+
"then",
336+
"unevaluatedItems",
337+
"unevaluatedProperties",
338+
},
339+
in_subarray={"allOf", "anyOf", "oneOf"},
340+
in_subvalues={
341+
"$defs",
342+
"dependentSchemas",
343+
"patternProperties",
344+
"properties",
345+
},
346+
),
280347
)
281348
DRAFT7 = Specification(
282349
name="draft-07",
@@ -296,6 +363,20 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
296363
in_subvalues={"definitions", "patternProperties", "properties"},
297364
),
298365
anchors_in=_legacy_anchor_in_dollar_id,
366+
maybe_in_subresource=_maybe_in_subresource(
367+
in_value={
368+
"additionalItems",
369+
"additionalProperties",
370+
"contains",
371+
"else",
372+
"if",
373+
"not",
374+
"propertyNames",
375+
"then",
376+
},
377+
in_subarray={"allOf", "anyOf", "oneOf"},
378+
in_subvalues={"definitions", "patternProperties", "properties"},
379+
),
299380
)
300381
DRAFT6 = Specification(
301382
name="draft-06",
@@ -312,6 +393,17 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
312393
in_subvalues={"definitions", "patternProperties", "properties"},
313394
),
314395
anchors_in=_legacy_anchor_in_dollar_id,
396+
maybe_in_subresource=_maybe_in_subresource(
397+
in_value={
398+
"additionalItems",
399+
"additionalProperties",
400+
"contains",
401+
"not",
402+
"propertyNames",
403+
},
404+
in_subarray={"allOf", "anyOf", "oneOf"},
405+
in_subvalues={"definitions", "patternProperties", "properties"},
406+
),
315407
)
316408
DRAFT4 = Specification(
317409
name="draft-04",
@@ -322,6 +414,11 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
322414
in_subvalues={"definitions", "patternProperties", "properties"},
323415
),
324416
anchors_in=_legacy_anchor_in_id,
417+
maybe_in_subresource=_maybe_in_subresource(
418+
in_value={"additionalItems", "additionalProperties", "not"},
419+
in_subarray={"allOf", "anyOf", "oneOf"},
420+
in_subvalues={"definitions", "patternProperties", "properties"},
421+
),
325422
)
326423
DRAFT3 = Specification(
327424
name="draft-03",
@@ -332,6 +429,11 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
332429
in_subvalues={"definitions", "patternProperties", "properties"},
333430
),
334431
anchors_in=_legacy_anchor_in_id,
432+
maybe_in_subresource=_maybe_in_subresource(
433+
in_value={"additionalItems", "additionalProperties"},
434+
in_subarray={"extends"},
435+
in_subvalues={"definitions", "patternProperties", "properties"},
436+
),
335437
)
336438

337439

referencing/tests/test_core.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
)
1616
for name, each in contents.get("anchors", {}).items()
1717
],
18+
maybe_in_subresource=lambda segments, resolver, subresource: (
19+
resolver.in_subresource(subresource)
20+
if not len(segments) % 2
21+
and all(each == "children" for each in segments[::2])
22+
else resolver
23+
),
1824
)
1925

2026

@@ -534,6 +540,9 @@ def test_id_delegates_to_specification(self):
534540
id_of=lambda contents: "urn:fixedID",
535541
subresources_of=lambda contents: [],
536542
anchors_in=lambda specification, contents: [],
543+
maybe_in_subresource=(
544+
lambda segments, resolver, subresource: resolver
545+
),
537546
)
538547
resource = Resource(
539548
contents={"foo": "baz"},
@@ -816,6 +825,9 @@ def test_create_resource(self):
816825
id_of=lambda contents: "urn:fixedID",
817826
subresources_of=lambda contents: [],
818827
anchors_in=lambda specification, contents: [],
828+
maybe_in_subresource=(
829+
lambda segments, resolver, subresource: resolver
830+
),
819831
)
820832
resource = specification.create_resource(contents={"foo": "baz"})
821833
assert resource == Resource(

0 commit comments

Comments
 (0)