|
18 | 18 |
|
19 | 19 | from __future__ import annotations |
20 | 20 |
|
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 |
22 | 25 |
|
23 | 26 | from web_algebra.operation import Operation |
24 | 27 |
|
25 | 28 | PROV_GENERATED_BY = URIRef("http://www.w3.org/ns/prov#wasGeneratedBy") |
26 | 29 | 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") |
27 | 33 | DOC_URI = URIRef("https://example.org/doc/") |
28 | 34 | ACTIVITY_URI = URIRef("https://example.org/activity/#this") |
29 | 35 | MESSAGE_URI = URIRef("https://example.org/message/#this") |
@@ -120,3 +126,108 @@ def test_pure_data_jsonld_parses_to_expected_graph(self, settings): |
120 | 126 | graph.parse(data=json.dumps(result), format="json-ld") |
121 | 127 |
|
122 | 128 | 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