diff --git a/pyproject.toml b/pyproject.toml index 91b0b2e..a417f21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "web-algebra" -version = "1.4.0" +version = "1.4.1" description = "Composable RDF operations in JSON" readme = "README.md" license = "Apache-2.0" diff --git a/src/web_algebra/operation.py b/src/web_algebra/operation.py index fd26ea9..2cae5b5 100644 --- a/src/web_algebra/operation.py +++ b/src/web_algebra/operation.py @@ -222,18 +222,23 @@ def to_graph(data: Any, *, base: Optional[str] = None) -> Graph: already a `Graph` (e.g. the output of an upstream op such as CONSTRUCT) passes through unchanged. + Both shapes of JSON-LD document are accepted: a single node object + (`dict`) and a top-level array of nodes (`list`) — the latter is what + SPARQL DESCRIBE/CONSTRUCT over multiple subjects returns. rdflib's + JSON-LD parser handles both natively. + `base` is the document's base IRI — typically the target URL of the enclosing op — used to resolve any relative IRIs in the JSON-LD. """ if isinstance(data, Graph): return data - if isinstance(data, dict): + if isinstance(data, (dict, list)): graph = Graph() graph.parse(data=json.dumps(data), format="json-ld", publicID=base) return graph raise TypeError( f"Cannot convert {type(data).__name__} to Graph; " - "expected a JSON-LD dict or an rdflib.Graph" + "expected a JSON-LD dict/list or an rdflib.Graph" ) @staticmethod diff --git a/tests/unit/test_to_graph.py b/tests/unit/test_to_graph.py index 53009cd..461681b 100644 --- a/tests/unit/test_to_graph.py +++ b/tests/unit/test_to_graph.py @@ -10,12 +10,14 @@ import pytest from rdflib import Graph, URIRef +from rdflib.namespace import RDF from web_algebra.operation import Operation DOC_URI = URIRef("https://example.org/doc/") PROV_GENERATED_BY = URIRef("http://www.w3.org/ns/prov#wasGeneratedBy") ACTIVITY_URI = URIRef("https://example.org/activity/#this") +DOC_TYPE = URIRef("https://example.org/T") class TestToGraph: @@ -30,6 +32,20 @@ def test_dict_is_parsed_as_jsonld(self): assert isinstance(graph, Graph) assert (DOC_URI, PROV_GENERATED_BY, ACTIVITY_URI) in graph + def test_top_level_array_is_parsed_as_jsonld(self): + # SPARQL DESCRIBE/CONSTRUCT over multiple subjects returns a top-level + # JSON-LD array of nodes; to_graph must accept it, not raise. + data = [ + {"@id": str(DOC_URI), "@type": [str(DOC_TYPE)]}, + {"@id": str(ACTIVITY_URI), str(PROV_GENERATED_BY): {"@id": str(DOC_URI)}}, + ] + + graph = Operation.to_graph(data) + + assert isinstance(graph, Graph) + assert (DOC_URI, RDF.type, DOC_TYPE) in graph + assert (ACTIVITY_URI, PROV_GENERATED_BY, DOC_URI) in graph + def test_graph_passes_through_unchanged(self): # An upstream op (e.g. CONSTRUCT) already produced a Graph — identity. g = Graph()