Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Types of changes are:

## [Unreleased]

* Added support for references which point to within the object containing the
`$ref` key.

## [0.7.1] - 2021-09-10

### Fixed
Expand Down
44 changes: 41 additions & 3 deletions json_ref_dict/ref_pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,47 @@ def resolve_remote_with_uri(
:return: tuple indicating (1) if doc was a ref (the ref URI returned)
and (2) what that ref value was.
"""
if not (
isinstance(doc, abc.Mapping) and isinstance(doc.get("$ref"), str)
):

is_ref = isinstance(doc, abc.Mapping) and isinstance(
doc.get("$ref"), str
)

if not is_ref:
return None, None

ref = doc["$ref"]

is_local = ref.startswith("#")

if is_local:
# If this reference is local to this document, check the following
# to determine if this reference is actually pointing to within
# an object containing a `$ref` which is currently being resolved:
#
# 1) If we are already resolving this reference (i.e, `self` is the
# same as the reference we just found)
# 2) This reference doesn't just point to itself (i.e, there are
# more parts to resolve)
#
# If both these things are true, ignore this `$ref`, and allow the
# existing resolve process (represented by `self`) to continue.

# If a URI ended in `/`, there will be an empty item on the end of
# `parts`. For our purposes, this is the same as if it were not
# present. Modify the ephemeral `ref_pointer` `parts` to resolve
# this discrepancy
ref_pointer = RefPointer(ref)
if self.parts[-1] == "" and ref_pointer.parts[-1] != "":
ref_pointer.parts.append("")

is_same = self.parts == ref_pointer.parts
has_more = parts_idx + 1 < len(self.parts)

if is_same and has_more:
# `$ref` points to within the JSON object represented by `self`.
# Ignore.
return None, None

remote_uri = self.uri.relative(doc["$ref"]).get(
*[parse_segment(part) for part in self.parts[parts_idx + 1 :]]
)
Expand Down Expand Up @@ -113,6 +150,7 @@ def resolve_with_uri(
if default is _nothing:
raise
return self.uri, default

return self.uri, doc

def set(self, doc: Any, value: Any, inplace: bool = True) -> NoReturn:
Expand Down
5 changes: 5 additions & 0 deletions tests/schemas/conflicting-keys.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
conflicting:
$ref: "#/real"
value: "bad"
real:
value: "good"
1 change: 1 addition & 0 deletions tests/schemas/root-ref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$ref": "#/definitions/bar", "definitions": {"bar": {"type": "integer"}}}
10 changes: 10 additions & 0 deletions tests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,13 @@ def test_immediate_references_is_detected():
def test_immediate_references_can_be_bypassed():
value = RefDict.from_uri("tests/schemas/immediate-ref.json#/type")
assert value == "integer"


def test_local_root_ref():
value = RefDict.from_uri("tests/schemas/root-ref.json#/")
assert value == {"type": "integer"}


def test_local_conflicting_keys():
value = RefDict.from_uri("tests/schemas/conflicting-keys.yaml#/conflicting/value")
assert value == "good"