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
- `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
@ -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
graph name resp. dataset name through a `name` option, instead of the first
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)

View file

@ -11,7 +11,7 @@ defmodule RDF.Graph do
"""
defstruct name: nil, descriptions: %{}
defstruct name: nil, descriptions: %{}, prefixes: nil
@behaviour Access
@ -66,6 +66,8 @@ defmodule RDF.Graph do
Available options:
- `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
@ -81,6 +83,7 @@ defmodule RDF.Graph do
def new(%RDF.Graph{} = graph, options) do
%RDF.Graph{graph | name: options |> Keyword.get(:name) |> coerce_graph_name()}
|> add_prefixes(Keyword.get(options, :prefixes))
end
def new(data, options) do
@ -107,11 +110,14 @@ defmodule RDF.Graph do
@doc """
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
are added. As opposed to that `RDF.Data.merge/2` will produce a `RDF.Dataset`
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)
@ -130,15 +136,21 @@ defmodule RDF.Graph do
def add(%RDF.Graph{} = graph, %Description{subject: 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
graph =
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
defp do_add(%RDF.Graph{name: name, descriptions: descriptions},
subject, statements) do
%RDF.Graph{name: name,
defp do_add(%RDF.Graph{descriptions: descriptions} = graph, subject, statements) do
%RDF.Graph{graph |
descriptions:
Map.update(descriptions, subject, Description.new(statements),
fn description ->
@ -151,6 +163,10 @@ defmodule RDF.Graph do
@doc """
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
iex> RDF.Graph.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}]) |>
@ -169,10 +185,17 @@ defmodule RDF.Graph do
def put(%RDF.Graph{} = graph, %Description{subject: 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
graph =
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
def put(%RDF.Graph{} = graph, statements) when is_map(statements) do
@ -190,11 +213,11 @@ defmodule RDF.Graph do
"""
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
with subject = coerce_subject(subject) do
# 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:
Map.update(descriptions, subject, Description.new(subject, predications),
fn current ->
@ -207,9 +230,8 @@ defmodule RDF.Graph do
def put(graph, subject, {_predicate, _objects} = predications),
do: put(graph, subject, [predications])
defp do_put(%RDF.Graph{name: name, descriptions: descriptions},
subject, statements) do
%RDF.Graph{name: name,
defp do_put(%RDF.Graph{descriptions: descriptions} = graph, subject, statements) do
%RDF.Graph{graph |
descriptions:
Map.update(descriptions, subject, Description.new(statements),
fn current ->
@ -271,12 +293,12 @@ defmodule RDF.Graph do
end
end
defp do_delete(%RDF.Graph{name: name, descriptions: descriptions} = graph,
defp do_delete(%RDF.Graph{descriptions: descriptions} = graph,
subject, statements) do
with description when not is_nil(description) <- descriptions[subject],
new_description = Description.delete(description, statements)
do
%RDF.Graph{name: name,
%RDF.Graph{graph |
descriptions:
if Enum.empty?(new_description) do
Map.delete(descriptions, subject)
@ -301,9 +323,9 @@ defmodule RDF.Graph do
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
%RDF.Graph{name: name, descriptions: Map.delete(descriptions, subject)}
%RDF.Graph{graph | descriptions: Map.delete(descriptions, subject)}
end
end
@ -409,7 +431,7 @@ defmodule RDF.Graph do
def pop(%RDF.Graph{descriptions: descriptions} = 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 ...
[{subject, description}] = Enum.take(descriptions, 1)
{triple, popped_description} = Description.pop(description)
@ -417,7 +439,7 @@ defmodule RDF.Graph do
do: descriptions |> Map.delete(subject),
else: descriptions |> Map.put(subject, popped_description)
{triple, %RDF.Graph{name: name, descriptions: popped}}
{triple, %RDF.Graph{graph | descriptions: popped}}
end
@doc """
@ -435,12 +457,12 @@ defmodule RDF.Graph do
"""
@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
{nil, _} ->
{nil, graph}
{description, new_descriptions} ->
{description, %RDF.Graph{name: name, descriptions: new_descriptions}}
{description, %RDF.Graph{graph | descriptions: new_descriptions}}
end
end
@ -661,6 +683,56 @@ defmodule RDF.Graph do
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
defimpl Enumerable do
def member?(graph, triple), do: {:ok, RDF.Graph.include?(graph, triple)}
def count(graph), do: {:ok, RDF.Graph.triple_count(graph)}

View file

@ -3,6 +3,8 @@ defmodule RDF.GraphTest do
doctest RDF.Graph
alias RDF.PrefixMap
alias RDF.NS.{XSD, RDFS}
describe "new" do
test "creating an empty unnamed graph" do
@ -107,6 +109,25 @@ defmodule RDF.GraphTest do
assert unnamed_graph?(g)
assert graph_includes_statement?(g, {EX.Subject, EX.predicate, EX.Object})
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
describe "add" do
@ -194,6 +215,25 @@ defmodule RDF.GraphTest do
assert graph_includes_statement?(g, {EX.Subject3, EX.predicate3, EX.Object3})
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
assert_raise RDF.IRI.InvalidError, fn ->
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.S3, EX.P3, EX.O3})
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
@ -322,6 +381,12 @@ defmodule RDF.GraphTest do
])) == Graph.new({EX.S3, EX.p3, ~L"bar"})
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
@ -397,6 +462,48 @@ defmodule RDF.GraphTest do
}
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
test "Enum.count" do