Skip to content

Commit

Permalink
Better handle traversing into subresources while resolving pointers.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Julian committed Feb 19, 2023
1 parent b66b330 commit 9835d62
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 7 deletions.
29 changes: 23 additions & 6 deletions referencing/_core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections.abc import Iterable, Iterator, Sequence
from typing import Any, Callable, ClassVar, Generic
from typing import Any, Callable, ClassVar, Generic, Protocol
from urllib.parse import unquote, urldefrag, urljoin

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


class _MaybeInSubresource(Protocol[D]):
def __call__(
self,
segments: Sequence[int | str],
resolver: Resolver[D],
subresource: Resource[D],
) -> Resolver[D]:
...


@frozen
class Specification(Generic[D]):
"""
Expand All @@ -32,6 +42,10 @@ class Specification(Generic[D]):
#: the subresources themselves).
subresources_of: Callable[[D], Iterable[D]]

#: While resolving a JSON pointer, conditionally enter a subresource
#: (if e.g. we have just entered a keyword whose value is a subresource)
maybe_in_subresource: _MaybeInSubresource[D]

#: Retrieve the anchors contained in the given document.
_anchors_in: Callable[
[Specification[D], D],
Expand Down Expand Up @@ -63,6 +77,7 @@ def create_resource(self, contents: D) -> Resource[D]:
id_of=lambda contents: None,
subresources_of=lambda contents: [],
anchors_in=lambda specification, contents: [],
maybe_in_subresource=lambda segments, resolver, subresource: resolver,
)


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

# FIXME: this is slightly wrong, we need to know that we are
# entering a subresource specifically, not just any mapping
if isinstance(contents, Mapping):
subresource = self._specification.create_resource(contents) # type: ignore[reportUnknownArgumentType] # noqa: E501
resolver = resolver.in_subresource(subresource)
segments.append(segment)
resolver = self._specification.maybe_in_subresource(
segments=segments,
resolver=resolver,
subresource=self._specification.create_resource(contents), # type: ignore[reportUnknownArgumentType] # noqa: E501
)
return Resolved(contents=contents, resolver=resolver) # type: ignore[reportUnknownArgumentType] # noqa: E501


Expand Down
102 changes: 102 additions & 0 deletions referencing/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,29 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
return subresources_of


def _maybe_in_subresource(
in_value: Set[str] = frozenset(),
in_subvalues: Set[str] = frozenset(),
in_subarray: Set[str] = frozenset(),
):
def maybe_in_subresource(
segments: Sequence[int | str],
resolver: _Resolver[Any],
subresource: Resource[Any],
) -> _Resolver[Any]:
_segments = iter(segments)
for segment in _segments:
if segment in in_value:
continue
elif segment in in_subarray or segment in in_subvalues:
if next(_segments, None) is not None:
continue
return resolver
return resolver.in_subresource(subresource)

return maybe_in_subresource


DRAFT202012 = Specification(
name="draft2020-12",
id_of=_dollar_id,
Expand All @@ -250,6 +273,28 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
},
),
anchors_in=_anchor,
maybe_in_subresource=_maybe_in_subresource(
in_value={
"additionalProperties",
"contains",
"contentSchema",
"else",
"if",
"items",
"not",
"propertyNames",
"then",
"unevaluatedItems",
"unevaluatedProperties",
},
in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"},
in_subvalues={
"$defs",
"dependentSchemas",
"patternProperties",
"properties",
},
),
)
DRAFT201909 = Specification(
name="draft2019-09",
Expand Down Expand Up @@ -277,6 +322,28 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
},
),
anchors_in=_anchor_2019,
maybe_in_subresource=_maybe_in_subresource(
in_value={
"additionalItems",
"additionalProperties",
"contains",
"contentSchema",
"else",
"if",
"not",
"propertyNames",
"then",
"unevaluatedItems",
"unevaluatedProperties",
},
in_subarray={"allOf", "anyOf", "oneOf"},
in_subvalues={
"$defs",
"dependentSchemas",
"patternProperties",
"properties",
},
),
)
DRAFT7 = Specification(
name="draft-07",
Expand All @@ -296,6 +363,20 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
in_subvalues={"definitions", "patternProperties", "properties"},
),
anchors_in=_legacy_anchor_in_dollar_id,
maybe_in_subresource=_maybe_in_subresource(
in_value={
"additionalItems",
"additionalProperties",
"contains",
"else",
"if",
"not",
"propertyNames",
"then",
},
in_subarray={"allOf", "anyOf", "oneOf"},
in_subvalues={"definitions", "patternProperties", "properties"},
),
)
DRAFT6 = Specification(
name="draft-06",
Expand All @@ -312,6 +393,17 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
in_subvalues={"definitions", "patternProperties", "properties"},
),
anchors_in=_legacy_anchor_in_dollar_id,
maybe_in_subresource=_maybe_in_subresource(
in_value={
"additionalItems",
"additionalProperties",
"contains",
"not",
"propertyNames",
},
in_subarray={"allOf", "anyOf", "oneOf"},
in_subvalues={"definitions", "patternProperties", "properties"},
),
)
DRAFT4 = Specification(
name="draft-04",
Expand All @@ -322,6 +414,11 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
in_subvalues={"definitions", "patternProperties", "properties"},
),
anchors_in=_legacy_anchor_in_id,
maybe_in_subresource=_maybe_in_subresource(
in_value={"additionalItems", "additionalProperties", "not"},
in_subarray={"allOf", "anyOf", "oneOf"},
in_subvalues={"definitions", "patternProperties", "properties"},
),
)
DRAFT3 = Specification(
name="draft-03",
Expand All @@ -332,6 +429,11 @@ def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
in_subvalues={"definitions", "patternProperties", "properties"},
),
anchors_in=_legacy_anchor_in_id,
maybe_in_subresource=_maybe_in_subresource(
in_value={"additionalItems", "additionalProperties"},
in_subarray={"extends"},
in_subvalues={"definitions", "patternProperties", "properties"},
),
)


Expand Down
12 changes: 12 additions & 0 deletions referencing/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
)
for name, each in contents.get("anchors", {}).items()
],
maybe_in_subresource=lambda segments, resolver, subresource: (
resolver.in_subresource(subresource)
if not len(segments) % 2
and all(each == "children" for each in segments[::2])
else resolver
),
)


Expand Down Expand Up @@ -534,6 +540,9 @@ def test_id_delegates_to_specification(self):
id_of=lambda contents: "urn:fixedID",
subresources_of=lambda contents: [],
anchors_in=lambda specification, contents: [],
maybe_in_subresource=(
lambda segments, resolver, subresource: resolver
),
)
resource = Resource(
contents={"foo": "baz"},
Expand Down Expand Up @@ -816,6 +825,9 @@ def test_create_resource(self):
id_of=lambda contents: "urn:fixedID",
subresources_of=lambda contents: [],
anchors_in=lambda specification, contents: [],
maybe_in_subresource=(
lambda segments, resolver, subresource: resolver
),
)
resource = specification.create_resource(contents={"foo": "baz"})
assert resource == Resource(
Expand Down

0 comments on commit 9835d62

Please sign in to comment.