From 82724e143868b9272d873fa4e597747646cf859a Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Mon, 21 Sep 2026 06:02:52 +0200 Subject: [PATCH] fix(frame): treat a bare @reverse term as reference-valued A reverse term's values are node references by definition, so "@type": "@id" beside it is redundant and authors omit it. Keying only on @type missed the idiomatic spelling, and framing embedded the targets. Refs OO-LD/oold-schema#181 --- src/oold/validation/frame.py | 19 +++++++++++++++---- tests/test_validation/test_jsonld.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/oold/validation/frame.py b/src/oold/validation/frame.py index d87c7b0..be315b7 100644 --- a/src/oold/validation/frame.py +++ b/src/oold/validation/frame.py @@ -155,12 +155,23 @@ def walk(node: Any) -> None: return found +def _is_reference_term(definition: Any) -> bool: + """True when a context term definition makes its values node references.""" + if not isinstance(definition, dict): + return False + return definition.get("@type") == "@id" or "@reverse" in definition + + def reference_properties(schema: dict[str, Any]) -> list[str]: """Properties whose value is a reference, so framing must leave it an IRI. - Three signals, per ``OOLD-EXT-68fa``: an ``x-oold-range`` on a string-typed value, an - IRI-family ``format`` (the family ``OOLD-EXT-6ea3`` recommends), or a context term mapped - ``"@type": "@id"``. + Four signals, per ``OOLD-EXT-6d10``: an ``x-oold-range`` on a string-typed value, an + IRI-family ``format`` (the family ``OOLD-EXT-6ea3`` recommends), a context term mapped + ``"@type": "@id"``, or one mapped with ``@reverse``. + + ``@reverse`` stands on its own. A reverse term's values are node references by definition + (JSON-LD 1.1 4.1.10), so ``"@type": "@id"`` beside it is redundant and authors omit it; + keying only on ``@type`` misses the idiomatic spelling and embeds the targets. Embedding takes precedence where a property carries both: a property shaped like an object is an embed whatever its term says. @@ -189,7 +200,7 @@ def is_reference(node: Any) -> bool: for name, prop in properties.items() if name not in aliases and not is_embed(prop) - and (is_reference(prop) or terms.get(name, {}).get("@type") == "@id") + and (is_reference(prop) or _is_reference_term(terms.get(name, {}))) ] diff --git a/tests/test_validation/test_jsonld.py b/tests/test_validation/test_jsonld.py index 6842ec5..5b50052 100644 --- a/tests/test_validation/test_jsonld.py +++ b/tests/test_validation/test_jsonld.py @@ -175,6 +175,21 @@ def test_embedding_wins_where_a_property_carries_both_signals(): assert reference_properties(schema) == [] +def test_a_bare_reverse_term_is_reference_valued(): + """A reverse term's values are node references by definition (JSON-LD 1.1 4.1.10). + + ``"@type": "@id"`` beside ``@reverse`` is redundant, and the specification's own worked + example writes both, so keying only on ``@type`` passed every fixture while missing the + idiomatic spelling and embedding the targets. + """ + schema = { + "@context": {"employees": {"@reverse": "schema:worksFor"}}, + "properties": {"employees": {"type": "array", "items": {"type": "string"}}}, + } + assert reference_properties(schema) == ["employees"] + assert schema_to_frame(schema)["employees"] == {"@embed": "@never"} + + def test_a_keyword_alias_never_gets_a_subframe(): """``id`` is the node's name, not a predicate.