Skip to content

Commit b82eabb

Browse files
namedgraphclaude
andcommitted
Broaden embedded-JSON-LD test coverage
Add cases for ops/variables in positions beyond a subject @id: - op as a property value (object position) -> literal - op-valued @type -> IRI (rdf:type triple) - variable from a context binding (bare name) — the ForEach-row case - embedded ops inside a @graph array (list recursion + @graph trigger) - @context preserved intact alongside a sibling embedded op - native JSON scalar left untouched in an op-bearing document so its JSON-LD datatype (xsd:integer) survives Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 92c8e9c commit b82eabb

1 file changed

Lines changed: 112 additions & 1 deletion

File tree

tests/unit/test_process_json_jsonld.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@
1818

1919
from __future__ import annotations
2020

21-
from rdflib import BNode, Graph, URIRef
21+
from types import SimpleNamespace
22+
23+
from rdflib import BNode, Graph, Literal, URIRef
24+
from rdflib.namespace import RDF
2225

2326
from web_algebra.operation import Operation
2427

2528
PROV_GENERATED_BY = URIRef("http://www.w3.org/ns/prov#wasGeneratedBy")
2629
FOAF_PRIMARY_TOPIC = URIRef("http://xmlns.com/foaf/0.1/primaryTopic")
30+
FOAF_NAME = URIRef("http://xmlns.com/foaf/0.1/name")
31+
EX_AGE = URIRef("http://example.org/age")
32+
DOC_TYPE = URIRef("http://example.org/Document")
2733
DOC_URI = URIRef("https://example.org/doc/")
2834
ACTIVITY_URI = URIRef("https://example.org/activity/#this")
2935
MESSAGE_URI = URIRef("https://example.org/message/#this")
@@ -120,3 +126,108 @@ def test_pure_data_jsonld_parses_to_expected_graph(self, settings):
120126
graph.parse(data=json.dumps(result), format="json-ld")
121127

122128
assert (DOC_URI, PROV_GENERATED_BY, ACTIVITY_URI) in graph
129+
130+
131+
class TestEmbeddedOpsAndVariables:
132+
"""Ops/variables embedded in positions other than a subject @id."""
133+
134+
def test_op_as_property_value_resolves_to_literal(self, settings):
135+
# An embedded op in object position (a property value), not an @id.
136+
json_data = {
137+
"@id": str(DOC_URI),
138+
str(FOAF_NAME): {"@op": "Value", "args": {"name": "$name"}},
139+
}
140+
stack = [{"name": Literal("Alice")}]
141+
142+
result = Operation.process_json(settings, json_data, {}, stack)
143+
graph = Operation.to_graph(result)
144+
145+
names = list(graph.objects(DOC_URI, FOAF_NAME))
146+
assert [str(n) for n in names] == ["Alice"]
147+
148+
def test_op_in_type_resolves_to_iri(self, settings):
149+
# @type expects an IRI; an op-valued @type must resolve to a URIRef.
150+
json_data = {
151+
"@id": str(DOC_URI),
152+
"@type": {"@op": "Value", "args": {"name": "$cls"}},
153+
}
154+
stack = [{"cls": DOC_TYPE}]
155+
156+
result = Operation.process_json(settings, json_data, {}, stack)
157+
graph = Operation.to_graph(result)
158+
159+
assert (DOC_URI, RDF.type, DOC_TYPE) in graph
160+
161+
def test_variable_from_context_binding(self, settings):
162+
# Bare name (no $) resolves from the context — the ForEach-row case from
163+
# the original bug report.
164+
json_data = {
165+
"@id": {"@op": "Value", "args": {"name": "doc"}},
166+
str(FOAF_PRIMARY_TOPIC): {
167+
"@id": {"@op": "Value", "args": {"name": "topic"}}
168+
},
169+
}
170+
context = SimpleNamespace(doc=DOC_URI, topic=MESSAGE_URI)
171+
172+
result = Operation.process_json(settings, json_data, context, [])
173+
graph = Operation.to_graph(result)
174+
175+
assert (DOC_URI, FOAF_PRIMARY_TOPIC, MESSAGE_URI) in graph
176+
assert not any(isinstance(s, BNode) for s, _, _ in graph)
177+
178+
def test_embedded_ops_in_graph_array(self, settings):
179+
# @graph is a list of nodes; embedded ops in each must resolve, and the
180+
# @graph keyword must trigger JSON-LD handling (not generic recursion).
181+
a, b = URIRef("https://example.org/a"), URIRef("https://example.org/b")
182+
json_data = {
183+
"@graph": [
184+
{
185+
"@id": {"@op": "Value", "args": {"name": "$a"}},
186+
str(FOAF_NAME): "A",
187+
},
188+
{
189+
"@id": {"@op": "Value", "args": {"name": "$b"}},
190+
str(FOAF_NAME): "B",
191+
},
192+
]
193+
}
194+
stack = [{"a": a, "b": b}]
195+
196+
result = Operation.process_json(settings, json_data, {}, stack)
197+
graph = Operation.to_graph(result)
198+
199+
assert [str(n) for n in graph.objects(a, FOAF_NAME)] == ["A"]
200+
assert [str(n) for n in graph.objects(b, FOAF_NAME)] == ["B"]
201+
assert not any(isinstance(s, BNode) for s, _, _ in graph)
202+
203+
def test_context_keyword_preserved_with_embedded_op(self, settings):
204+
# A @context (which carries no @op) must survive intact so the parser
205+
# can use it, while a sibling embedded op still resolves.
206+
json_data = {
207+
"@context": {"name": str(FOAF_NAME)},
208+
"@id": str(DOC_URI),
209+
"name": {"@op": "Value", "args": {"name": "$name"}},
210+
}
211+
stack = [{"name": Literal("Alice")}]
212+
213+
result = Operation.process_json(settings, json_data, {}, stack)
214+
215+
assert result["@context"] == {"name": str(FOAF_NAME)}
216+
graph = Operation.to_graph(result)
217+
assert [str(n) for n in graph.objects(DOC_URI, FOAF_NAME)] == ["Alice"]
218+
219+
def test_native_scalar_preserved_in_op_bearing_document(self, settings):
220+
# A plain JSON scalar inside an op-bearing document is left as native
221+
# JSON (not coerced to an rdflib term), so its JSON-LD datatype survives.
222+
json_data = {
223+
"@id": {"@op": "Value", "args": {"name": "$doc"}},
224+
str(EX_AGE): 30,
225+
}
226+
stack = [{"doc": DOC_URI}]
227+
228+
result = Operation.process_json(settings, json_data, {}, stack)
229+
230+
assert result[str(EX_AGE)] == 30 # still a Python int, not a Literal
231+
graph = Operation.to_graph(result)
232+
ages = list(graph.objects(DOC_URI, EX_AGE))
233+
assert len(ages) == 1 and ages[0].value == 30 # parsed as xsd:integer

0 commit comments

Comments
 (0)