Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Options from the test manifest now override the options configured in `create_test_options()`, instead of the other way around. This fixes tests not able to override default options in the test-setup such as `extractAllScripts`. Fixes [html#tf004](https://w3c.github.io/json-ld-api/tests/html-manifest#tf004).
- When `@type` is `@json` in a frame, it no longer raises an "Invalid JSON-LD syntax" error. Fixes [frame#t0069](https://w3c.github.io/json-ld-framing/tests/frame-manifest.html#t0069).
- Use safeguard for non-dict values of `options['link']`
- Local and type-scoped contexts are now properly resolved for nested node objects, so a scoped context on a `@nest` term is being applied to nested properties. Fixes [toRdf#tc037](https://w3c.github.io/json-ld-api/tests/toRdf-manifest.html#tc037) and [toRdf#tc038](https://w3c.github.io/json-ld-api/tests/toRdf-manifest.html#tc038).

### Changed
- `requests_document_loader()` and `aiohttp_document_loader()` now return
Expand Down
109 changes: 72 additions & 37 deletions lib/pyld/jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -2139,35 +2139,10 @@ def _expand(
active_ctx, property_scoped_ctx, options, override_protected=True
)

# recursively expand object
# if element has a context, process it
if '@context' in element:
active_ctx = self._process_context(active_ctx, element['@context'], options)

# set the type-scoped context to the context on input, for use later
type_scoped_ctx = active_ctx

# Remember the first key found expanding to @type
type_key = None

# look for scoped context on @type
for key, _value in sorted(element.items()):
expanded_property = self._expand_iri(active_ctx, key, vocab=True)
if expanded_property == '@type':
if not type_key:
type_key = key
# set scoped contexts from @type
types = [
t for t in JsonLdProcessor.arrayify(element[key]) if _is_string(t)
]
for type_ in sorted(types):
ctx = JsonLdProcessor.get_context_value(
type_scoped_ctx, type_, '@context'
)
if ctx is not None and ctx is not False:
active_ctx = self._process_context(
active_ctx, ctx, options, propagate=False
)
# prepare type-scoped contexts when nested
active_ctx, type_key, type_scoped_ctx = (
self._prepare_nested_context(active_ctx, element, options)
)

# process each key and value in element, ignoring @nest content
rval = {}
Expand Down Expand Up @@ -2620,11 +2595,6 @@ def _expand_object(

continue

# nested keys
if expanded_property == '@nest':
nests.append(key)
continue

# use potential scoped context for key
term_ctx = active_ctx
ctx = JsonLdProcessor.get_context_value(active_ctx, key, '@context')
Expand All @@ -2633,6 +2603,11 @@ def _expand_object(
active_ctx, ctx, options, propagate=True, override_protected=True
)

# for nested keys, add scoped context with key
if expanded_property == '@nest':
nests.append((key, term_ctx))
continue

container = JsonLdProcessor.arrayify(
JsonLdProcessor.get_context_value(active_ctx, key, '@container')
)
Expand Down Expand Up @@ -2775,10 +2750,24 @@ def _expand_object(
code='invalid value object value',
)

# expand each nested key
for key in nests:
# Nested values merge into the current node, but their term and type
# scoped contexts must still be prepared as if expanding a node object.
for key, term_ctx in nests:
for nv in JsonLdProcessor.arrayify(element[key]):
if not _is_object(nv) or [
if not _is_object(nv):
raise JsonLdError(
'Invalid JSON-LD syntax; nested value must be a node object.',
'jsonld.SyntaxError',
{'value': nv},
code='invalid @nest value',
)

# prepare type-scoped contexts when nested
active_ctx, type_key, type_scoped_ctx = (
self._prepare_nested_context(term_ctx, nv, options)
)

if [
k
for k, v in nv.items()
if self._expand_iri(active_ctx, k, vocab=True) == '@value'
Expand All @@ -2789,6 +2778,7 @@ def _expand_object(
{'value': nv},
code='invalid @nest value',
)

self._expand_object(
active_ctx,
active_property,
Expand All @@ -2801,6 +2791,51 @@ def _expand_object(
type_scoped_ctx=type_scoped_ctx,
)

def _prepare_nested_context(self, active_ctx, element, options):
"""
Prepare local and type-scoped contexts for a nested node object.

`@nest` is semantically transparent in JSON-LD 1.1, so nested properties
are merged into the parent node. Context processing is not transparent:
a scoped context on the nest term must be active while discovering any
@type entries and applying the type-scoped contexts they reference.
"""
# Recursively expand object: process context if element has one
# Local contexts on the nested node apply before looking for @type,
# just like they do for an ordinary node object.
if '@context' in element:
active_ctx = self._process_context(active_ctx, element['@context'], options)

# Set the type-scoped context to the context on input, for use later
type_scoped_ctx = active_ctx

# Remember the first key found expanding to @type
type_key = None

# Type terms are looked up against the context that was active before
# applying any type-scoped contexts discovered below.
for key, value in sorted(element.items()):
# The @type entry itself may be aliased by the nest term's scoped
# context, so use the nested active context to identify it.
if self._expand_iri(active_ctx, key, vocab=True) != '@type':
continue

type_key = type_key or key
# set scoped contexts from @type
types = [t for t in JsonLdProcessor.arrayify(value) if _is_string(t)]
for type_ in sorted(types):
ctx = JsonLdProcessor.get_context_value(
type_scoped_ctx, type_, '@context'
)
if ctx is not None and ctx is not False:
# Type-scoped contexts affect expansion of this nested node,
# but must not leak into sibling properties or parent nodes.
active_ctx = self._process_context(
active_ctx, ctx, options, propagate=False
)

return active_ctx, type_key, type_scoped_ctx

def _flatten(self, input):
"""
Performs JSON-LD flattening.
Expand Down
2 changes: 0 additions & 2 deletions tests/runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,12 +1019,10 @@ def write(self, filename):
# well formed
'.*toRdf-manifest#twf05$',
# uncategorized
'.*toRdf-manifest#tc038$',
'.*toRdf-manifest#ter54$',
'.*toRdf-manifest#ter56$',
'.*toRdf-manifest#tli12$',
'.*toRdf-manifest#tli14$',
'.*toRdf-manifest#tc037$',
]
},
'skip': {
Expand Down
132 changes: 132 additions & 0 deletions tests/test_jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,85 @@ def test_structured_value_still_works_with_scoped_context(self):
# meaning -> @id
assert "@id" in prop_val

# Issue 204
def test_scoped_context_on_nest_term_expands_nested_properties(self):
"""A scoped context on a @nest term should apply to nested properties."""
input = {
"@context": {
"@vocab": "http://example.org/vocab#",
"p1": {
"@id": "@nest",
"@context": {"p2": "http://example.org/ns#P2"},
},
},
"p1": {"p2": "foo"},
}

expected = [
{
"http://example.org/ns#P2": [
{
"@value": "foo",
}
],
}
]

result = jsonld.expand(input)

assert result == expected

# Issue 204
def test_scoped_context_on_nest_term_expands_nested_type_scoped_context(self):
"""
A scoped context on a @nest term should be in effect when expanding the
nested node, including when processing any type-scoped contexts found on
that node.
"""
input = {
"@context": {
"@vocab": "http://example.org/outer#",
# p1 is an @nest term with a property-scoped context. That context defines
# Type and gives Type its own type-scoped context.
"p1": {
"@id": "@nest",
"@context": {
# The nested node uses Type and then uses p2 from Type's scoped context.
"Type": {
"@id": "http://example.org/ns#Type",
"@context": {
"p2": "http://example.org/ns#P2",
},
},
},
},
},
"p1": {
"@type": "Type",
"p2": "foo",
},
}

# The @nest term context is active before @type is expanded and before Type's scoped
# context is applied.
expected = [
{
# If nested values are expanded by directly walking their keys instead of
# running the normal expansion setup for the nested node, Type and p2 fall
# back to the outer @vocab.
"@type": ["http://example.org/ns#Type"],
"http://example.org/ns#P2": [
{
"@value": "foo",
}
],
}
]

result = jsonld.expand(input)

assert result == expected


def test_mixed_plain_and_vocab_terms(self):
"""Contexts with both plain and @type:@vocab terms should work correctly."""
Expand Down Expand Up @@ -752,6 +831,59 @@ def test_double_and_float_values(self):
result = jsonld.to_rdf(input)
assert result == expected

# Issue 204
def test_conflicting_property_names(self):
"""
Conversion to RDF should allow a node in the root @context with
a conflicting property name in its own @context
"""
input = {
"@context": {
"dublinCore": {
"@id": "http://foo.bar/dc",
"@context": {"title": "http://purl.org/dc/terms/title"},
},
"title": "http://foo.bar/title",
},
"@id": "http://foo.bar/obj/test",
"title": "test",
"dublinCore": {"title": "Chapter 1: Jonathan Harker's Journal"},
}

expected = """<http://foo.bar/obj/test> <http://foo.bar/dc> _:b0 .
<http://foo.bar/obj/test> <http://foo.bar/title> "test" .
_:b0 <http://purl.org/dc/terms/title> "Chapter 1: Jonathan Harker's Journal" .
"""

nquads = jsonld.to_rdf(input, options={'format': 'application/n-quads'})
assert nquads == expected


def test_conflicting_property_names_in_nested_node(self):
"""
Conversion to RDF should not ignore a @nest'ed node in the root @context
a conflicting property name in its own @context
"""
input = {
"@context": {
"dublinCore": {
"@id": "@nest",
"@context": {"title": "http://purl.org/dc/terms/title"},
},
"title": "http://foo.bar/title",
},
"@id": "http://foo.bar/obj/test",
"title": "test",
"dublinCore": {"title": "Chapter 1: Jonathan Harker's Journal"},
}

expected = """<http://foo.bar/obj/test> <http://foo.bar/title> "test" .
<http://foo.bar/obj/test> <http://purl.org/dc/terms/title> "Chapter 1: Jonathan Harker's Journal" .
"""

nquads = jsonld.to_rdf(input, options={'format': 'application/n-quads'})
assert nquads == expected


class TestCompact:
# Issue 59 - PR: https://github.com/digitalbazaar/pyld/pull/60
Expand Down
Loading