diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0180c..1bb20a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/json_ref_dict/ref_pointer.py b/json_ref_dict/ref_pointer.py index 6e6bba8..4dcd70b 100644 --- a/json_ref_dict/ref_pointer.py +++ b/json_ref_dict/ref_pointer.py @@ -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 :]] ) @@ -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: diff --git a/tests/schemas/conflicting-keys.yaml b/tests/schemas/conflicting-keys.yaml new file mode 100644 index 0000000..f9d672c --- /dev/null +++ b/tests/schemas/conflicting-keys.yaml @@ -0,0 +1,5 @@ +conflicting: + $ref: "#/real" + value: "bad" +real: + value: "good" diff --git a/tests/schemas/root-ref.json b/tests/schemas/root-ref.json new file mode 100644 index 0000000..df92cf2 --- /dev/null +++ b/tests/schemas/root-ref.json @@ -0,0 +1 @@ +{"$ref": "#/definitions/bar", "definitions": {"bar": {"type": "integer"}}} diff --git a/tests/test_loader.py b/tests/test_loader.py index e959a64..693f372 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -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"