Skip to content

Commit b5d5f1a

Browse files
committed
fix(store): check ancestor $ref dialects during chain validation
_validate_chain_dialect seeded its reference walk from the selected type only, so a cross-dialect gts:// $ref on an ancestor was accepted by validate_instance_content and OP#12. Seed the walk from every type in the chain so the whole chain plus its reference closure shares the root dialect (spec 11.0/12), matching the Rust reference. Signed-off-by: Artifizer <artifizer@gmail.com>
1 parent fdce27d commit b5d5f1a

2 files changed

Lines changed: 62 additions & 9 deletions

File tree

‎gts/src/gts/store.py‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ def _validate_chain_dialect(
502502
return
503503
root_dialect = self._schema_dialect(root_content)
504504

505+
visited: set[str] = set()
506+
queue: deque[tuple[str, dict[str, Any]]] = deque()
505507
for chain_id in chain_ids:
506508
entity = self.get(chain_id)
507509
content = (
@@ -521,15 +523,12 @@ def _validate_chain_dialect(
521523
"$id hierarchy must use the root type's dialect"
522524
)
523525
self._validate_local_ref_dialects(content, chain_ids[0], root_dialect)
524-
525-
visited: set[str] = set()
526-
queue: deque[tuple[str, dict[str, Any]]] = deque(
527-
[(gts_id, transient_schema)] if transient_schema is not None else []
528-
)
529-
if not queue:
530-
entity = self.get(gts_id)
531-
if entity and isinstance(entity.content, dict):
532-
queue.append((gts_id, entity.content))
526+
# Seed the reference walk from every chain member, not just the
527+
# leaf, so a cross-dialect gts:// $ref on an ancestor is caught
528+
# even when the leaf does not reference it (spec §11.0/§12;
529+
# matches the Rust reference which validates each related type in
530+
# the closure).
531+
queue.append((chain_id, content))
533532
while queue:
534533
current_id, content = queue.popleft()
535534
if current_id in visited:

‎tests/test_store_extra.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,60 @@ def test_transitive_ref_dialect_mismatch_raises(self):
452452
with pytest.raises(ValueError, match="gts.x.test._.foreign.v1~"):
453453
store._validate_schema_chain("gts.x.test._.host.v1~")
454454

455+
def test_ancestor_ref_dialect_mismatch_raises(self):
456+
# Issue C: the cross-dialect $ref lives on an ancestor; the descendant
457+
# derives by re-declaration and references neither the ancestor nor the
458+
# 2020-12 target. A leaf-only reference walk would accept it; walking the
459+
# whole chain closure must reject it (OP#12 path).
460+
foreign = _schema_entity(
461+
"gts.x.test._.ancforeign.v1~",
462+
{"$schema": "https://json-schema.org/draft/2020-12/schema"},
463+
)
464+
base = _schema_entity(
465+
"gts.x.test._.ancbase.v1~",
466+
{"properties": {"ext": {"$ref": "gts://gts.x.test._.ancforeign.v1~"}}},
467+
)
468+
child = _schema_entity(
469+
"gts.x.test._.ancbase.v1~x.test._.ancchild.v1~",
470+
{"properties": {"label": {"type": "string"}}},
471+
)
472+
store = GtsStore(reader=None)
473+
store.register(foreign)
474+
store.register(base)
475+
store.register(child)
476+
477+
with pytest.raises(ValueError, match="gts.x.test._.ancforeign.v1~"):
478+
store._validate_schema_chain(
479+
"gts.x.test._.ancbase.v1~x.test._.ancchild.v1~"
480+
)
481+
482+
def test_instance_content_rejects_ancestor_cross_dialect_ref(self):
483+
# Issue C on the OP#6 instance path: validating an instance of the
484+
# descendant must reject it because an ancestor references a schema of a
485+
# different dialect.
486+
foreign = _schema_entity(
487+
"gts.x.test._.ancforeign2.v1~",
488+
{"$schema": "https://json-schema.org/draft/2020-12/schema"},
489+
)
490+
base = _schema_entity(
491+
"gts.x.test._.ancbase2.v1~",
492+
{"properties": {"ext": {"$ref": "gts://gts.x.test._.ancforeign2.v1~"}}},
493+
)
494+
child = _schema_entity(
495+
"gts.x.test._.ancbase2.v1~x.test._.ancchild2.v1~",
496+
{"properties": {"label": {"type": "string"}}},
497+
)
498+
store = GtsStore(reader=None)
499+
store.register(foreign)
500+
store.register(base)
501+
store.register(child)
502+
503+
with pytest.raises(ValueError, match="gts.x.test._.ancforeign2.v1~"):
504+
store.validate_instance_content(
505+
{"label": "ok"},
506+
"gts.x.test._.ancbase2.v1~x.test._.ancchild2.v1~",
507+
)
508+
455509
def test_trait_resource_dialect_mismatch_raises(self):
456510
schema_id = "gts.x.test._.trait_resource.v1~"
457511
schema = _schema_entity(

0 commit comments

Comments
 (0)