Skip to content

Commit 39a5364

Browse files
authored
Fix handling of local $refs (#3)
- To streamline sorting code, creating sort keys for all of the schema's nodes, not just object values - Set JSON node's sort key only if not already set: for referenced schemas, this makes sure we assign the sort key according to the location in referring schema (the property containing the $ref), not the referred schema. - Rewrite traversal from "iterate over children and do work then recurse" to "do work then iterate over children and recurse"
1 parent 1ce0ab3 commit 39a5364

File tree

3 files changed

+93
-11
lines changed

3 files changed

+93
-11
lines changed

jschon_sort/_main.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,68 @@
22
import math
33
from typing import Dict
44
from typing import List
5+
from typing import Mapping
56
from typing import Tuple
67

78
import jschon.jsonschema
89
from jschon.json import AnyJSONCompatible
910

1011

11-
def _get_sort_keys_for_json_nodes(node: jschon.JSON) -> Dict[jschon.JSONPointer, Tuple[int, ...]]:
12+
def _get_sort_keys_for_json_nodes(root_node: jschon.JSON) -> Mapping[jschon.JSONPointer, Tuple[int, ...]]:
1213
"""
1314
Gets a mapping from JSON nodes (as JSON pointers) to sort keys (as tuples of integers) that match their position
1415
within the JSON.
1516
"""
1617
mapping = {}
18+
root_depth = len(root_node.path)
1719

1820
def _recurse(node: jschon.JSON, node_sort_key: Tuple[int, ...]) -> None:
21+
relative_path = node.path[root_depth:]
22+
mapping[relative_path] = node_sort_key
23+
1924
if node.type == "object":
2025
for idx, v in enumerate(node.data.values()):
2126
new_loc = (*node_sort_key, idx)
22-
mapping[v.path] = new_loc
2327
_recurse(v, new_loc)
2428
elif node.type == "array":
2529
for idx, v in enumerate(node.data):
2630
new_loc = (*node_sort_key, idx)
2731
_recurse(v, new_loc)
2832

29-
_recurse(node, ())
33+
_recurse(root_node, ())
3034

3135
return mapping
3236

3337

3438
def sort_doc_by_schema(*, doc_data: AnyJSONCompatible, schema_data: AnyJSONCompatible) -> AnyJSONCompatible:
35-
schema_json = jschon.JSON(schema_data)
36-
schema_sort_keys = _get_sort_keys_for_json_nodes(schema_json)
37-
3839
try:
39-
schema = jschon.JSONSchema(schema_data)
40+
root_schema = jschon.JSONSchema(schema_data)
4041
except jschon.CatalogError:
4142
# jschon only supports newer jsonschema drafts
4243
schema_data = copy.copy(schema_data)
4344
schema_data['$schema'] = "https://json-schema.org/draft/2020-12/schema"
44-
schema = jschon.JSONSchema(schema_data)
45+
root_schema = jschon.JSONSchema(schema_data)
4546

4647
doc_json = jschon.JSON(doc_data)
47-
res = schema.evaluate(doc_json)
48+
res = root_schema.evaluate(doc_json)
4849
if not res.valid:
4950
raise ValueError('Document failed schema validation')
5051

52+
schema_sort_keys_cache: Dict[jschon.URI, Mapping[jschon.JSONPointer, Tuple[int, ...]]] = {}
53+
54+
def _get_sort_keys_for_schema(schema: jschon.JSONSchema) -> Mapping[jschon.JSONPointer, Tuple[int, ...]]:
55+
if sort_keys := schema_sort_keys_cache.get(schema.canonical_uri):
56+
return sort_keys
57+
sort_keys = _get_sort_keys_for_json_nodes(schema)
58+
schema_sort_keys_cache[schema.canonical_uri] = sort_keys
59+
return sort_keys
60+
5161
doc_sort_keys: Dict[jschon.JSONPointer, Tuple[int, ...]] = {}
5262

5363
def _traverse_scope(scope: jschon.jsonschema.Scope) -> None:
64+
schema_sort_keys = _get_sort_keys_for_schema(scope.schema)
65+
doc_sort_keys.setdefault(scope.instpath, schema_sort_keys[scope.relpath])
5466
for child in scope.iter_children():
55-
doc_sort_keys[child.instpath] = schema_sort_keys[child.path]
5667
_traverse_scope(child)
5768

5869
_traverse_scope(res)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = jschon-sort
3-
version = 0.0.2
3+
version = 0.0.3
44
description = Sorts a JSON or YAML document to match a JSON Schema's order of properties
55
long_description = file: README.md
66
long_description_content_type = text/markdown

tests/test_jschon_sort.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,74 @@ def test_sort_doc_by_schema(schema_version: str) -> None:
6262
assert actual is not doc
6363
assert json.dumps(doc) == doc_str, "ensure doc is not modified in place"
6464
assert json.dumps(actual) == '{"ranges": [{"BBB": 42, "AAA": 42, "start": 10, "end": 20}]}'
65+
66+
67+
def test_sort_doc_by_schema__local_ref() -> None:
68+
# Arrange
69+
doc_str = '{"foo": {"end": 20, "start": 10}}'
70+
doc = json.loads(doc_str)
71+
72+
schema = {
73+
"$schema": "https://json-schema.org/draft/2020-12/schema",
74+
"type": "object",
75+
"$defs": {
76+
"range": {
77+
"type": "object",
78+
"properties": {
79+
"start": {"type": "number"},
80+
"end": {"type": "number"},
81+
},
82+
"required": ["start", "end"],
83+
"additionalProperties": False,
84+
},
85+
},
86+
"additionalProperties": {
87+
"$ref": "#/$defs/range",
88+
},
89+
}
90+
91+
# Act
92+
actual = sort_doc_by_schema(doc_data=doc, schema_data=schema)
93+
94+
# Assert
95+
assert actual is not doc
96+
assert json.dumps(actual) == '{"foo": {"start": 10, "end": 20}}'
97+
98+
99+
def test_sort_doc_by_schema__oneof() -> None:
100+
# Arrange
101+
doc_str = '{"abc": {"end": 20, "start": 10}, "xyz": {"to": 40, "from": 30}}'
102+
doc = json.loads(doc_str)
103+
104+
schema = {
105+
"$schema": "https://json-schema.org/draft/2020-12/schema",
106+
"type": "object",
107+
"additionalProperties": {
108+
"type": "object",
109+
"oneOf": [
110+
{
111+
"properties": {
112+
"start": {"type": "number"},
113+
"end": {"type": "number"},
114+
},
115+
"required": ["start", "end"],
116+
"additionalProperties": False,
117+
},
118+
{
119+
"properties": {
120+
"from": {"type": "number"},
121+
"to": {"type": "number"},
122+
},
123+
"required": ["from", "to"],
124+
"additionalProperties": False,
125+
},
126+
],
127+
},
128+
}
129+
130+
# Act
131+
actual = sort_doc_by_schema(doc_data=doc, schema_data=schema)
132+
133+
# Assert
134+
assert actual is not doc
135+
assert json.dumps(actual) == '{"abc": {"start": 10, "end": 20}, "xyz": {"from": 30, "to": 40}}'

0 commit comments

Comments
 (0)