diff --git a/lib/pyld/jsonld.py b/lib/pyld/jsonld.py index cc6e0bc..e86bec6 100644 --- a/lib/pyld/jsonld.py +++ b/lib/pyld/jsonld.py @@ -2615,11 +2615,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') @@ -2628,6 +2623,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') ) @@ -2770,13 +2770,13 @@ def _expand_object( code='invalid value object value', ) - # expand each nested key - for key in nests: + # expand each nested key and scoped context + for key, term_ctx in nests: for nv in JsonLdProcessor.arrayify(element[key]): if not _is_object(nv) or [ k for k, v in nv.items() - if self._expand_iri(active_ctx, k, vocab=True) == '@value' + if self._expand_iri(term_ctx, k, vocab=True) == '@value' ]: raise JsonLdError( 'Invalid JSON-LD syntax; nested value must be a node object.', @@ -2785,7 +2785,7 @@ def _expand_object( code='invalid @nest value', ) self._expand_object( - active_ctx, + term_ctx, active_property, expanded_active_property, nv, diff --git a/tests/runtests.py b/tests/runtests.py index dd82b31..b9f7f97 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -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': { diff --git a/tests/test_jsonld.py b/tests/test_jsonld.py index 6f9699d..9f01e0d 100644 --- a/tests/test_jsonld.py +++ b/tests/test_jsonld.py @@ -316,6 +316,94 @@ 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. + + JSON-LD 1.1 defines property nesting as semantically transparent: nested + properties are removed during expansion and treated as if their contents + were declared directly on the containing node object. It also defines + scoped contexts as property-scoped when the term is used as a property + and type-scoped when the term is used as a type. This test combines both + rules deliberately: + + * p1 is an @nest term with a property-scoped context. + * That context defines Type and gives Type its own type-scoped context. + * The nested node uses Type and then uses p2 from Type's scoped context. + + 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. The expected result proves that the @nest term + context is active before @type is expanded and before Type's scoped + context is applied. + """ + input = { + "@context": { + "@vocab": "http://example.org/outer#", + "p1": { + "@id": "@nest", + "@context": { + "Type": { + "@id": "http://example.org/ns#Type", + "@context": { + "p2": "http://example.org/ns#P2", + }, + }, + }, + }, + }, + "p1": { + "@type": "Type", + "p2": "foo", + }, + } + + expected = [ + { + "@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.""" @@ -692,6 +780,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 = """ _:b0 . + "test" . +_:b0 "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 = """ "test" . + "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