diff --git a/src/oold/validation/context_resolution.py b/src/oold/validation/context_resolution.py index a767ad7..6dc5e19 100644 --- a/src/oold/validation/context_resolution.py +++ b/src/oold/validation/context_resolution.py @@ -7,10 +7,9 @@ exactly what OO-LD's rule ``OOLD-CMP-b926`` guarantees by requiring a schema to be directly usable as a context. -What a processor does not expose is the *flattened active context itself* as a value a caller can -inspect. Expanding a document tells you the resulting triples, not which terms were in scope or -where each came from. This module exists for the callers that need that: reporting which terms a -schema defines, and the per-property attribution in :mod:`~oold.validation.predicates`. +The callers here need the flattened active context as a *value*: which terms a schema defines, +and the per-property attribution in :mod:`~oold.validation.predicates`. Expanding a document +gives the resulting triples, not that. ``@context`` entries are usually relative siblings, referencing *other OO-LD schemas*:: @@ -28,9 +27,25 @@ of context objects rather than being merged by hand, so JSON-LD's own override semantics still apply. -Replacing this walk with a JSON-LD processor's own context resolution is worth evaluating, if a -future need exposes that flattened form through a stable API; this module exists because none -does today, not because a processor could not in principle resolve the chain itself. +A processor does expose that value. pyld returns it from the public +``JsonLdProcessor.process_context``, one hop at a time, carrying ``@type``, ``@container``, +``protected``, ``reverse`` and prefix flags per term; an earlier version of this docstring said +no processor did, and that was wrong (issue #118). Two things keep the walk here anyway. + +``process_context`` resolves a term's *scoped* ``@context`` and then discards it. The fetch is +eager, through the document loader, and without one it raises ``invalid scoped context`` rather +than deferring anything; but the term mapping it returns still carries the value as authored, so +``{"@id": ..., "@context": "Pet.schema.json"}`` comes back with that string intact and nothing +reachable behind it. Something has to embed the scoped content itself, which is most of what +:func:`_resolve_inline` does. + +And pyld's own cycle guard does not survive the way this module has to call it. One +``process_context`` over a loop raises ``Cyclical @context URLs detected``, but attributing a term +to the hop that defined it means driving the chain a hop at a time, and successive calls share no +memory of prior hops. Provenance is therefore what costs the guard. The stack in +:func:`_walk_reference` and ``max_scoped_depth`` in :func:`_resolve_inline` replace it. No +committed fixture is cyclic, so a replacement could not be validated against the corpus here +either. """ from __future__ import annotations diff --git a/tests/test_validation/test_jsonld.py b/tests/test_validation/test_jsonld.py index a280294..6cf5a61 100644 --- a/tests/test_validation/test_jsonld.py +++ b/tests/test_validation/test_jsonld.py @@ -221,6 +221,76 @@ def test_unresolvable_context_reference_is_reported(resolver, broken_dir): assert context.errors and "NoSuchSchema" in context.errors[0] +def test_the_walker_and_pyld_agree_on_the_effective_term_set(resolver, data_dir): + """Pins the claim the module docstring rests on, across the whole committed corpus. + + The docstring justifies a hand-written walk by naming what pyld does *not* give back: the + scoped `@context` content, and a cycle bound across hop-by-hop calls. That argument is only + honest while the two agree on everything else. Where a caller needs the effective mapping and + nothing more, `process_context` is the better source - it applies JSON-LD's own override + semantics rather than `terms()`'s later-key-wins flatten - and a divergence here is the signal + that the justification needs revisiting rather than repeating. + + Key sets, not values: this module reports terms as authored (`schema:name`), pyld reports them + resolved (`http://schema.org/name`). + """ + from pyld.jsonld import JsonLdProcessor + + from oold.validation.loader import DocumentLoader + + loader = DocumentLoader(resolver, directory=data_dir) + processor = JsonLdProcessor() + # pyld reads the processing mode off the *initial* context, not the per-call options, and + # defaulting to 1.0 rejects every scoped context as "a term definition must not contain + # @context" - a failure of the harness that reads exactly like a failure of the subject. + options = {"processingMode": "json-ld-1.1"} + + compared = 0 + for path in sorted(data_dir.glob("*.schema.json")): + loaded = resolver.load(path) + if "@context" not in loaded.schema: + continue + context = resolve_context(loaded.schema, loaded.base_uri, resolver) + if context.errors or context.is_empty: + continue + + active = processor.process_context( + processor._get_initial_context(options), + context.as_jsonld(), + {**options, "base": loader.url_for(path.name), "documentLoader": loader}, + ) + theirs = {term for term in active["mappings"] if not term.startswith("@")} + assert set(context.terms()) == theirs, path.name + compared += 1 + + assert compared >= 13, f"only {compared} schemas carried a resolvable @context" + + +def test_pyld_keeps_a_scoped_context_unresolved(resolver, data_dir): + """The specific capability the walker adds, as a fact rather than an assertion in prose. + + `process_context` resolves a scoped `@context` eagerly and then discards the result, so the + term mapping still holds whatever was authored. Embedding it is `_resolve_inline`'s job. + """ + from pyld.jsonld import JsonLdProcessor + + from oold.validation.loader import DocumentLoader + + loaded = resolver.load(data_dir / "PersonWithPet.schema.json") + context = resolve_context(loaded.schema, loaded.base_uri, resolver) + loader = DocumentLoader(resolver, directory=data_dir) + options = {"processingMode": "json-ld-1.1"} + + active = JsonLdProcessor().process_context( + JsonLdProcessor()._get_initial_context(options), + context.as_jsonld(), + {**options, "base": loader.url_for("PersonWithPet.schema.json"), "documentLoader": loader}, + ) + # The walker embedded Pet's terms; pyld hands back the same value it was given and no way to + # reach behind it. + assert active["mappings"]["pets"]["@context"] == context.terms()["pets"]["@context"] + + def test_find_alias_keys_discovers_id_and_type_terms(): assert find_alias_keys({"id": "@id", "type": "@type"}) == ("id", "type") assert find_alias_keys({"identifier": {"@id": "@id"}}) == ("identifier", "@type")