diff --git a/lib/rdf/data.ex b/lib/rdf/data.ex index 6a8a12e..b9632a7 100644 --- a/lib/rdf/data.ex +++ b/lib/rdf/data.ex @@ -3,8 +3,26 @@ defprotocol RDF.Data do An abstraction over the different data structures for collections of RDF statements. """ + @doc """ + Adds statements to a RDF data structure. + + As opposed to the specific `add` functions on the RDF data structures, which + always return the same structure type than the first argument, `merge` might + result in another RDF data structure, eg. merging two `RDF.Description` with + different subjects results in a `RDF.Graph` or adding a quad to a `RDF.Graph` + with a different name than the graph context of the quad results in a + `RDF.Dataset`. But it is always guaranteed that the resulting structure has + a `RDF.Data` implementation. + """ + def merge(data, statements) + @doc """ Deletes statements from a RDF data structure. + + As opposed to the `delete` functions on RDF data structures directly, this + function only deletes exactly matching structures. + + TODO: rename this function to make the different semantics explicit """ def delete(data, statements) @@ -64,10 +82,40 @@ defprotocol RDF.Data do end defimpl RDF.Data, for: RDF.Description do + def merge(%RDF.Description{subject: subject} = description, {s, _, _} = triple) do + with ^subject <- RDF.Statement.convert_subject(s) do + RDF.Description.add(description, triple) + else + _ -> + RDF.Graph.new(description) + |> RDF.Graph.add(triple) + end + end + + def merge(description, {_, _, _, _} = quad), + do: RDF.Dataset.new(description) |> RDF.Dataset.add(quad) + + def merge(%RDF.Description{subject: subject} = description, + %RDF.Description{subject: other_subject} = other_description) + when other_subject == subject, + do: RDF.Description.add(description, other_description) + + def merge(description, %RDF.Description{} = other_description), + do: RDF.Graph.new(description) |> RDF.Graph.add(other_description) + + def merge(description, %RDF.Graph{} = graph), + do: RDF.Data.merge(graph, description) + + def merge(description, %RDF.Dataset{} = dataset), + do: RDF.Data.merge(dataset, description) + + def delete(%RDF.Description{subject: subject} = description, %RDF.Description{subject: other_subject}) when subject != other_subject, do: description def delete(description, statements), do: RDF.Description.delete(description, statements) + + def pop(description), do: RDF.Description.pop(description) def include?(description, statements), @@ -75,11 +123,11 @@ defimpl RDF.Data, for: RDF.Description do def statements(description), do: RDF.Description.statements(description) - def description(%RDF.Description{subject: subject} = description, requested_subject) do - with ^subject <- RDF.Statement.convert_subject(requested_subject) do + def description(%RDF.Description{subject: subject} = description, s) do + with ^subject <- RDF.Statement.convert_subject(s) do description else - _ -> RDF.Description.new(requested_subject) + _ -> RDF.Description.new(s) end end @@ -94,10 +142,43 @@ defimpl RDF.Data, for: RDF.Description do def statement_count(description), do: RDF.Description.count(description) end + defimpl RDF.Data, for: RDF.Graph do + def merge(%RDF.Graph{name: name} = graph, {_, _, _, graph_context} = quad) do + with ^name <- RDF.Statement.convert_graph_name(graph_context) do + RDF.Graph.add(graph, quad) + else + _ -> + RDF.Dataset.new(graph) + |> RDF.Dataset.add(quad) + end + end + + def merge(graph, {_, _, _} = triple), + do: RDF.Graph.add(graph, triple) + + def merge(description, {_, _, _, _} = quad), + do: RDF.Dataset.new(description) |> RDF.Dataset.add(quad) + + def merge(graph, %RDF.Description{} = description), + do: RDF.Graph.add(graph, description) + + def merge(%RDF.Graph{name: name} = graph, + %RDF.Graph{name: other_name} = other_graph) + when other_name == name, + do: RDF.Graph.add(graph, other_graph) + + def merge(graph, %RDF.Graph{} = other_graph), + do: RDF.Dataset.new(graph) |> RDF.Dataset.add(other_graph) + + def merge(graph, %RDF.Dataset{} = dataset), + do: RDF.Data.merge(dataset, graph) + + def delete(%RDF.Graph{name: name} = graph, %RDF.Graph{name: other_name}) when name != other_name, do: graph def delete(graph, statements), do: RDF.Graph.delete(graph, statements) + def pop(graph), do: RDF.Graph.pop(graph) def include?(graph, statements), do: RDF.Graph.include?(graph, statements) @@ -116,10 +197,24 @@ defimpl RDF.Data, for: RDF.Graph do def statement_count(graph), do: RDF.Graph.triple_count(graph) end + defimpl RDF.Data, for: RDF.Dataset do + def merge(dataset, {_, _, _} = triple), + do: RDF.Dataset.add(dataset, triple) + def merge(dataset, {_, _, _, _} = quad), + do: RDF.Dataset.add(dataset, quad) + def merge(dataset, %RDF.Description{} = description), + do: RDF.Dataset.add(dataset, description) + def merge(dataset, %RDF.Graph{} = graph), + do: RDF.Dataset.add(dataset, graph) + def merge(dataset, %RDF.Dataset{} = other_dataset), + do: RDF.Dataset.add(dataset, other_dataset) + + def delete(%RDF.Dataset{name: name} = dataset, %RDF.Dataset{name: other_name}) when name != other_name, do: dataset def delete(dataset, statements), do: RDF.Dataset.delete(dataset, statements) + def pop(dataset), do: RDF.Dataset.pop(dataset) def include?(dataset, statements), do: RDF.Dataset.include?(dataset, statements) diff --git a/test/unit/data_test.exs b/test/unit/data_test.exs index 0a3bd8f..cb8cd11 100644 --- a/test/unit/data_test.exs +++ b/test/unit/data_test.exs @@ -14,24 +14,62 @@ defmodule RDF.DataTest do EX.S2 |> EX.p2(EX.O3, EX.O4) ) + named_graph = %Graph{graph | name: uri(EX.NamedGraph)} dataset = Dataset.new |> Dataset.add(graph) - |> Dataset.add(( + |> Dataset.add( Graph.new(EX.NamedGraph) |> Graph.add(description) |> Graph.add({EX.S3, EX.p3, EX.O5}) - |> Graph.add({EX.S, EX.p3, EX.O5})), EX.NamedGraph - ) + |> Graph.add({EX.S, EX.p3, EX.O5})) {:ok, description: description, - graph: graph, + graph: graph, named_graph: named_graph, dataset: dataset, } end describe "RDF.Data protocol implementation of RDF.Description" do + test "merge of a single triple with different subject", %{description: description} do + assert RDF.Data.merge(description, {EX.Other, EX.p1, EX.O3}) == + Graph.new(description) |> Graph.add({EX.Other, EX.p1, EX.O3}) + end + + test "merge of a single triple with same subject", %{description: description} do + assert RDF.Data.merge(description, {EX.S, EX.p1, EX.O3}) == + Description.add(description, {EX.S, EX.p1, EX.O3}) + end + + test "merge of a single quad", %{description: description} do + assert RDF.Data.merge(description, {EX.Other, EX.p1, EX.O3, EX.Graph}) == + Dataset.new(description) |> Dataset.add({EX.Other, EX.p1, EX.O3, EX.Graph}) + assert RDF.Data.merge(description, {EX.S, EX.p1, EX.O3, EX.Graph}) == + Dataset.new(description) |> Dataset.add({EX.S, EX.p1, EX.O3, EX.Graph}) + end + + test "merge of a description with different subject", %{description: description} do + assert RDF.Data.merge(description, Description.new({EX.Other, EX.p1, EX.O3})) == + Graph.new(description) |> Graph.add({EX.Other, EX.p1, EX.O3}) + end + + test "merge of a description with same subject", %{description: description} do + assert RDF.Data.merge(description, Description.new({EX.S, EX.p1, EX.O3})) == + Description.add(description, {EX.S, EX.p1, EX.O3}) + end + + test "merge of a graph", %{graph: graph} do + assert RDF.Data.merge(Description.new({EX.Other, EX.p1, EX.O3}), graph) == + Graph.add(graph, {EX.Other, EX.p1, EX.O3}) + end + + test "merge of a dataset", %{dataset: dataset} do + assert RDF.Data.merge(Description.new({EX.Other, EX.p1, EX.O3}), dataset) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3}) + end + + test "delete", %{description: description} do assert RDF.Data.delete(description, {EX.S, EX.p1, EX.O2}) == Description.delete(description, {EX.S, EX.p1, EX.O2}) @@ -43,6 +81,7 @@ defmodule RDF.DataTest do %Description{description | subject: EX.Other}) == description end + test "pop", %{description: description} do assert RDF.Data.pop(description) == Description.pop(description) end @@ -97,6 +136,60 @@ defmodule RDF.DataTest do describe "RDF.Data protocol implementation of RDF.Graph" do + test "merge of a single triple", %{graph: graph, named_graph: named_graph} do + assert RDF.Data.merge(graph, {EX.Other, EX.p, EX.O}) == + Graph.add(graph, {EX.Other, EX.p, EX.O}) + assert RDF.Data.merge(named_graph, {EX.Other, EX.p, EX.O}) == + Graph.add(named_graph, {EX.Other, EX.p, EX.O}) + end + + test "merge of a single quad with the same graph context", + %{graph: graph, named_graph: named_graph} do + assert RDF.Data.merge(graph, {EX.Other, EX.p, EX.O, nil}) == + Graph.add(graph, {EX.Other, EX.p, EX.O}) + assert RDF.Data.merge(named_graph, {EX.Other, EX.p, EX.O, EX.NamedGraph}) == + Graph.add(named_graph, {EX.Other, EX.p, EX.O}) + end + + test "merge of a single quad with a different graph context", + %{graph: graph, named_graph: named_graph} do + assert RDF.Data.merge(graph, {EX.S, EX.p1, EX.O3, EX.NamedGraph}) == + Dataset.new(graph) |> Dataset.add({EX.S, EX.p1, EX.O3, EX.NamedGraph}) + assert RDF.Data.merge(named_graph, {EX.S, EX.p1, EX.O3, nil}) == + Dataset.new(named_graph) |> Dataset.add({EX.S, EX.p1, EX.O3, nil}) + end + + test "merge of a description", %{graph: graph} do + assert RDF.Data.merge(graph, Description.new({EX.Other, EX.p1, EX.O3})) == + Graph.add(graph, {EX.Other, EX.p1, EX.O3}) + assert RDF.Data.merge(graph, Description.new({EX.S, EX.p1, EX.O3})) == + Graph.add(graph, {EX.S, EX.p1, EX.O3}) + end + + test "merge of a graph with the same name", + %{graph: graph, named_graph: named_graph} do + assert RDF.Data.merge(graph, Graph.add(graph, {EX.Other, EX.p1, EX.O3})) == + Graph.add(graph, {EX.Other, EX.p1, EX.O3}) + assert RDF.Data.merge(named_graph, Graph.add(named_graph, {EX.Other, EX.p1, EX.O3})) == + Graph.add(named_graph, {EX.Other, EX.p1, EX.O3}) + end + + test "merge of a graph with a different name", + %{graph: graph, named_graph: named_graph} do + assert RDF.Data.merge(graph, named_graph) == + Dataset.new(graph) |> Dataset.add(named_graph) + assert RDF.Data.merge(named_graph, graph) == + Dataset.new(named_graph) |> Dataset.add(graph) + end + + test "merge of a dataset", %{dataset: dataset} do + assert RDF.Data.merge(Graph.new({EX.Other, EX.p1, EX.O3}), dataset) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3}) + assert RDF.Data.merge(Graph.new(EX.NamedGraph, {EX.Other, EX.p1, EX.O3}), dataset) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3, EX.NamedGraph}) + end + + test "delete", %{graph: graph} do assert RDF.Data.delete(graph, {EX.S, EX.p1, EX.O2}) == Graph.delete(graph, {EX.S, EX.p1, EX.O2}) @@ -163,6 +256,38 @@ defmodule RDF.DataTest do describe "RDF.Data protocol implementation of RDF.Dataset" do + test "merge of a single triple", %{dataset: dataset} do + assert RDF.Data.merge(dataset, {EX.Other, EX.p, EX.O}) == + Dataset.add(dataset, {EX.Other, EX.p, EX.O}) + end + + test "merge of a single quad", %{dataset: dataset} do + assert RDF.Data.merge(dataset, {EX.Other, EX.p, EX.O, nil}) == + Dataset.add(dataset, {EX.Other, EX.p, EX.O}) + assert RDF.Data.merge(dataset, {EX.Other, EX.p, EX.O, EX.NamedGraph}) == + Dataset.add(dataset, {EX.Other, EX.p, EX.O, EX.NamedGraph}) + end + + test "merge of a description", %{dataset: dataset} do + assert RDF.Data.merge(dataset, Description.new({EX.Other, EX.p1, EX.O3})) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3}) + end + + test "merge of a graph", %{dataset: dataset} do + assert RDF.Data.merge(dataset, Graph.new({EX.Other, EX.p1, EX.O3})) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3}) + assert RDF.Data.merge(dataset, Graph.new(EX.NamedGraph, {EX.Other, EX.p1, EX.O3})) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3, EX.NamedGraph}) + end + + test "merge of a dataset", %{dataset: dataset} do + assert RDF.Data.merge(dataset, Dataset.new({EX.Other, EX.p1, EX.O3})) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3}) + assert RDF.Data.merge(dataset, Dataset.new(EX.NamedDataset, {EX.Other, EX.p1, EX.O3})) == + Dataset.add(dataset, {EX.Other, EX.p1, EX.O3}) + end + + test "delete", %{dataset: dataset} do assert RDF.Data.delete(dataset, {EX.S, EX.p1, EX.O2}) == Dataset.delete(dataset, {EX.S, EX.p1, EX.O2})