Add prefix management to RDF.Graph

This commit is contained in:
Marcel Otto 2019-03-31 01:15:56 +01:00
parent 8bffff7c76
commit 195b967b93
3 changed files with 214 additions and 27 deletions

View file

@ -10,6 +10,9 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
### Added ### Added
- `RDF.PrefixMap` - `RDF.PrefixMap`
- prefix management of `RDF.Graph`s:
- the structure now has `prefixes` field with an optional `RDF.PrefixMap`
- new functions `add_prefixes/2`, `delete_prefixes/2` and `clear_prefixes/1`
- configurable RDF.default_prefixes - configurable RDF.default_prefixes
@ -18,6 +21,11 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
- the constructor functions for `RDF.Graph`s and `RDF.Dataset`s now take the - the constructor functions for `RDF.Graph`s and `RDF.Dataset`s now take the
graph name resp. dataset name through a `name` option, instead of the first graph name resp. dataset name through a `name` option, instead of the first
argument argument
- `RDF.Graph.new` supports an additional `prefixes` argument to initialize the
`prefixes` field
- when `RDF.Graph.add` and `RDF.Graph.put` are called with another graph, its
prefixes are merged
[Compare v0.5.4...HEAD](https://github.com/marcelotto/rdf-ex/compare/v0.5.4...HEAD) [Compare v0.5.4...HEAD](https://github.com/marcelotto/rdf-ex/compare/v0.5.4...HEAD)

View file

@ -11,7 +11,7 @@ defmodule RDF.Graph do
""" """
defstruct name: nil, descriptions: %{} defstruct name: nil, descriptions: %{}, prefixes: nil
@behaviour Access @behaviour Access
@ -66,6 +66,8 @@ defmodule RDF.Graph do
Available options: Available options:
- `name`: the name of the graph to be created - `name`: the name of the graph to be created
- `prefixes`: some prefix mappings which should be stored alongside the graph
and will be used for example when serializing in a format with prefix support
## Examples ## Examples
@ -81,6 +83,7 @@ defmodule RDF.Graph do
def new(%RDF.Graph{} = graph, options) do def new(%RDF.Graph{} = graph, options) do
%RDF.Graph{graph | name: options |> Keyword.get(:name) |> coerce_graph_name()} %RDF.Graph{graph | name: options |> Keyword.get(:name) |> coerce_graph_name()}
|> add_prefixes(Keyword.get(options, :prefixes))
end end
def new(data, options) do def new(data, options) do
@ -107,11 +110,14 @@ defmodule RDF.Graph do
@doc """ @doc """
Adds triples to a `RDF.Graph`. Adds triples to a `RDF.Graph`.
Note: When the statements to be added are given as another `RDF.Graph`, When the statements to be added are given as another `RDF.Graph`,
the graph name must not match graph name of the graph to which the statements the graph name must not match graph name of the graph to which the statements
are added. As opposed to that `RDF.Data.merge/2` will produce a `RDF.Dataset` are added. As opposed to that `RDF.Data.merge/2` will produce a `RDF.Dataset`
containing both graphs. containing both graphs.
Also when the statements to be added are given as another `RDF.Graph`, the
prefixes of this graph will be added. In case of conflicting prefix mappings
the original prefix from `graph` will be kept.
""" """
def add(graph, triples) def add(graph, triples)
@ -130,15 +136,21 @@ defmodule RDF.Graph do
def add(%RDF.Graph{} = graph, %Description{subject: subject} = description), def add(%RDF.Graph{} = graph, %Description{subject: subject} = description),
do: do_add(graph, subject, description) do: do_add(graph, subject, description)
def add(graph, %RDF.Graph{descriptions: descriptions}) do def add(graph, %RDF.Graph{descriptions: descriptions, prefixes: prefixes}) do
Enum.reduce descriptions, graph, fn ({_, description}, graph) -> graph =
add(graph, description) Enum.reduce descriptions, graph, fn ({_, description}, graph) ->
add(graph, description)
end
if prefixes do
add_prefixes(graph, prefixes, fn _, ns, _ -> ns end)
else
graph
end end
end end
defp do_add(%RDF.Graph{name: name, descriptions: descriptions}, defp do_add(%RDF.Graph{descriptions: descriptions} = graph, subject, statements) do
subject, statements) do %RDF.Graph{graph |
%RDF.Graph{name: name,
descriptions: descriptions:
Map.update(descriptions, subject, Description.new(statements), Map.update(descriptions, subject, Description.new(statements),
fn description -> fn description ->
@ -151,6 +163,10 @@ defmodule RDF.Graph do
@doc """ @doc """
Adds statements to a `RDF.Graph` and overwrites all existing statements with the same subjects and predicates. Adds statements to a `RDF.Graph` and overwrites all existing statements with the same subjects and predicates.
When the statements to be added are given as another `RDF.Graph`, the prefixes
of this graph will be added. In case of conflicting prefix mappings the
original prefix from `graph` will be kept.
## Examples ## Examples
iex> RDF.Graph.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}]) |> iex> RDF.Graph.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}]) |>
@ -169,9 +185,16 @@ defmodule RDF.Graph do
def put(%RDF.Graph{} = graph, %Description{subject: subject} = description), def put(%RDF.Graph{} = graph, %Description{subject: subject} = description),
do: do_put(graph, subject, description) do: do_put(graph, subject, description)
def put(graph, %RDF.Graph{descriptions: descriptions}) do def put(graph, %RDF.Graph{descriptions: descriptions, prefixes: prefixes}) do
Enum.reduce descriptions, graph, fn ({_, description}, graph) -> graph =
put(graph, description) Enum.reduce descriptions, graph, fn ({_, description}, graph) ->
put(graph, description)
end
if prefixes do
add_prefixes(graph, prefixes, fn _, ns, _ -> ns end)
else
graph
end end
end end
@ -190,11 +213,11 @@ defmodule RDF.Graph do
""" """
def put(graph, subject, predications) def put(graph, subject, predications)
def put(%RDF.Graph{name: name, descriptions: descriptions}, subject, predications) def put(%RDF.Graph{descriptions: descriptions} = graph, subject, predications)
when is_list(predications) do when is_list(predications) do
with subject = coerce_subject(subject) do with subject = coerce_subject(subject) do
# TODO: Can we reduce this case also to do_put somehow? Only the initializer of Map.update differs ... # TODO: Can we reduce this case also to do_put somehow? Only the initializer of Map.update differs ...
%RDF.Graph{name: name, %RDF.Graph{graph |
descriptions: descriptions:
Map.update(descriptions, subject, Description.new(subject, predications), Map.update(descriptions, subject, Description.new(subject, predications),
fn current -> fn current ->
@ -207,9 +230,8 @@ defmodule RDF.Graph do
def put(graph, subject, {_predicate, _objects} = predications), def put(graph, subject, {_predicate, _objects} = predications),
do: put(graph, subject, [predications]) do: put(graph, subject, [predications])
defp do_put(%RDF.Graph{name: name, descriptions: descriptions}, defp do_put(%RDF.Graph{descriptions: descriptions} = graph, subject, statements) do
subject, statements) do %RDF.Graph{graph |
%RDF.Graph{name: name,
descriptions: descriptions:
Map.update(descriptions, subject, Description.new(statements), Map.update(descriptions, subject, Description.new(statements),
fn current -> fn current ->
@ -271,12 +293,12 @@ defmodule RDF.Graph do
end end
end end
defp do_delete(%RDF.Graph{name: name, descriptions: descriptions} = graph, defp do_delete(%RDF.Graph{descriptions: descriptions} = graph,
subject, statements) do subject, statements) do
with description when not is_nil(description) <- descriptions[subject], with description when not is_nil(description) <- descriptions[subject],
new_description = Description.delete(description, statements) new_description = Description.delete(description, statements)
do do
%RDF.Graph{name: name, %RDF.Graph{graph |
descriptions: descriptions:
if Enum.empty?(new_description) do if Enum.empty?(new_description) do
Map.delete(descriptions, subject) Map.delete(descriptions, subject)
@ -301,9 +323,9 @@ defmodule RDF.Graph do
end end
end end
def delete_subjects(%RDF.Graph{name: name, descriptions: descriptions}, subject) do def delete_subjects(%RDF.Graph{descriptions: descriptions} = graph, subject) do
with subject = coerce_subject(subject) do with subject = coerce_subject(subject) do
%RDF.Graph{name: name, descriptions: Map.delete(descriptions, subject)} %RDF.Graph{graph | descriptions: Map.delete(descriptions, subject)}
end end
end end
@ -409,7 +431,7 @@ defmodule RDF.Graph do
def pop(%RDF.Graph{descriptions: descriptions} = graph) def pop(%RDF.Graph{descriptions: descriptions} = graph)
when descriptions == %{}, do: {nil, graph} when descriptions == %{}, do: {nil, graph}
def pop(%RDF.Graph{name: name, descriptions: descriptions}) do def pop(%RDF.Graph{descriptions: descriptions} = graph) do
# TODO: Find a faster way ... # TODO: Find a faster way ...
[{subject, description}] = Enum.take(descriptions, 1) [{subject, description}] = Enum.take(descriptions, 1)
{triple, popped_description} = Description.pop(description) {triple, popped_description} = Description.pop(description)
@ -417,7 +439,7 @@ defmodule RDF.Graph do
do: descriptions |> Map.delete(subject), do: descriptions |> Map.delete(subject),
else: descriptions |> Map.put(subject, popped_description) else: descriptions |> Map.put(subject, popped_description)
{triple, %RDF.Graph{name: name, descriptions: popped}} {triple, %RDF.Graph{graph | descriptions: popped}}
end end
@doc """ @doc """
@ -435,12 +457,12 @@ defmodule RDF.Graph do
""" """
@impl Access @impl Access
def pop(%RDF.Graph{name: name, descriptions: descriptions} = graph, subject) do def pop(%RDF.Graph{descriptions: descriptions} = graph, subject) do
case Access.pop(descriptions, coerce_subject(subject)) do case Access.pop(descriptions, coerce_subject(subject)) do
{nil, _} -> {nil, _} ->
{nil, graph} {nil, graph}
{description, new_descriptions} -> {description, new_descriptions} ->
{description, %RDF.Graph{name: name, descriptions: new_descriptions}} {description, %RDF.Graph{graph | descriptions: new_descriptions}}
end end
end end
@ -655,9 +677,59 @@ defmodule RDF.Graph do
def values(graph, mapping \\ &RDF.Statement.default_term_mapping/1) def values(graph, mapping \\ &RDF.Statement.default_term_mapping/1)
def values(%RDF.Graph{descriptions: descriptions}, mapping) do def values(%RDF.Graph{descriptions: descriptions}, mapping) do
Map.new descriptions, fn {subject, description} -> Map.new descriptions, fn {subject, description} ->
{mapping.({:subject, subject}), Description.values(description, mapping)} {mapping.({:subject, subject}), Description.values(description, mapping)}
end end
end
@doc """
Adds `prefixes` to the given `graph`.
The `prefixes` mappings can be given as any structure convertible to a
`RDF.PrefixMap`.
When a prefix with another mapping already exists it will be overwritten with
the new one. This behaviour can be customized by providing a `conflict_resolver`
function. See `RDF.PrefixMap.merge/3` for more on that.
"""
def add_prefixes(graph, prefixes, conflict_resolver \\ nil)
def add_prefixes(%RDF.Graph{} = graph, nil, _), do: graph
def add_prefixes(%RDF.Graph{prefixes: nil} = graph, prefixes, _) do
%RDF.Graph{graph | prefixes: RDF.PrefixMap.new(prefixes)}
end
def add_prefixes(%RDF.Graph{} = graph, additions, nil) do
add_prefixes(%RDF.Graph{} = graph, additions, fn _, _, ns -> ns end)
end
def add_prefixes(%RDF.Graph{prefixes: prefixes} = graph, additions, conflict_resolver) do
%RDF.Graph{graph |
prefixes: RDF.PrefixMap.merge!(prefixes, additions, conflict_resolver)
}
end
@doc """
Deletes `prefixes` from the given `graph`.
The `prefixes` can be a single prefix or a list of prefixes.
Prefixes not in prefixes of the graph are simply ignored.
"""
def delete_prefixes(graph, prefixes)
def delete_prefixes(%RDF.Graph{prefixes: nil} = graph, _), do: graph
def delete_prefixes(%RDF.Graph{prefixes: prefixes} = graph, deletions) do
%RDF.Graph{graph | prefixes: RDF.PrefixMap.drop(prefixes, List.wrap(deletions))}
end
@doc """
Clears all prefixes of the given `graph`.
"""
def clear_prefixes(%RDF.Graph{} = graph) do
%RDF.Graph{graph | prefixes: nil}
end end

View file

@ -3,6 +3,8 @@ defmodule RDF.GraphTest do
doctest RDF.Graph doctest RDF.Graph
alias RDF.PrefixMap
alias RDF.NS.{XSD, RDFS}
describe "new" do describe "new" do
test "creating an empty unnamed graph" do test "creating an empty unnamed graph" do
@ -107,6 +109,25 @@ defmodule RDF.GraphTest do
assert unnamed_graph?(g) assert unnamed_graph?(g)
assert graph_includes_statement?(g, {EX.Subject, EX.predicate, EX.Object}) assert graph_includes_statement?(g, {EX.Subject, EX.predicate, EX.Object})
end end
test "with prefixes" do
assert Graph.new(prefixes: %{ex: EX}) ==
%Graph{prefixes: PrefixMap.new(ex: EX)}
assert Graph.new(prefixes: %{ex: EX}, name: EX.graph_name) ==
%Graph{prefixes: PrefixMap.new(ex: EX), name: EX.graph_name}
assert Graph.new({EX.Subject, EX.predicate, EX.Object}, prefixes: %{ex: EX}) ==
%Graph{Graph.new({EX.Subject, EX.predicate, EX.Object}) | prefixes: PrefixMap.new(ex: EX)}
end
test "creating a graph from another graph takes the prefixes from the other graph, but overwrites if necessary" do
prefix_map = PrefixMap.new(ex: EX)
g = Graph.new(Graph.new(prefixes: prefix_map))
assert g.prefixes == prefix_map
g = Graph.new(Graph.new(prefixes: %{ex: XSD, rdfs: RDFS}), prefixes: prefix_map)
assert g.prefixes == PrefixMap.new(ex: EX, rdfs: RDFS)
end
end end
describe "add" do describe "add" do
@ -194,6 +215,25 @@ defmodule RDF.GraphTest do
assert graph_includes_statement?(g, {EX.Subject3, EX.predicate3, EX.Object3}) assert graph_includes_statement?(g, {EX.Subject3, EX.predicate3, EX.Object3})
end end
test "merges the prefixes of another graph" do
graph = Graph.new(prefixes: %{xsd: XSD})
|> Graph.add(Graph.new(prefixes: %{rdfs: RDFS}))
assert graph.prefixes == PrefixMap.new(xsd: XSD, rdfs: RDFS)
end
test "merges the prefixes of another graph and keeps the original mapping in case of conflicts" do
graph = Graph.new(prefixes: %{ex: EX})
|> Graph.add(Graph.new(prefixes: %{ex: XSD}))
assert graph.prefixes == PrefixMap.new(ex: EX)
end
test "preserves the name and prefixes on when the data provided is not a graph" do
graph = Graph.new(name: EX.GraphName, prefixes: %{ex: EX})
|> Graph.add(EX.Subject, EX.predicate, EX.Object)
assert graph.name == RDF.iri(EX.GraphName)
assert graph.prefixes == PrefixMap.new(ex: EX)
end
test "non-coercible Triple elements are causing an error" do test "non-coercible Triple elements are causing an error" do
assert_raise RDF.IRI.InvalidError, fn -> assert_raise RDF.IRI.InvalidError, fn ->
Graph.add(graph(), {"not a IRI", EX.predicate, iri(EX.Object)}) Graph.add(graph(), {"not a IRI", EX.predicate, iri(EX.Object)})
@ -249,6 +289,25 @@ defmodule RDF.GraphTest do
assert graph_includes_statement?(g, {EX.S2, EX.P2, bnode(:foo)}) assert graph_includes_statement?(g, {EX.S2, EX.P2, bnode(:foo)})
assert graph_includes_statement?(g, {EX.S3, EX.P3, EX.O3}) assert graph_includes_statement?(g, {EX.S3, EX.P3, EX.O3})
end end
test "merges the prefixes of another graph" do
graph = Graph.new(prefixes: %{xsd: XSD})
|> Graph.put(Graph.new(prefixes: %{rdfs: RDFS}))
assert graph.prefixes == PrefixMap.new(xsd: XSD, rdfs: RDFS)
end
test "merges the prefixes of another graph and keeps the original mapping in case of conflicts" do
graph = Graph.new(prefixes: %{ex: EX})
|> Graph.put(Graph.new(prefixes: %{ex: XSD}))
assert graph.prefixes == PrefixMap.new(ex: EX)
end
test "preserves the name and prefixes" do
graph = Graph.new(name: EX.GraphName, prefixes: %{ex: EX})
|> Graph.put(EX.Subject, EX.predicate, EX.Object)
assert graph.name == RDF.iri(EX.GraphName)
assert graph.prefixes == PrefixMap.new(ex: EX)
end
end end
@ -322,6 +381,12 @@ defmodule RDF.GraphTest do
])) == Graph.new({EX.S3, EX.p3, ~L"bar"}) ])) == Graph.new({EX.S3, EX.p3, ~L"bar"})
end end
test "preserves the name and prefixes" do
graph = Graph.new(EX.Subject, EX.predicate, EX.Object, name: EX.GraphName, prefixes: %{ex: EX})
|> Graph.delete(EX.Subject, EX.predicate, EX.Object)
assert graph.name == RDF.iri(EX.GraphName)
assert graph.prefixes == PrefixMap.new(ex: EX)
end
end end
@ -397,6 +462,48 @@ defmodule RDF.GraphTest do
} }
end end
describe "add_prefixes/2" do
test "when prefixes already exist" do
graph = Graph.new(prefixes: %{xsd: XSD}) |> Graph.add_prefixes(ex: EX)
assert graph.prefixes == PrefixMap.new(xsd: XSD, ex: EX)
end
test "when prefixes are not defined yet" do
graph = Graph.new() |> Graph.add_prefixes(ex: EX)
assert graph.prefixes == PrefixMap.new(ex: EX)
end
test "when prefixes have conflicting mappings, the new mapping is used" do
graph = Graph.new(prefixes: %{ex: EX}) |> Graph.add_prefixes(ex: XSD)
assert graph.prefixes == PrefixMap.new(ex: XSD)
end
test "when prefixes have conflicting mappings and a conflict resolver function is provided" do
graph = Graph.new(prefixes: %{ex: EX}) |> Graph.add_prefixes([ex: XSD], fn _, ns, _ -> ns end)
assert graph.prefixes == PrefixMap.new(ex: EX)
end
end
describe "delete_prefixes/2" do
test "when given a single prefix" do
graph = Graph.new(prefixes: %{ex: EX}) |> Graph.delete_prefixes(:ex)
assert graph.prefixes == PrefixMap.new()
end
test "when given a list of prefixes" do
graph = Graph.new(prefixes: %{ex1: EX, ex2: EX}) |> Graph.delete_prefixes([:ex1, :ex2, :ex3])
assert graph.prefixes == PrefixMap.new()
end
test "when prefixes are not defined yet" do
graph = Graph.new() |> Graph.delete_prefixes(:ex)
assert graph.prefixes == nil
end
end
test "clear_prefixes/1" do
assert Graph.clear_prefixes(Graph.new(prefixes: %{ex: EX})) == Graph.new
end
describe "Enumerable protocol" do describe "Enumerable protocol" do
test "Enum.count" do test "Enum.count" do