Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/oold/validation/context_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*::

Expand All @@ -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
Expand Down
70 changes: 70 additions & 0 deletions tests/test_validation/test_jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading