Extract map/2 function from values/2 on all RDF data structures

and add support for RDF.PropertyMaps on values/2 instead
This commit is contained in:
Marcel Otto 2020-10-10 15:45:25 +02:00
parent 2fbf57a172
commit a49229384e
14 changed files with 350 additions and 109 deletions

View file

@ -59,6 +59,11 @@ are specified.
of overwriting only statements with the same subject and predicate, which was of overwriting only statements with the same subject and predicate, which was
almost never the expected behaviour. This is fixed now by relying on the new almost never the expected behaviour. This is fixed now by relying on the new
`put/2` behaviour. `put/2` behaviour.
- the `values/2` functions of `RDF.Statement`, `RDF.Triple`, `RDF.Quad`, `RDF.Description`,
`RDF.Graph` and `RDF.Dataset` now accept on their second argument an optional
`RDF.PropertyMap`which will be used to map predicates accordingly; the variant of
these `values/2` functions to provide a custom mapping function was extracted into
a new function `map/2` on all of these modules
- for consistency reasons the internal `:id` struct field of `RDF.BlankNode` was renamed - for consistency reasons the internal `:id` struct field of `RDF.BlankNode` was renamed
to `:value` to `:value`
- allow the `base_iri` of `RDF.Vocabulary.Namespace`s to end with a `.` to support - allow the `base_iri` of `RDF.Vocabulary.Namespace`s to end with a `.` to support

View file

@ -94,13 +94,16 @@ defprotocol RDF.Data do
@doc """ @doc """
Returns a nested map of the native Elixir values of a RDF data structure. Returns a nested map of the native Elixir values of a RDF data structure.
When the optional `property_map` argument is given, predicates will be mapped
to the terms defined in the `RDF.PropertyMap` if present.
""" """
def values(data) def values(data, property_map \\ nil)
@doc """ @doc """
Returns a nested map of the native Elixir values of a RDF data structure with values mapped with the given function. Returns a map representation of a RDF data structure where each element from its statements is mapped with the given function.
""" """
def values(data, mapping) def map(data, fun)
@doc """ @doc """
Checks if two RDF data structures are equal. Checks if two RDF data structures are equal.
@ -191,8 +194,11 @@ defimpl RDF.Data, for: RDF.Description do
def subject_count(_), do: 1 def subject_count(_), do: 1
def statement_count(description), do: RDF.Description.count(description) 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) def values(description, property_map \\ nil),
do: RDF.Description.values(description, property_map)
def map(description, fun), do: RDF.Description.map(description, fun)
def equal?(description, %RDF.Description{} = other_description) do def equal?(description, %RDF.Description{} = other_description) do
RDF.Description.equal?(description, other_description) RDF.Description.equal?(description, other_description)
@ -273,8 +279,8 @@ defimpl RDF.Data, for: RDF.Graph do
def subject_count(graph), do: RDF.Graph.subject_count(graph) def subject_count(graph), do: RDF.Graph.subject_count(graph)
def statement_count(graph), do: RDF.Graph.triple_count(graph) def statement_count(graph), do: RDF.Graph.triple_count(graph)
def values(graph), do: RDF.Graph.values(graph) def values(graph, property_map \\ nil), do: RDF.Graph.values(graph, property_map)
def values(graph, mapping), do: RDF.Graph.values(graph, mapping) def map(graph, fun), do: RDF.Graph.map(graph, fun)
def equal?(graph, %RDF.Description{} = description), def equal?(graph, %RDF.Description{} = description),
do: RDF.Data.equal?(description, graph) do: RDF.Data.equal?(description, graph)
@ -348,8 +354,8 @@ defimpl RDF.Data, for: RDF.Dataset do
def subject_count(dataset), do: dataset |> subjects |> Enum.count() def subject_count(dataset), do: dataset |> subjects |> Enum.count()
def statement_count(dataset), do: RDF.Dataset.statement_count(dataset) def statement_count(dataset), do: RDF.Dataset.statement_count(dataset)
def values(dataset), do: RDF.Dataset.values(dataset) def values(dataset, property_map \\ nil), do: RDF.Dataset.values(dataset, property_map)
def values(dataset, mapping), do: RDF.Dataset.values(dataset, mapping) def map(dataset, fun), do: RDF.Dataset.map(dataset, fun)
def equal?(dataset, %RDF.Description{} = description) do def equal?(dataset, %RDF.Description{} = description) do
with [graph] <- RDF.Dataset.graphs(dataset) do with [graph] <- RDF.Dataset.graphs(dataset) do

View file

@ -17,8 +17,8 @@ defmodule RDF.Dataset do
@behaviour Access @behaviour Access
alias RDF.{Graph, Description, IRI, Statement} alias RDF.{Graph, Description, IRI, Statement, PropertyMap}
import RDF.Statement import RDF.Statement, only: [coerce_subject: 1, coerce_graph_name: 1]
import RDF.Utils import RDF.Utils
@type graph_name :: IRI.t() | nil @type graph_name :: IRI.t() | nil
@ -761,10 +761,8 @@ defmodule RDF.Dataset do
@doc """ @doc """
Returns a nested map of the native Elixir values of a `RDF.Dataset`. 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 When the optional `property_map` argument is given, predicates will be mapped
which will receive a tuple `{statement_position, rdf_term}` where to the terms defined in the `RDF.PropertyMap` if present.
`statement_position` is one of the atoms `:subject`, `:predicate`, `:object`,
or `graph_name` while `rdf_term` is the RDF term to be mapped.
## Examples ## Examples
@ -783,12 +781,35 @@ defmodule RDF.Dataset do
} }
} }
"""
@spec values(t, PropertyMap.t() | nil) :: map
def values(dataset, property_map \\ nil)
def values(%__MODULE__{} = dataset, nil) do
map(dataset, &Statement.default_term_mapping/1)
end
def values(%__MODULE__{} = dataset, %PropertyMap{} = property_map) do
map(dataset, Statement.default_property_mapping(property_map))
end
@doc """
Returns a nested map of a `RDF.Dataset` where each element from its quads is mapped with the given function.
The function `fun` 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 `map/2` call.
## Examples
iex> [ 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>, ~L"Foo", ~I<http://example.com/Graph>},
...> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.XSD.integer(42), } ...> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.XSD.integer(42), }
...> ] ...> ]
...> |> RDF.Dataset.new() ...> |> RDF.Dataset.new()
...> |> RDF.Dataset.values(fn ...> |> RDF.Dataset.map(fn
...> {:graph_name, graph_name} -> ...> {:graph_name, graph_name} ->
...> graph_name ...> graph_name
...> {:predicate, predicate} -> ...> {:predicate, predicate} ->
@ -810,12 +831,12 @@ defmodule RDF.Dataset do
} }
""" """
@spec values(t, Statement.term_mapping()) :: map @spec map(t, Statement.term_mapping()) :: map
def values(dataset, mapping \\ &Statement.default_term_mapping/1) def map(dataset, fun)
def values(%__MODULE__{} = dataset, mapping) do def map(%__MODULE__{} = dataset, fun) do
Map.new(dataset.graphs, fn {graph_name, graph} -> Map.new(dataset.graphs, fn {graph_name, graph} ->
{mapping.({:graph_name, graph_name}), Graph.values(graph, mapping)} {fun.({:graph_name, graph_name}), Graph.map(graph, fun)}
end) end)
end end

View file

@ -15,7 +15,9 @@ defmodule RDF.Description do
@behaviour Access @behaviour Access
import RDF.Statement import RDF.Statement,
only: [coerce_subject: 1, coerce_predicate: 1, coerce_predicate: 2, coerce_object: 1]
alias RDF.{Statement, Triple, PropertyMap} alias RDF.{Statement, Triple, PropertyMap}
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -719,12 +721,11 @@ defmodule RDF.Description do
Returns a map of the native Elixir values of a `RDF.Description`. Returns a map of the native Elixir values of a `RDF.Description`.
The subject is not part of the result. It can be converted separately with The subject is not part of the result. It can be converted separately with
`RDF.Term.value/1`. `RDF.Term.value/1`, or, if you want the subject in an outer map, just put the
the description in a graph and use `RDF.Graph.values/2`.
The optional second argument allows to specify a custom mapping with a function When the optional `property_map` argument is given, predicates will be mapped
which will receive a tuple `{statement_position, rdf_term}` where to the terms defined in the `RDF.PropertyMap` if present.
`statement_position` is one of the atoms `:predicate` or `:object`,
while `rdf_term` is the RDF term to be mapped.
## Examples ## Examples
@ -733,7 +734,37 @@ defmodule RDF.Description do
%{"http://example.com/p" => ["Foo"]} %{"http://example.com/p" => ["Foo"]}
iex> RDF.Description.new(~I<http://example.com/S>, init: {~I<http://example.com/p>, ~L"Foo"}) iex> RDF.Description.new(~I<http://example.com/S>, init: {~I<http://example.com/p>, ~L"Foo"})
...> |> RDF.Description.values(fn ...> |> RDF.Description.values(PropertyMap.new(p: ~I<http://example.com/p>))
%{p: ["Foo"]}
"""
@spec values(t, PropertyMap.t() | nil) :: map
def values(description, property_map \\ nil)
def values(%__MODULE__{} = description, nil) do
map(description, &Statement.default_term_mapping/1)
end
def values(%__MODULE__{} = description, %PropertyMap{} = property_map) do
map(description, Statement.default_property_mapping(property_map))
end
@doc """
Returns a map of a `RDF.Description` where each element from its triples is mapped with the given function.
The subject is not part of the result. If you want the subject in an outer map,
just put the the description in a graph and use `RDF.Graph.map/2`.
The function `fun` 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. When the given function returns
`nil` this will be interpreted as an error and will become the overhaul result
of the `map/2` call.
## Examples
iex> RDF.Description.new(~I<http://example.com/S>, init: {~I<http://example.com/p>, ~L"Foo"})
...> |> RDF.Description.map(fn
...> {:predicate, predicate} -> ...> {:predicate, predicate} ->
...> predicate ...> predicate
...> |> to_string() ...> |> to_string()
@ -746,14 +777,14 @@ defmodule RDF.Description do
%{p: ["Foo"]} %{p: ["Foo"]}
""" """
@spec values(t, Statement.term_mapping()) :: map @spec map(t, Statement.term_mapping()) :: map
def values(description, mapping \\ &Statement.default_term_mapping/1) def map(description, fun)
def values(%__MODULE__{} = description, mapping) do def map(%__MODULE__{} = description, fun) do
Map.new(description.predications, fn {predicate, objects} -> Map.new(description.predications, fn {predicate, objects} ->
{ {
mapping.({:predicate, predicate}), fun.({:predicate, predicate}),
objects |> Map.keys() |> Enum.map(&mapping.({:object, &1})) objects |> Map.keys() |> Enum.map(&fun.({:object, &1}))
} }
end) end)
end end

View file

@ -15,9 +15,9 @@ defmodule RDF.Graph do
@behaviour Access @behaviour Access
import RDF.Statement import RDF.Statement, only: [coerce_subject: 1, coerce_graph_name: 1]
import RDF.Utils import RDF.Utils
alias RDF.{Description, IRI, PrefixMap, Statement} alias RDF.{Description, IRI, PrefixMap, Statement, PropertyMap}
@type graph_description :: %{Statement.subject() => Description.t()} @type graph_description :: %{Statement.subject() => Description.t()}
@ -851,10 +851,8 @@ defmodule RDF.Graph do
@doc """ @doc """
Returns a nested map of the native Elixir values of a `RDF.Graph`. 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 When the optional `property_map` argument is given, predicates will be mapped
which will receive a tuple `{statement_position, rdf_term}` where to the terms defined in the `RDF.PropertyMap` if present.
`statement_position` is one of the atoms `:subject`, `:predicate` or `:object`,
while `rdf_term` is the RDF term to be mapped.
## Examples ## Examples
@ -872,7 +870,40 @@ defmodule RDF.Graph do
...> {~I<http://example.com/S1>, ~I<http://example.com/p>, ~L"Foo"}, ...> {~I<http://example.com/S1>, ~I<http://example.com/p>, ~L"Foo"},
...> {~I<http://example.com/S2>, ~I<http://example.com/p>, RDF.XSD.integer(42)} ...> {~I<http://example.com/S2>, ~I<http://example.com/p>, RDF.XSD.integer(42)}
...> ]) ...> ])
...> |> RDF.Graph.values(fn ...> |> RDF.Graph.values(PropertyMap.new(p: ~I<http://example.com/p>))
%{
"http://example.com/S1" => %{p: ["Foo"]},
"http://example.com/S2" => %{p: [42]}
}
"""
@spec values(t, PropertyMap.t() | nil) :: map
def values(graph, property_map \\ nil)
def values(%__MODULE__{} = graph, nil) do
map(graph, &Statement.default_term_mapping/1)
end
def values(%__MODULE__{} = graph, %PropertyMap{} = property_map) do
map(graph, Statement.default_property_mapping(property_map))
end
@doc """
Returns a nested map of a `RDF.Graph` where each element from its triples is mapped with the given function.
The function `fun` 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 `map/2` call.
## Examples
iex> RDF.Graph.new([
...> {~I<http://example.com/S1>, ~I<http://example.com/p>, ~L"Foo"},
...> {~I<http://example.com/S2>, ~I<http://example.com/p>, RDF.XSD.integer(42)}
...> ])
...> |> RDF.Graph.map(fn
...> {:predicate, predicate} -> ...> {:predicate, predicate} ->
...> predicate ...> predicate
...> |> to_string() ...> |> to_string()
@ -888,14 +919,14 @@ defmodule RDF.Graph do
} }
""" """
@spec values(t, Statement.term_mapping()) :: map @spec map(t, Statement.term_mapping()) :: map
def values(graph, mapping \\ &Statement.default_term_mapping/1) def map(description, fun)
def values(%__MODULE__{} = graph, mapping) do def map(%__MODULE__{} = graph, fun) do
Map.new(graph.descriptions, fn {subject, description} -> Map.new(graph.descriptions, fn {subject, description} ->
{ {
mapping.({:subject, subject}), fun.({:subject, subject}),
Description.values(description, mapping) Description.map(description, fun)
} }
end) end)
end end

View file

@ -8,8 +8,12 @@ defmodule RDF.Quad do
alias RDF.{Statement, PropertyMap} alias RDF.{Statement, PropertyMap}
@type t :: @type t :: {
{Statement.subject(), Statement.predicate(), Statement.object(), Statement.graph_name()} Statement.subject(),
Statement.predicate(),
Statement.object(),
Statement.graph_name()
}
@type t_values :: {String.t(), String.t(), any, String.t()} @type t_values :: {String.t(), String.t(), any, String.t()}
@ -93,14 +97,10 @@ defmodule RDF.Quad do
@doc """ @doc """
Returns a tuple of native Elixir values from a `RDF.Quad` of RDF terms. Returns a tuple of native Elixir values from a `RDF.Quad` of RDF terms.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`. When the optional `property_map` argument is given, predicates will be mapped
to the terms defined in the `RDF.PropertyMap` if present.
The optional second argument allows to specify a custom mapping with a function Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
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 ## Examples
@ -108,7 +108,36 @@ defmodule RDF.Quad do
{"http://example.com/S", "http://example.com/p", 42, "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>} iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
...> |> RDF.Quad.values(fn ...> |> RDF.Quad.values(PropertyMap.new(p: ~I<http://example.com/p>))
{"http://example.com/S", :p, 42, "http://example.com/Graph"}
"""
@spec values(t, PropertyMap.t() | nil) :: t_values | nil
def values(quad, property_map \\ nil)
def values(quad, nil) do
map(quad, &Statement.default_term_mapping/1)
end
def values(quad, %PropertyMap{} = property_map) do
map(quad, Statement.default_property_mapping(property_map))
end
@doc """
Returns a tuple where each element from a `RDF.Quad` is mapped with the given function.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
The function `fun` 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 `map/2` call.
## Examples
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
...> |> RDF.Quad.map(fn
...> {:object, object} -> ...> {:object, object} ->
...> RDF.Term.value(object) ...> RDF.Term.value(object)
...> {:graph_name, graph_name} -> ...> {:graph_name, graph_name} ->
@ -119,22 +148,18 @@ defmodule RDF.Quad do
{:S, :p, 42, ~I<http://example.com/Graph>} {:S, :p, 42, ~I<http://example.com/Graph>}
""" """
@spec values(t | any, Statement.term_mapping()) :: t_values | nil @spec map(t, Statement.term_mapping()) :: t_values | nil
def values(quad, mapping \\ &Statement.default_term_mapping/1) def map({subject, predicate, object, graph_name}, fun) do
with subject_value when not is_nil(subject_value) <- fun.({:subject, subject}),
def values({subject, predicate, object, graph_name}, mapping) do predicate_value when not is_nil(predicate_value) <- fun.({:predicate, predicate}),
with subject_value when not is_nil(subject_value) <- mapping.({:subject, subject}), object_value when not is_nil(object_value) <- fun.({:object, object}),
predicate_value when not is_nil(predicate_value) <- mapping.({:predicate, predicate}), graph_name_value <- fun.({:graph_name, graph_name}) do
object_value when not is_nil(object_value) <- mapping.({:object, object}),
graph_name_value <- mapping.({:graph_name, graph_name}) do
{subject_value, predicate_value, object_value, graph_name_value} {subject_value, predicate_value, object_value, graph_name_value}
else else
_ -> nil _ -> nil
end end
end end
def values(_, _), do: nil
@doc """ @doc """
Checks if the given tuple is a valid RDF quad. Checks if the given tuple is a valid RDF quad.

View file

@ -18,7 +18,8 @@ defmodule RDF.Statement do
@type coercible_object :: object | any @type coercible_object :: object | any
@type coercible_graph_name :: graph_name | atom | String.t() @type coercible_graph_name :: graph_name | atom | String.t()
@type qualified_term :: {atom, Term.t() | nil} @type position :: :subject | :predicate | :object | :graph_name
@type qualified_term :: {position, Term.t() | nil}
@type term_mapping :: (qualified_term -> any | nil) @type term_mapping :: (qualified_term -> any | nil)
@type t :: Triple.t() | Quad.t() @type t :: Triple.t() | Quad.t()
@ -67,8 +68,8 @@ defmodule RDF.Statement do
@spec coerce_predicate(coercible_predicate, PropertyMap.t()) :: predicate @spec coerce_predicate(coercible_predicate, PropertyMap.t()) :: predicate
def coerce_predicate(term, context) def coerce_predicate(term, context)
def coerce_predicate(term, %PropertyMap{} = context) when is_atom(term) do def coerce_predicate(term, %PropertyMap{} = property_map) when is_atom(term) do
PropertyMap.iri(context, term) || coerce_predicate(term) PropertyMap.iri(property_map, term) || coerce_predicate(term)
end end
def coerce_predicate(term, _), do: coerce_predicate(term) def coerce_predicate(term, _), do: coerce_predicate(term)
@ -98,6 +99,32 @@ defmodule RDF.Statement do
@doc """ @doc """
Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms. Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms.
When the optional `property_map` argument is given, predicates will be mapped
to the terms defined in the `RDF.PropertyMap` if present.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
## Examples
iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
{"http://example.com/S", "http://example.com/p", 42}
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)}
...> |> RDF.Statement.values(PropertyMap.new(p: ~I<http://example.com/p>))
{"http://example.com/S", :p, 42}
"""
@spec values(t, PropertyMap.t() | nil) :: Triple.t_values() | Quad.t_values() | nil
def values(quad, property_map \\ nil)
def values({_, _, _} = triple, property_map), do: Triple.values(triple, property_map)
def values({_, _, _, _} = quad, property_map), do: Quad.values(quad, property_map)
@doc """
Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`. 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 The optional second argument allows to specify a custom mapping with a function
@ -109,13 +136,8 @@ defmodule RDF.Statement do
## Examples ## Examples
iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
{"http://example.com/S", "http://example.com/p", 42}
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>} iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
...> |> RDF.Statement.values(fn ...> |> RDF.Statement.map(fn
...> {:subject, subject} -> ...> {:subject, subject} ->
...> subject |> to_string() |> String.last() ...> subject |> to_string() |> String.last()
...> {:predicate, predicate} -> ...> {:predicate, predicate} ->
@ -128,11 +150,10 @@ defmodule RDF.Statement do
{"S", :p, 42, ~I<http://example.com/Graph>} {"S", :p, 42, ~I<http://example.com/Graph>}
""" """
@spec values(t | any, term_mapping) :: Triple.t_values() | Quad.t_values() | nil @spec map(t, term_mapping()) :: Triple.t_values() | Quad.t_values() | nil | nil
def values(statement, mapping \\ &default_term_mapping/1) def map(statement, fun)
def values({_, _, _} = triple, mapping), do: RDF.Triple.values(triple, mapping) def map({_, _, _} = triple, fun), do: RDF.Triple.map(triple, fun)
def values({_, _, _, _} = quad, mapping), do: RDF.Quad.values(quad, mapping) def map({_, _, _, _} = quad, fun), do: RDF.Quad.map(quad, fun)
def values(_, _), do: nil
@doc false @doc false
@spec default_term_mapping(qualified_term) :: any | nil @spec default_term_mapping(qualified_term) :: any | nil
@ -140,6 +161,17 @@ defmodule RDF.Statement do
def default_term_mapping({:graph_name, nil}), do: nil def default_term_mapping({:graph_name, nil}), do: nil
def default_term_mapping({_, term}), do: RDF.Term.value(term) def default_term_mapping({_, term}), do: RDF.Term.value(term)
@spec default_property_mapping(PropertyMap.t()) :: term_mapping
def default_property_mapping(%PropertyMap{} = property_map) do
fn
{:predicate, predicate} ->
PropertyMap.term(property_map, predicate) || default_term_mapping({:predicate, predicate})
other ->
default_term_mapping(other)
end
end
@doc """ @doc """
Checks if the given tuple is a valid RDF statement, i.e. RDF triple or quad. Checks if the given tuple is a valid RDF statement, i.e. RDF triple or quad.

View file

@ -87,14 +87,10 @@ defmodule RDF.Triple do
@doc """ @doc """
Returns a tuple of native Elixir values from a `RDF.Triple` of RDF terms. Returns a tuple of native Elixir values from a `RDF.Triple` of RDF terms.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`. When the optional `property_map` argument is given, predicates will be mapped
to the terms defined in the `RDF.PropertyMap` if present.
The optional second argument allows to specify a custom mapping with a function Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
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 ## Examples
@ -102,28 +98,54 @@ defmodule RDF.Triple do
{"http://example.com/S", "http://example.com/p", 42} {"http://example.com/S", "http://example.com/p", 42}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)} iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
...> |> RDF.Triple.values(fn ...> |> RDF.Triple.values(PropertyMap.new(p: ~I<http://example.com/p>))
{"http://example.com/S", :p, 42}
"""
@spec values(t, PropertyMap.t() | nil) :: t_values | nil
def values(triple, property_map \\ nil)
def values(triple, nil) do
map(triple, &Statement.default_term_mapping/1)
end
def values(triple, %PropertyMap{} = property_map) do
map(triple, Statement.default_property_mapping(property_map))
end
@doc """
Returns a triple where each element from a `RDF.Triple` is mapped with the given function.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
The function `fun` 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 `map/2` call.
## Examples
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
...> |> RDF.Triple.map(fn
...> {:object, object} -> RDF.Term.value(object) ...> {:object, object} -> RDF.Term.value(object)
...> {_, term} -> term |> to_string() |> String.last() ...> {_, term} -> term |> to_string() |> String.last()
...> end) ...> end)
{"S", "p", 42} {"S", "p", 42}
""" """
@spec values(t | any, Statement.term_mapping()) :: t_values | nil @spec map(t, Statement.term_mapping()) :: t_values | nil
def values(triple, mapping \\ &Statement.default_term_mapping/1) def map({subject, predicate, object}, fun) do
with subject_value when not is_nil(subject_value) <- fun.({:subject, subject}),
def values({subject, predicate, object}, mapping) do predicate_value when not is_nil(predicate_value) <- fun.({:predicate, predicate}),
with subject_value when not is_nil(subject_value) <- mapping.({:subject, subject}), object_value when not is_nil(object_value) <- fun.({:object, object}) do
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} {subject_value, predicate_value, object_value}
else else
_ -> nil _ -> nil
end end
end end
def values(_, _), do: nil
@doc """ @doc """
Checks if the given tuple is a valid RDF triple. Checks if the given tuple is a valid RDF triple.

View file

@ -534,7 +534,7 @@ defmodule RDF.DataTest do
} }
end end
test "values/2", %{dataset: dataset} do test "map/2", %{dataset: dataset} do
mapping = fn mapping = fn
{:graph_name, graph_name} -> {:graph_name, graph_name} ->
graph_name graph_name
@ -546,7 +546,7 @@ defmodule RDF.DataTest do
RDF.Term.value(term) RDF.Term.value(term)
end end
assert RDF.Data.values(dataset, mapping) == assert RDF.Data.map(dataset, mapping) ==
%{ %{
nil => %{ nil => %{
RDF.Term.value(RDF.iri(EX.S)) => %{ RDF.Term.value(RDF.iri(EX.S)) => %{

View file

@ -1681,6 +1681,19 @@ defmodule RDF.DatasetTest do
end end
test "values/2" do test "values/2" do
assert Dataset.new([{EX.s1(), EX.p(), EX.o1()}, {EX.s2(), EX.p(), EX.o2(), EX.graph()}])
|> Dataset.values(PropertyMap.new(p: EX.p())) ==
%{
nil => %{
RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]}
},
RDF.Term.value(EX.graph()) => %{
RDF.Term.value(EX.s2()) => %{p: [RDF.Term.value(EX.o2())]}
}
}
end
test "map/2" do
mapping = fn mapping = fn
{:graph_name, graph_name} -> {:graph_name, graph_name} ->
graph_name graph_name
@ -1692,10 +1705,10 @@ defmodule RDF.DatasetTest do
RDF.Term.value(term) RDF.Term.value(term)
end end
assert Dataset.new() |> Dataset.values(mapping) == %{} assert Dataset.new() |> Dataset.map(mapping) == %{}
assert Dataset.new([{EX.s1(), EX.p(), EX.o1()}, {EX.s2(), EX.p(), EX.o2(), EX.graph()}]) assert Dataset.new([{EX.s1(), EX.p(), EX.o1()}, {EX.s2(), EX.p(), EX.o2(), EX.graph()}])
|> Dataset.values(mapping) == |> Dataset.map(mapping) ==
%{ %{
nil => %{ nil => %{
RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]} RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]}

View file

@ -839,6 +839,12 @@ defmodule RDF.DescriptionTest do
end end
test "values/2" do test "values/2" do
assert Description.new(EX.s(), init: {EX.s(), EX.p(), ~L"Foo"})
|> Description.values(PropertyMap.new(p: EX.p())) ==
%{p: ["Foo"]}
end
test "map/2" do
mapping = fn mapping = fn
{:predicate, predicate} -> {:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom() predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
@ -847,9 +853,9 @@ defmodule RDF.DescriptionTest do
RDF.Term.value(term) RDF.Term.value(term)
end end
assert Description.new(EX.s()) |> Description.values(mapping) == %{} assert Description.new(EX.s()) |> Description.map(mapping) == %{}
assert Description.new(EX.s(), init: {EX.s(), EX.p(), ~L"Foo"}) |> Description.values(mapping) == assert Description.new(EX.s(), init: {EX.s(), EX.p(), ~L"Foo"}) |> Description.map(mapping) ==
%{p: ["Foo"]} %{p: ["Foo"]}
end end

View file

@ -1252,6 +1252,15 @@ defmodule RDF.GraphTest do
end end
test "values/2" do test "values/2" do
assert Graph.new([{EX.s1(), EX.p(), EX.o1()}, {EX.s2(), EX.p(), EX.o2()}])
|> Graph.values(PropertyMap.new(p: EX.p())) ==
%{
RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]},
RDF.Term.value(EX.s2()) => %{p: [RDF.Term.value(EX.o2())]}
}
end
test "map/2" do
mapping = fn mapping = fn
{:predicate, predicate} -> {:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom() predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
@ -1260,10 +1269,10 @@ defmodule RDF.GraphTest do
RDF.Term.value(term) RDF.Term.value(term)
end end
assert Graph.new() |> Graph.values(mapping) == %{} assert Graph.new() |> Graph.map(mapping) == %{}
assert Graph.new([{EX.s1(), EX.p(), EX.o1()}, {EX.s2(), EX.p(), EX.o2()}]) assert Graph.new([{EX.s1(), EX.p(), EX.o1()}, {EX.s2(), EX.p(), EX.o2()}])
|> Graph.values(mapping) == |> Graph.map(mapping) ==
%{ %{
RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]}, RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]},
RDF.Term.value(EX.s2()) => %{p: [RDF.Term.value(EX.o2())]} RDF.Term.value(EX.s2()) => %{p: [RDF.Term.value(EX.o2())]}

View file

@ -20,15 +20,36 @@ defmodule RDF.QuadTest do
end end
test "with an invalid RDF.Quad" do test "with an invalid RDF.Quad" do
refute Quad.values({~I<http://example.com/S>, ~I<http://example.com/p>})
refute Quad.values({self(), self(), self(), self()}) refute Quad.values({self(), self(), self(), self()})
end end
end end
test "values/2" do describe "values/2" do
test "with a valid RDF.Quad and RDF.PropertyMap" do
assert Quad.values(
{~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42),
~I<http://example.com/Graph>},
PropertyMap.new(p: ~I<http://example.com/p>)
) ==
{"http://example.com/S", :p, 42, "http://example.com/Graph"}
assert Quad.values(
{~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42),
~I<http://example.com/Graph>},
PropertyMap.new()
) ==
{"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
end
test "with an invalid RDF.Triple" do
refute Quad.values({self(), self(), self(), self()}, PropertyMap.new())
end
end
test "map/2" do
assert {~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42), assert {~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42),
~I<http://example.com/Graph>} ~I<http://example.com/Graph>}
|> Quad.values(fn |> Quad.map(fn
{:subject, subject} -> subject |> to_string() |> String.last() |> String.to_atom() {:subject, subject} -> subject |> to_string() |> String.last() |> String.to_atom()
{:predicate, _} -> :p {:predicate, _} -> :p
{:object, object} -> object |> RDF.Term.value() |> Kernel.+(1) {:object, object} -> object |> RDF.Term.value() |> Kernel.+(1)

View file

@ -12,14 +12,33 @@ defmodule RDF.TripleTest do
end end
test "with an invalid RDF.Triple" do test "with an invalid RDF.Triple" do
refute Triple.values({~I<http://example.com/S>, ~I<http://example.com/p>})
refute Triple.values({self(), self(), self()}) refute Triple.values({self(), self(), self()})
end end
end end
test "values/2" do describe "values/2" do
test "with a valid RDF.Triple and RDF.PropertyMap" do
assert Triple.values(
{~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42)},
PropertyMap.new(p: ~I<http://example.com/p>)
) ==
{"http://example.com/S", :p, 42}
assert Triple.values(
{~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42)},
PropertyMap.new()
) ==
{"http://example.com/S", "http://example.com/p", 42}
end
test "with an invalid RDF.Triple" do
refute Triple.values({self(), self(), self()}, PropertyMap.new())
end
end
test "map/2" do
assert {~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42)} assert {~I<http://example.com/S>, ~I<http://example.com/p>, XSD.integer(42)}
|> Triple.values(fn |> Triple.map(fn
{:object, object} -> object |> RDF.Term.value() |> Kernel.+(1) {:object, object} -> object |> RDF.Term.value() |> Kernel.+(1)
{_, term} -> term |> to_string() |> String.last() {_, term} -> term |> to_string() |> String.last()
end) == end) ==