Add a values/2 variant with a custom mapping function

This commit is contained in:
Marcel Otto 2018-11-04 22:27:25 +01:00
parent 535e5b3713
commit 4336602dcc
16 changed files with 391 additions and 30 deletions

View file

@ -16,6 +16,9 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
- `RDF.Description.values/1`, `RDF.Graph.values/1`, `RDF.Dataset.values/1` and
`RDF.Data.values/1` returning a map of `RDF.Term.value/1` converted native
Elixir values from the respective structure of RDF terms
- for all of aforementioned `values/1` functions a variant `values/2` which
allows to specify custom mapping function to be applied when creating the resp.
structure
- `RDF.Literal.compare/2`, `RDF.Literal.less_than?/2` and `RDF.Literal.greater_than?/2`
for `RDF.Datatype` aware comparisons of `RDF.Literal`s

View file

@ -689,6 +689,83 @@ iex> RDF.list(["foo", EX.Bar, ~B<bar>, [1, 2]]) |> RDF.List.values
%RDF.Literal{value: 2, datatype: ~I<http://www.w3.org/2001/XMLSchema#integer>}]]
```
### Mapping of RDF terms and structures
The `RDF.Term.value/1` function converts RDF terms to Elixir values:
```elixir
iex> RDF.Term.value(~I<http://example.com/>)
"http://example.com/"
iex> RDF.Term.value(~L"foo")
"foo"
iex> RDF.integer(42) |> RDF.Term.value()
42
```
It returns `nil` if no conversion is possible.
All structures of RDF terms also support a `values` function. On `RDF.Triple.values`, `RDF.Quad` and `RDF.Statement` the tuple of RDF terms is converted to an tuple of the resp. Elixir values. On all of the other RDF data structures (`RDF.Description`, `RDF.Graph` and `RDF.Dataset`) and the general `RDF.Data` protocol the `values` functions produces a map of the converted Elixir values.
```elixir
iex> RDF.Triple.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
{"http://example.com/S", "http://example.com/p", 42}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, ~L"Foo"}
...> |> RDF.Description.new()
...> |> RDF.Description.values()
%{"http://example.com/p" => ["Foo"]}
iex> [
...> {~I<http://example.com/S1>, ~I<http://example.com/p>, ~L"Foo"},
...> {~I<http://example.com/S2>, ~I<http://example.com/p>, RDF.integer(42)}
...> ]
...> |> RDF.Graph.new()
...> |> RDF.Graph.values()
%{
"http://example.com/S1" => %{"http://example.com/p" => ["Foo"]},
"http://example.com/S2" => %{"http://example.com/p" => [42]}
}
iex> [
...> {~I<http://example.com/S>, ~I<http://example.com/p>, ~L"Foo", ~I<http://example.com/Graph>},
...> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.integer(42), }
...> ]
...> |> RDF.Dataset.new()
...> |> RDF.Dataset.values()
%{
"http://example.com/Graph" => %{
"http://example.com/S" => %{"http://example.com/p" => ["Foo"]}
},
nil => %{
"http://example.com/S" => %{"http://example.com/p" => [42]}
}
}
```
All of these `values` functions also support an optional second argument for a function with a custom mapping of the terms depending on their statement position. The function will be called with a tuple `{statement_position, rdf_term}` where `statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or `:graph_name`, while `rdf_term` is the RDF term to be mapped.
```elixir
iex> [
...> {~I<http://example.com/S1>, ~I<http://example.com/p>, ~L"Foo"},
...> {~I<http://example.com/S2>, ~I<http://example.com/p>, RDF.integer(42)}
...> ]
...> |> RDF.Graph.new()
...> |> RDF.Graph.values(fn
...> {:predicate, predicate} ->
...> predicate
...> |> to_string()
...> |> String.split("/")
...> |> List.last()
...> |> String.to_atom()
...> {_, term} ->
...> RDF.Term.value(term)
...> end)
%{
"http://example.com/S1" => %{p: ["Foo"]},
"http://example.com/S2" => %{p: [42]}
}
```
### Serializations
@ -739,7 +816,7 @@ The `Date` and `DateTime` modules of Elixir versions < 1.7.2 don't handle negati
## Getting help
- [Documentation](http://hexdocs.pm/rdf)
- [A tutorial about working with RDF.ex vocabularies](https://medium.com/@tonyhammond/early-steps-in-elixir-and-rdf-5078a4ebfe0f)
- [A tutorial about working with RDF.ex vocabularies by Tony Hammond](https://medium.com/@tonyhammond/early-steps-in-elixir-and-rdf-5078a4ebfe0f)
- [Google Group](https://groups.google.com/d/forum/rdfex)
@ -772,7 +849,7 @@ see [CONTRIBUTING](CONTRIBUTING.md) for details.
[RDF.ex]: https://hex.pm/packages/rdf
[rdf_vocab]: https://hex.pm/packages/rdf_vocab
[JSON-LD.ex]: https://hex.pm/packages/json_ld
[SPARQL.ex]: https://hex.pm/packages/sparql
[SPARQL.ex]: https://hex.pm/packages/sparql
[SPARQL.Client]: https://hex.pm/packages/sparql_client
[N-Triples]: https://www.w3.org/TR/n-triples/
[N-Quads]: https://www.w3.org/TR/n-quads/

View file

@ -97,6 +97,10 @@ defprotocol RDF.Data do
"""
def values(data)
@doc """
Returns a nested map of the native Elixir values of a RDF data structure with values mapped with the given function.
"""
def values(data, mapping)
end
defimpl RDF.Data, for: RDF.Description do
@ -164,6 +168,7 @@ defimpl RDF.Data, for: RDF.Description do
def subject_count(_), do: 1
def statement_count(description), do: RDF.Description.count(description)
def values(description), do: RDF.Description.values(description)
def values(description, mapping), do: RDF.Description.values(description, mapping)
end
@ -225,6 +230,7 @@ defimpl RDF.Data, for: RDF.Graph do
def subject_count(graph), do: RDF.Graph.subject_count(graph)
def statement_count(graph), do: RDF.Graph.triple_count(graph)
def values(graph), do: RDF.Graph.values(graph)
def values(graph, mapping), do: RDF.Graph.values(graph, mapping)
end
@ -279,4 +285,5 @@ defimpl RDF.Data, for: RDF.Dataset do
def subject_count(dataset), do: dataset |> subjects |> Enum.count
def statement_count(dataset), do: RDF.Dataset.statement_count(dataset)
def values(dataset), do: RDF.Dataset.values(dataset)
def values(dataset, mapping), do: RDF.Dataset.values(dataset, mapping)
end

View file

@ -731,6 +731,11 @@ defmodule RDF.Dataset do
@doc """
Returns a nested map of the native Elixir values of a `RDF.Dataset`.
The optional second argument allows to specify a custom mapping with a function
which will receive a tuple `{statement_position, rdf_term}` where
`statement_position` is one of the atoms `:subject`, `:predicate`, `:object`,
or `graph_name` while `rdf_term` is the RDF term to be mapped.
## Examples
iex> [
@ -748,10 +753,38 @@ defmodule RDF.Dataset do
}
}
iex> [
...> {~I<http://example.com/S>, ~I<http://example.com/p>, ~L"Foo", ~I<http://example.com/Graph>},
...> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.integer(42), }
...> ]
...> |> RDF.Dataset.new()
...> |> RDF.Dataset.values(fn
...> {:graph_name, graph_name} ->
...> graph_name
...> {:predicate, predicate} ->
...> predicate
...> |> to_string()
...> |> String.split("/")
...> |> List.last()
...> |> String.to_atom()
...> {_, term} ->
...> RDF.Term.value(term)
...> end)
%{
~I<http://example.com/Graph> => %{
"http://example.com/S" => %{p: ["Foo"]}
},
nil => %{
"http://example.com/S" => %{p: [42]}
}
}
"""
def values(%RDF.Dataset{graphs: graphs}) do
def values(dataset, mapping \\ &RDF.Statement.default_term_mapping/1)
def values(%RDF.Dataset{graphs: graphs}, mapping) do
Map.new graphs, fn {graph_name, graph} ->
{RDF.Term.value(graph_name), Graph.values(graph)}
{mapping.({:graph_name, graph_name}), Graph.values(graph, mapping)}
end
end

View file

@ -578,6 +578,11 @@ defmodule RDF.Description do
The subject is not part of the result. It can be converted separately with
`RDF.Term.value/1`.
The optional second argument allows to specify a custom mapping with a function
which will receive a tuple `{statement_position, rdf_term}` where
`statement_position` is one of the atoms `:predicate` or `:object`,
while `rdf_term` is the RDF term to be mapped.
## Examples
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, ~L"Foo"}
@ -585,16 +590,33 @@ defmodule RDF.Description do
...> |> RDF.Description.values()
%{"http://example.com/p" => ["Foo"]}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, ~L"Foo"}
...> |> RDF.Description.new()
...> |> RDF.Description.values(fn
...> {:predicate, predicate} ->
...> predicate
...> |> to_string()
...> |> String.split("/")
...> |> List.last()
...> |> String.to_atom()
...> {_, term} ->
...> RDF.Term.value(term)
...> end)
%{p: ["Foo"]}
"""
def values(%RDF.Description{subject: subject, predications: predications}) do
def values(description, mapping \\ &RDF.Statement.default_term_mapping/1)
def values(%RDF.Description{predications: predications}, mapping) do
Map.new predications, fn {predicate, objects} ->
{
RDF.Term.value(predicate),
objects |> Map.keys() |> Enum.map(&RDF.Term.value/1)
mapping.({:predicate, predicate}),
objects |> Map.keys() |> Enum.map(&(mapping.({:object, &1})))
}
end
end
defimpl Enumerable do
def member?(desc, triple), do: {:ok, RDF.Description.include?(desc, triple)}
def count(desc), do: {:ok, RDF.Description.count(desc)}

View file

@ -605,6 +605,11 @@ defmodule RDF.Graph do
@doc """
Returns a nested map of the native Elixir values of a `RDF.Graph`.
The optional second argument allows to specify a custom mapping with a function
which will receive a tuple `{statement_position, rdf_term}` where
`statement_position` is one of the atoms `:subject`, `:predicate` or `:object`,
while `rdf_term` is the RDF term to be mapped.
## Examples
iex> [
@ -618,10 +623,32 @@ defmodule RDF.Graph do
"http://example.com/S2" => %{"http://example.com/p" => [42]}
}
iex> [
...> {~I<http://example.com/S1>, ~I<http://example.com/p>, ~L"Foo"},
...> {~I<http://example.com/S2>, ~I<http://example.com/p>, RDF.integer(42)}
...> ]
...> |> RDF.Graph.new()
...> |> RDF.Graph.values(fn
...> {:predicate, predicate} ->
...> predicate
...> |> to_string()
...> |> String.split("/")
...> |> List.last()
...> |> String.to_atom()
...> {_, term} ->
...> RDF.Term.value(term)
...> end)
%{
"http://example.com/S1" => %{p: ["Foo"]},
"http://example.com/S2" => %{p: [42]}
}
"""
def values(%RDF.Graph{descriptions: descriptions}) do
def values(graph, mapping \\ &RDF.Statement.default_term_mapping/1)
def values(%RDF.Graph{descriptions: descriptions}, mapping) do
Map.new descriptions, fn {subject, description} ->
{RDF.Term.value(subject), Description.values(description)}
{mapping.({:subject, subject}), Description.values(description, mapping)}
end
end

View file

@ -54,23 +54,44 @@ defmodule RDF.Quad do
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
The optional second argument allows to specify a custom mapping with a function
which will receive a tuple `{statement_position, rdf_term}` where
`statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or
`:graph_name`, while `rdf_term` is the RDF term to be mapped. When the given
function returns `nil` this will be interpreted as an error and will become
the overhaul result of the `values/2` call.
## Examples
iex> RDF.Quad.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
{"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
...> |> RDF.Quad.values(fn
...> {:object, object} ->
...> RDF.Term.value(object)
...> {:graph_name, graph_name} ->
...> graph_name
...> {_, resource} ->
...> resource |> to_string() |> String.last() |> String.to_atom()
...> end)
{:S, :p, 42, ~I<http://example.com/Graph>}
"""
def values({subject, predicate, object, graph_context}) do
with subject_value when not is_nil(subject_value) <- Term.value(subject),
predicate_value when not is_nil(predicate_value) <- Term.value(predicate),
object_value when not is_nil(object_value) <- Term.value(object),
graph_context_value when not is_nil(graph_context_value) or is_nil(graph_context) <-
Term.value(graph_context)
def values(quad, mapping \\ &Statement.default_term_mapping/1)
def values({subject, predicate, object, graph_context}, mapping) do
with subject_value when not is_nil(subject_value) <- mapping.({:subject, subject}),
predicate_value when not is_nil(predicate_value) <- mapping.({:predicate, predicate}),
object_value when not is_nil(object_value) <- mapping.({:object, object}),
graph_context_value <- mapping.({:graph_name, graph_context})
do
{subject_value, predicate_value, object_value, graph_context_value}
else
_ -> nil
end
end
def values(_), do: nil
def values(_, _), do: nil
end

View file

@ -25,9 +25,9 @@ defmodule RDF.Statement do
## Examples
iex> RDF.Statement.new {"http://example.com/S", "http://example.com/p", 42}
iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42}
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
iex> RDF.Statement.new {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
"""
def coerce(statement)
@ -77,6 +77,13 @@ defmodule RDF.Statement do
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
The optional second argument allows to specify a custom mapping with a function
which will receive a tuple `{statement_position, rdf_term}` where
`statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or
`:graph_name`, while `rdf_term` is the RDF term to be mapped. When the given
function returns `nil` this will be interpreted as an error and will become
the overhaul result of the `values/2` call.
## Examples
iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
@ -84,9 +91,28 @@ defmodule RDF.Statement do
iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
{"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
...> |> RDF.Statement.values(fn
...> {:subject, subject} ->
...> subject |> to_string() |> String.last()
...> {:predicate, predicate} ->
...> predicate |> to_string() |> String.last() |> String.to_atom()
...> {:object, object} ->
...> RDF.Term.value(object)
...> {:graph_name, graph_name} ->
...> graph_name
...> end)
{"S", :p, 42, ~I<http://example.com/Graph>}
"""
def values({_, _, _} = triple), do: RDF.Triple.values(triple)
def values({_, _, _, _} = quad), do: RDF.Quad.values(quad)
def values(_), do: nil
def values(statement, mapping \\ &default_term_mapping/1)
def values({_, _, _} = triple, mapping), do: RDF.Triple.values(triple, mapping)
def values({_, _, _, _} = quad, mapping), do: RDF.Quad.values(quad, mapping)
def values(_, _), do: nil
@doc false
def default_term_mapping(qualified_term)
def default_term_mapping({:graph_name, nil}), do: nil
def default_term_mapping({_, term}), do: RDF.Term.value(term)
end

View file

@ -52,21 +52,39 @@ defmodule RDF.Triple do
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
The optional second argument allows to specify a custom mapping with a function
which will receive a tuple `{statement_position, rdf_term}` where
`statement_position` is one of the atoms `:subject`, `:predicate` or `:object`,
while `rdf_term` is the RDF term to be mapped. When the given function returns
`nil` this will be interpreted as an error and will become the overhaul result
of the `values/2` call.
## Examples
iex> RDF.Triple.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
{"http://example.com/S", "http://example.com/p", 42}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
...> |> RDF.Triple.values(fn
...> {:object, object} -> RDF.Term.value(object)
...> {_, term} -> term |> to_string() |> String.last()
...> end)
{"S", "p", 42}
"""
def values({subject, predicate, object}) do
with subject_value when not is_nil(subject_value) <- Term.value(subject),
predicate_value when not is_nil(predicate_value) <- Term.value(predicate),
object_value when not is_nil(object_value) <- Term.value(object)
def values(triple, mapping \\ &Statement.default_term_mapping/1)
def values({subject, predicate, object}, mapping) do
with subject_value when not is_nil(subject_value) <- mapping.({:subject, subject}),
predicate_value when not is_nil(predicate_value) <- mapping.({:predicate, predicate}),
object_value when not is_nil(object_value) <- mapping.({:object, object})
do
{subject_value, predicate_value, object_value}
else
_ -> nil
end
end
def values(_), do: nil
def values(_, _), do: nil
end

View file

@ -423,7 +423,7 @@ defmodule RDF.DataTest do
assert RDF.Data.statement_count(dataset) == 14
end
test "values", %{dataset: dataset} do
test "values/1", %{dataset: dataset} do
assert RDF.Data.values(dataset) ==
%{
nil => %{
@ -459,6 +459,52 @@ defmodule RDF.DataTest do
}
}
end
test "values/2", %{dataset: dataset} do
mapping = fn
{:graph_name, graph_name} ->
graph_name
{:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
{_, term} ->
RDF.Term.value(term)
end
assert RDF.Data.values(dataset, mapping) ==
%{
nil => %{
RDF.Term.value(RDF.iri(EX.S)) => %{
p1: [
RDF.Term.value(RDF.iri(EX.O1)),
RDF.Term.value(RDF.iri(EX.O2))
],
p2: [RDF.Term.value(RDF.iri(EX.O3))],
p3: ["_:foo", "bar"],
},
RDF.Term.value(RDF.iri(EX.S2)) => %{
p2: [
RDF.Term.value(RDF.iri(EX.O3)),
RDF.Term.value(RDF.iri(EX.O4))
],
},
},
RDF.iri(EX.NamedGraph) => %{
RDF.Term.value(RDF.iri(EX.S)) => %{
p1: [
RDF.Term.value(RDF.iri(EX.O1)),
RDF.Term.value(RDF.iri(EX.O2))
],
p2: [RDF.Term.value(RDF.iri(EX.O3))],
p3: ["_:foo", RDF.Term.value(RDF.iri(EX.O5)), "bar"],
},
RDF.Term.value(RDF.iri(EX.S3)) => %{
p3: [
RDF.Term.value(RDF.iri(EX.O5))
],
},
}
}
end
end
end

View file

@ -715,6 +715,29 @@ defmodule RDF.DatasetTest do
}
end
test "values/2" do
mapping = fn
{:graph_name, graph_name} ->
graph_name
{:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
{_, term} ->
RDF.Term.value(term)
end
assert Dataset.new() |> Dataset.values(mapping) == %{}
assert Dataset.new([{EX.s1, EX.p, EX.o1}, {EX.s2, EX.p, EX.o2, EX.graph}])
|> Dataset.values(mapping) ==
%{
nil => %{
RDF.Term.value(EX.s1) => %{p: [RDF.Term.value(EX.o1)]}
},
EX.graph => %{
RDF.Term.value(EX.s2) => %{p: [RDF.Term.value(EX.o2)]},
}
}
end
describe "Enumerable protocol" do
test "Enum.count" do

View file

@ -340,9 +340,22 @@ defmodule RDF.DescriptionTest do
end
test "values/1" do
assert Description.new(EX.s) |> Description.values() == %{}
assert Description.new({EX.s, EX.p, ~L"Foo"}) |> Description.values() ==
%{RDF.Term.value(EX.p) => ["Foo"]}
assert Description.new(EX.s) |> Description.values() == %{}
assert Description.new({EX.s, EX.p, ~L"Foo"}) |> Description.values() ==
%{RDF.Term.value(EX.p) => ["Foo"]}
end
test "values/2" do
mapping = fn
{:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
{_, term} ->
RDF.Term.value(term)
end
assert Description.new(EX.s) |> Description.values(mapping) == %{}
assert Description.new({EX.s, EX.p, ~L"Foo"}) |> Description.values(mapping) ==
%{p: ["Foo"]}
end
describe "Enumerable protocol" do

View file

@ -374,6 +374,23 @@ defmodule RDF.GraphTest do
}
end
test "values/2" do
mapping = fn
{:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
{_, term} ->
RDF.Term.value(term)
end
assert Graph.new() |> Graph.values(mapping) == %{}
assert Graph.new([{EX.s1, EX.p, EX.o1}, {EX.s2, EX.p, EX.o2}])
|> Graph.values(mapping) ==
%{
RDF.Term.value(EX.s1) => %{p: [RDF.Term.value(EX.o1)]},
RDF.Term.value(EX.s2) => %{p: [RDF.Term.value(EX.o2)]},
}
end
describe "Enumerable protocol" do
test "Enum.count" do

View file

@ -18,4 +18,16 @@ defmodule RDF.QuadTest do
refute Quad.values({self(), self(), self(), self()})
end
end
test "values/2" do
assert {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.integer(42), ~I<http://example.com/Graph>}
|> Quad.values(fn
{:subject, subject} -> subject |> to_string() |> String.last() |> String.to_atom()
{:predicate, _} -> :p
{:object, object} -> object |> RDF.Term.value() |> Kernel.+(1)
{:graph_name, _} -> nil
end)
== {:S, :p, 43, nil}
end
end

View file

@ -0,0 +1,6 @@
defmodule RDF.StatementTest do
use RDF.Test.Case
doctest RDF.Statement
end

View file

@ -16,4 +16,14 @@ defmodule RDF.TripleTest do
refute Triple.values({self(), self(), self()})
end
end
test "values/2" do
assert {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.integer(42)}
|> Triple.values(fn
{:object, object} -> object |> RDF.Term.value() |> Kernel.+(1)
{_, term} -> term |> to_string() |> String.last()
end)
== {"S", "p", 43}
end
end