Add RDF-star support on RDF.Description and RDF.Graph

This commit is contained in:
Marcel Otto 2021-09-27 20:57:17 +02:00
parent 4f57fda00f
commit 2819092586
7 changed files with 784 additions and 176 deletions

View file

@ -45,19 +45,23 @@ defmodule RDF do
Namespace,
Literal,
BlankNode,
Triple,
Quad,
Statement,
Description,
Graph,
Dataset,
Serialization,
PrefixMap
}
import RDF.Guards
import RDF.Utils.Bootstrapping
defdelegate default_base_iri(), to: RDF.IRI, as: :default_base
@star? Application.get_env(:rdf, :star, true)
@doc """
Returns whether RDF-star support is enabled.
"""
def star?(), do: @star?
defdelegate default_base_iri(), to: IRI, as: :default_base
@standard_prefixes PrefixMap.new(
xsd: xsd_iri_base(),
@ -136,17 +140,17 @@ defmodule RDF do
default_prefixes() |> PrefixMap.merge!(prefix_mappings)
end
defdelegate read_string(string, opts), to: RDF.Serialization
defdelegate read_string!(string, opts), to: RDF.Serialization
defdelegate read_stream(stream, opts \\ []), to: RDF.Serialization
defdelegate read_stream!(stream, opts \\ []), to: RDF.Serialization
defdelegate read_file(filename, opts \\ []), to: RDF.Serialization
defdelegate read_file!(filename, opts \\ []), to: RDF.Serialization
defdelegate write_string(data, opts), to: RDF.Serialization
defdelegate write_string!(data, opts), to: RDF.Serialization
defdelegate write_stream(data, opts), to: RDF.Serialization
defdelegate write_file(data, filename, opts \\ []), to: RDF.Serialization
defdelegate write_file!(data, filename, opts \\ []), to: RDF.Serialization
defdelegate read_string(string, opts), to: Serialization
defdelegate read_string!(string, opts), to: Serialization
defdelegate read_stream(stream, opts \\ []), to: Serialization
defdelegate read_stream!(stream, opts \\ []), to: Serialization
defdelegate read_file(filename, opts \\ []), to: Serialization
defdelegate read_file!(filename, opts \\ []), to: Serialization
defdelegate write_string(data, opts), to: Serialization
defdelegate write_string!(data, opts), to: Serialization
defdelegate write_stream(data, opts), to: Serialization
defdelegate write_file(data, filename, opts \\ []), to: Serialization
defdelegate write_file!(data, filename, opts \\ []), to: Serialization
@doc """
Checks if the given value is a RDF resource.
@ -181,6 +185,10 @@ defmodule RDF do
end
end
if @star? do
def resource?({_, _, _} = triple), do: RDF.Triple.valid?(triple)
end
def resource?(_), do: false
@doc """
@ -243,15 +251,41 @@ defmodule RDF do
defdelegate literal(value), to: Literal, as: :new
defdelegate literal(value, opts), to: Literal, as: :new
defdelegate triple(s, p, o, property_map \\ nil), to: Triple, as: :new
defdelegate triple(tuple, property_map \\ nil), to: Triple, as: :new
if @star? do
alias RDF.Star.{Triple, Quad, Statement}
defdelegate quad(s, p, o, g, property_map \\ nil), to: Quad, as: :new
defdelegate quad(tuple, property_map \\ nil), to: Quad, as: :new
defdelegate triple(s, p, o, property_map \\ nil), to: Triple, as: :new
defdelegate triple(tuple, property_map \\ nil), to: Triple, as: :new
defdelegate statement(s, p, o), to: Statement, as: :new
defdelegate statement(s, p, o, g), to: Statement, as: :new
defdelegate statement(tuple, property_map \\ nil), to: Statement, as: :new
defdelegate quad(s, p, o, g, property_map \\ nil), to: Quad, as: :new
defdelegate quad(tuple, property_map \\ nil), to: Quad, as: :new
defdelegate statement(s, p, o), to: Statement, as: :new
defdelegate statement(s, p, o, g), to: Statement, as: :new
defdelegate statement(tuple, property_map \\ nil), to: Statement, as: :new
defdelegate coerce_subject(subject, property_map \\ nil), to: Statement
defdelegate coerce_predicate(predicate, property_map \\ nil), to: Statement
defdelegate coerce_object(object, property_map \\ nil), to: Statement
defdelegate coerce_graph_name(graph_name), to: Statement
else
alias RDF.{Triple, Quad, Statement}
defdelegate triple(s, p, o, property_map \\ nil), to: Triple, as: :new
defdelegate triple(tuple, property_map \\ nil), to: Triple, as: :new
defdelegate quad(s, p, o, g, property_map \\ nil), to: Quad, as: :new
defdelegate quad(tuple, property_map \\ nil), to: Quad, as: :new
defdelegate statement(s, p, o), to: Statement, as: :new
defdelegate statement(s, p, o, g), to: Statement, as: :new
defdelegate statement(tuple, property_map \\ nil), to: Statement, as: :new
defdelegate coerce_subject(subject), to: Statement
defdelegate coerce_predicate(predicate, property_map \\ nil), to: Statement
defdelegate coerce_object(object), to: Statement
defdelegate coerce_graph_name(graph_name), to: Statement
end
defdelegate description(subject, opts \\ []), to: Description, as: :new

View file

@ -15,10 +15,8 @@ defmodule RDF.Description do
@behaviour Access
import RDF.Statement,
only: [coerce_subject: 1, coerce_predicate: 1, coerce_predicate: 2, coerce_object: 1]
alias RDF.{Statement, Triple, PropertyMap}
alias RDF.PropertyMap
alias RDF.Star.{Statement, Triple}
@type t :: %__MODULE__{
subject: Statement.subject(),
@ -72,7 +70,7 @@ defmodule RDF.Description do
def new(subject, opts) do
{data, opts} = Keyword.pop(opts, :init)
%__MODULE__{subject: coerce_subject(subject)}
%__MODULE__{subject: RDF.coerce_subject(subject)}
|> init(data, opts)
end
@ -91,7 +89,7 @@ defmodule RDF.Description do
"""
@spec change_subject(t, Statement.coercible_subject()) :: t
def change_subject(%__MODULE__{} = description, new_subject) do
%__MODULE__{description | subject: coerce_subject(new_subject)}
%__MODULE__{description | subject: RDF.coerce_subject(new_subject)}
end
@doc """
@ -121,7 +119,7 @@ defmodule RDF.Description do
end
def add(%__MODULE__{} = description, {subject, predicate, objects}, opts) do
if coerce_subject(subject) == description.subject do
if RDF.coerce_subject(subject) == description.subject do
add(description, {predicate, objects}, opts)
else
description
@ -132,7 +130,7 @@ defmodule RDF.Description do
normalized_objects =
objects
|> List.wrap()
|> Map.new(&{coerce_object(&1), nil})
|> Map.new(&{RDF.coerce_object(&1), nil})
if Enum.empty?(normalized_objects) do
description
@ -142,11 +140,9 @@ defmodule RDF.Description do
| predications:
Map.update(
description.predications,
coerce_predicate(predicate, PropertyMap.from_opts(opts)),
RDF.coerce_predicate(predicate, PropertyMap.from_opts(opts)),
normalized_objects,
fn objects ->
Map.merge(objects, normalized_objects)
end
&Map.merge(&1, normalized_objects)
)
}
end
@ -237,7 +233,7 @@ defmodule RDF.Description do
def delete(description, input, opts \\ [])
def delete(%__MODULE__{} = description, {subject, predicate, objects}, opts) do
if coerce_subject(subject) == description.subject do
if RDF.coerce_subject(subject) == description.subject do
delete(description, {predicate, objects}, opts)
else
description
@ -249,13 +245,13 @@ defmodule RDF.Description do
end
def delete(%__MODULE__{} = description, {predicate, objects}, opts) do
predicate = coerce_predicate(predicate, PropertyMap.from_opts(opts))
predicate = RDF.coerce_predicate(predicate, PropertyMap.from_opts(opts))
if current_objects = Map.get(description.predications, predicate) do
normalized_objects =
objects
|> List.wrap()
|> Enum.map(&coerce_object/1)
|> Enum.map(&RDF.coerce_object/1)
rest = Map.drop(current_objects, normalized_objects)
@ -329,7 +325,7 @@ defmodule RDF.Description do
def delete_predicates(%__MODULE__{} = description, property) do
%__MODULE__{
description
| predications: Map.delete(description.predications, coerce_predicate(property))
| predications: Map.delete(description.predications, RDF.coerce_predicate(property))
}
end
@ -352,7 +348,7 @@ defmodule RDF.Description do
@spec fetch(t, Statement.coercible_predicate()) :: {:ok, [Statement.object()]} | :error
def fetch(%__MODULE__{} = description, predicate) do
with {:ok, objects} <-
Access.fetch(description.predications, coerce_predicate(predicate)) do
Access.fetch(description.predications, RDF.coerce_predicate(predicate)) do
{:ok, Map.keys(objects)}
end
end
@ -427,15 +423,14 @@ defmodule RDF.Description do
([Statement.Object] -> [Statement.Object])
) :: t
def update(%__MODULE__{} = description, predicate, initial \\ nil, fun) do
predicate = coerce_predicate(predicate)
predicate = RDF.coerce_predicate(predicate)
case get(description, predicate) do
nil when is_nil(initial) ->
description
nil ->
if initial do
put(description, {predicate, initial})
else
description
end
put(description, {predicate, initial})
objects ->
objects
@ -481,7 +476,7 @@ defmodule RDF.Description do
([Statement.Object] -> {[Statement.Object], t} | :pop)
) :: {[Statement.Object], t}
def get_and_update(%__MODULE__{} = description, predicate, fun) do
triple_predicate = coerce_predicate(predicate)
triple_predicate = RDF.coerce_predicate(predicate)
case fun.(get(description, triple_predicate)) do
{objects_to_return, new_objects} ->
@ -533,7 +528,7 @@ defmodule RDF.Description do
"""
@impl Access
def pop(%__MODULE__{} = description, predicate) do
case Access.pop(description.predications, coerce_predicate(predicate)) do
case Access.pop(description.predications, RDF.coerce_predicate(predicate)) do
{nil, _} ->
{nil, description}
@ -613,22 +608,37 @@ defmodule RDF.Description do
"""
@spec resources(t) :: MapSet.t()
def resources(%__MODULE__{} = description) do
description
|> objects()
objects(description)
|> MapSet.union(predicates(description))
end
@doc """
The list of all triples within a `RDF.Description`.
When the optional `:filter_star` flag is set to `true` RDF-star triples with a triple as subject or object
will be filtered. So, for a description with a triple as a subject you'll always get an empty list.
"""
@spec triples(t) :: keyword
def triples(%__MODULE__{subject: s} = description) do
Enum.flat_map(description.predications, fn {p, os} ->
Enum.map(os, fn {o, _} -> {s, p, o} end)
end)
@spec triples(t, keyword) :: list(Triple.t())
def triples(%__MODULE__{subject: s} = description, opts \\ []) do
filter_star = Keyword.get(opts, :filter_star, false)
cond do
filter_star and is_tuple(s) ->
[]
filter_star ->
for {p, os} <- description.predications, {o, _} when not is_tuple(o) <- os do
{s, p, o}
end
true ->
for {p, os} <- description.predications, {o, _} <- os do
{s, p, o}
end
end
end
defdelegate statements(description), to: __MODULE__, as: :triples
defdelegate statements(description, opts \\ []), to: __MODULE__, as: :triples
@doc """
Returns the number of statements of a `RDF.Description`.
@ -649,7 +659,7 @@ defmodule RDF.Description do
def include?(description, input, opts \\ [])
def include?(%__MODULE__{} = description, {subject, predicate, objects}, opts) do
coerce_subject(subject) == description.subject &&
RDF.coerce_subject(subject) == description.subject &&
include?(description, {predicate, objects}, opts)
end
@ -659,10 +669,10 @@ defmodule RDF.Description do
def include?(%__MODULE__{} = description, {predicate, objects}, opts) do
if existing_objects =
description.predications[coerce_predicate(predicate, PropertyMap.from_opts(opts))] do
description.predications[RDF.coerce_predicate(predicate, PropertyMap.from_opts(opts))] do
objects
|> List.wrap()
|> Enum.map(&coerce_object/1)
|> Enum.map(&RDF.coerce_object/1)
|> Enum.all?(fn object -> Map.has_key?(existing_objects, object) end)
else
false
@ -714,7 +724,7 @@ defmodule RDF.Description do
"""
@spec describes?(t, Statement.subject()) :: boolean
def describes?(%__MODULE__{subject: subject}, other_subject) do
subject == coerce_subject(other_subject)
subject == RDF.coerce_subject(other_subject)
end
@doc """
@ -727,6 +737,8 @@ defmodule RDF.Description do
When a `:context` option is given with a `RDF.PropertyMap`, predicates will
be mapped to the terms defined in the `RDF.PropertyMap`, if present.
Note: RDF-star statements where the object is a triple will be ignored.
## Examples
iex> RDF.Description.new(~I<http://example.com/S>, init: {~I<http://example.com/p>, ~L"Foo"})
@ -741,9 +753,9 @@ defmodule RDF.Description do
@spec values(t, keyword) :: map
def values(%__MODULE__{} = description, opts \\ []) do
if property_map = PropertyMap.from_opts(opts) do
map(description, Statement.default_property_mapping(property_map))
map(description, RDF.Statement.default_property_mapping(property_map))
else
map(description, &Statement.default_term_mapping/1)
map(description, &RDF.Statement.default_term_mapping/1)
end
end
@ -759,6 +771,8 @@ defmodule RDF.Description do
`nil` this will be interpreted as an error and will become the overhaul result
of the `map/2` call.
Note: RDF-star statements where the object is a triple will be ignored.
## Examples
iex> RDF.Description.new(~I<http://example.com/S>, init: {~I<http://example.com/p>, ~L"Foo"})
@ -779,11 +793,21 @@ defmodule RDF.Description do
def map(description, fun)
def map(%__MODULE__{} = description, fun) do
Map.new(description.predications, fn {predicate, objects} ->
{
fun.({:predicate, predicate}),
objects |> Map.keys() |> Enum.map(&fun.({:object, &1}))
}
Enum.reduce(description.predications, %{}, fn {predicate, objects}, map ->
objects
|> Map.keys()
|> Enum.reject(&is_tuple/1)
|> case do
[] ->
map
objects ->
Map.put(
map,
fun.({:predicate, predicate}),
Enum.map(objects, &fun.({:object, &1}))
)
end
end)
end
@ -803,7 +827,7 @@ defmodule RDF.Description do
%__MODULE__{
description
| predications:
Map.take(description.predications, Enum.map(predicates, &coerce_predicate/1))
Map.take(description.predications, Enum.map(predicates, &RDF.coerce_predicate/1))
}
end

View file

@ -15,9 +15,10 @@ defmodule RDF.Graph do
@behaviour Access
import RDF.Statement, only: [coerce_subject: 1, coerce_graph_name: 1]
alias RDF.{Description, IRI, PrefixMap, PropertyMap}
alias RDF.Star.Statement
import RDF.Utils
alias RDF.{Description, IRI, PrefixMap, Statement, PropertyMap}
@type graph_description :: %{Statement.subject() => Description.t()}
@ -120,7 +121,7 @@ defmodule RDF.Graph do
def new(data, opts)
def new(%__MODULE__{} = graph, opts) do
%__MODULE__{graph | name: opts |> Keyword.get(:name) |> coerce_graph_name()}
%__MODULE__{graph | name: opts |> Keyword.get(:name) |> RDF.coerce_graph_name()}
|> add_prefixes(Keyword.get(opts, :prefixes))
|> set_base_iri(Keyword.get(opts, :base_iri))
end
@ -158,7 +159,7 @@ defmodule RDF.Graph do
"""
@spec change_name(t, Statement.coercible_graph_name()) :: t
def change_name(%__MODULE__{} = graph, new_name) do
%__MODULE__{graph | name: coerce_graph_name(new_name)}
%__MODULE__{graph | name: RDF.coerce_graph_name(new_name)}
end
@doc """
@ -184,10 +185,10 @@ defmodule RDF.Graph do
def add(graph, input, opts \\ [])
def add(%__MODULE__{} = graph, {subject, predications}, opts),
do: do_add(graph, coerce_subject(subject), predications, opts)
do: do_add(graph, RDF.coerce_subject(subject), predications, opts)
def add(%__MODULE__{} = graph, {subject, _, _} = triple, opts),
do: do_add(graph, coerce_subject(subject), triple, opts)
do: do_add(graph, RDF.coerce_subject(subject), triple, opts)
def add(graph, {subject, predicate, object, _}, opts),
do: add(graph, {subject, predicate, object}, opts)
@ -340,10 +341,10 @@ defmodule RDF.Graph do
def delete(graph, input, opts \\ [])
def delete(%__MODULE__{} = graph, {subject, _, _} = triple, opts),
do: do_delete(graph, coerce_subject(subject), triple, opts)
do: do_delete(graph, RDF.coerce_subject(subject), triple, opts)
def delete(%__MODULE__{} = graph, {subject, predications}, opts),
do: do_delete(graph, coerce_subject(subject), predications, opts)
do: do_delete(graph, RDF.coerce_subject(subject), predications, opts)
def delete(graph, {subject, predicate, object, _}, opts),
do: delete(graph, {subject, predicate, object}, opts)
@ -404,7 +405,7 @@ defmodule RDF.Graph do
end
def delete_descriptions(%__MODULE__{} = graph, subject) do
%__MODULE__{graph | descriptions: Map.delete(graph.descriptions, coerce_subject(subject))}
%__MODULE__{graph | descriptions: Map.delete(graph.descriptions, RDF.coerce_subject(subject))}
end
defdelegate delete_subjects(graph, subjects), to: __MODULE__, as: :delete_descriptions
@ -449,7 +450,7 @@ defmodule RDF.Graph do
update_description_fun
) :: t
def update(%__MODULE__{} = graph, subject, initial \\ nil, fun) do
subject = coerce_subject(subject)
subject = RDF.coerce_subject(subject)
case get(graph, subject) do
nil ->
@ -491,31 +492,7 @@ defmodule RDF.Graph do
@impl Access
@spec fetch(t, Statement.coercible_subject()) :: {:ok, Description.t()} | :error
def fetch(%__MODULE__{} = graph, subject) do
Access.fetch(graph.descriptions, coerce_subject(subject))
end
@doc """
Execute the given `query` against the given `graph`.
This is just a convenience delegator function to `RDF.Query.execute!/3` with
the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`.
See `RDF.Query.execute/3` and `RDF.Query.execute!/3` for more information and examples.
"""
def query(graph, query, opts \\ []) do
RDF.Query.execute!(query, graph, opts)
end
@doc """
Returns a `Stream` for the execution of the given `query` against the given `graph`.
This is just a convenience delegator function to `RDF.Query.stream!/3` with
the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`.
See `RDF.Query.stream/3` and `RDF.Query.stream!/3` for more information and examples.
"""
def query_stream(graph, query, opts \\ []) do
RDF.Query.stream!(query, graph, opts)
Access.fetch(graph.descriptions, RDF.coerce_subject(subject))
end
@doc """
@ -542,13 +519,7 @@ defmodule RDF.Graph do
end
end
@doc """
The `RDF.Description` of the given subject.
"""
@spec description(t, Statement.coercible_subject()) :: Description.t() | nil
def description(%__MODULE__{} = graph, subject) do
Map.get(graph.descriptions, coerce_subject(subject))
end
defdelegate description(graph, subject), to: __MODULE__, as: :get
@doc """
All `RDF.Description`s within a `RDF.Graph`.
@ -585,7 +556,7 @@ defmodule RDF.Graph do
@spec get_and_update(t, Statement.coercible_subject(), get_and_update_description_fun) ::
{Description.t(), input}
def get_and_update(%__MODULE__{} = graph, subject, fun) do
subject = coerce_subject(subject)
subject = RDF.coerce_subject(subject)
case fun.(get(graph, subject)) do
{old_description, new_description} ->
@ -639,7 +610,7 @@ defmodule RDF.Graph do
@impl Access
@spec pop(t, Statement.coercible_subject()) :: {Description.t() | nil, t}
def pop(%__MODULE__{} = graph, subject) do
case Access.pop(graph.descriptions, coerce_subject(subject)) do
case Access.pop(graph.descriptions, RDF.coerce_subject(subject)) do
{nil, _} ->
{nil, graph}
@ -786,13 +757,20 @@ defmodule RDF.Graph do
{RDF.iri(EX.S2), RDF.iri(EX.p2), RDF.iri(EX.O2)}]
"""
@spec triples(t) :: [Statement.t()]
def triples(%__MODULE__{} = graph) do
Enum.flat_map(graph.descriptions, fn {_, description} ->
Description.triples(description)
end)
def triples(%__MODULE__{} = graph, opts \\ []) do
if Keyword.get(opts, :filter_star, false) do
Enum.flat_map(graph.descriptions, fn
{subject, _} when is_tuple(subject) -> []
{_, description} -> Description.triples(description, opts)
end)
else
Enum.flat_map(graph.descriptions, fn {_, description} ->
Description.triples(description, opts)
end)
end
end
defdelegate statements(graph), to: __MODULE__, as: :triples
defdelegate statements(graph, opts \\ []), to: __MODULE__, as: :triples
@doc """
Checks if the given `input` statements exist within `graph`.
@ -801,13 +779,13 @@ defmodule RDF.Graph do
def include?(graph, input, opts \\ [])
def include?(%__MODULE__{} = graph, {subject, _, _} = triple, opts),
do: do_include?(graph, coerce_subject(subject), triple, opts)
do: do_include?(graph, RDF.coerce_subject(subject), triple, opts)
def include?(graph, {subject, predicate, object, _}, opts),
do: include?(graph, {subject, predicate, object}, opts)
def include?(%__MODULE__{} = graph, {subject, predications}, opts),
do: do_include?(graph, coerce_subject(subject), predications, opts)
do: do_include?(graph, RDF.coerce_subject(subject), predications, opts)
def include?(%__MODULE__{} = graph, %Description{subject: subject} = description, opts),
do: do_include?(graph, subject, description, opts)
@ -851,7 +829,68 @@ defmodule RDF.Graph do
"""
@spec describes?(t, Statement.coercible_subject()) :: boolean
def describes?(%__MODULE__{} = graph, subject) do
Map.has_key?(graph.descriptions, coerce_subject(subject))
Map.has_key?(graph.descriptions, RDF.coerce_subject(subject))
end
@doc """
Creates a graph from another one by limiting its statements to those using one of the given `subjects`.
If `subjects` contains IRIs that are not used in the `graph`, they're simply ignored.
The optional `properties` argument allows to limit also properties of the subject descriptions.
If `nil` is passed as the `subjects`, the subjects will not be limited.
"""
@spec take(
t,
[Statement.coercible_subject()] | Enum.t() | nil,
[Statement.coercible_predicate()] | Enum.t() | nil
) :: t
def take(graph, subjects, properties \\ nil)
def take(%__MODULE__{} = graph, nil, nil), do: graph
def take(%__MODULE__{descriptions: descriptions} = graph, subjects, nil) do
%__MODULE__{
graph
| descriptions: Map.take(descriptions, Enum.map(subjects, &RDF.coerce_subject/1))
}
end
def take(%__MODULE__{} = graph, subjects, properties) do
graph = take(graph, subjects, nil)
%__MODULE__{
graph
| descriptions:
Map.new(graph.descriptions, fn {subject, description} ->
{subject, Description.take(description, properties)}
end)
}
end
@doc """
Execute the given `query` against the given `graph`.
This is just a convenience delegator function to `RDF.Query.execute!/3` with
the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`.
See `RDF.Query.execute/3` and `RDF.Query.execute!/3` for more information and examples.
"""
def query(graph, query, opts \\ []) do
RDF.Query.execute!(query, graph, opts)
end
@doc """
Returns a `Stream` for the execution of the given `query` against the given `graph`.
This is just a convenience delegator function to `RDF.Query.stream!/3` with
the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`.
See `RDF.Query.stream/3` and `RDF.Query.stream!/3` for more information and examples.
"""
def query_stream(graph, query, opts \\ []) do
RDF.Query.stream!(query, graph, opts)
end
@doc """
@ -886,9 +925,9 @@ defmodule RDF.Graph do
@spec values(t, keyword) :: map
def values(%__MODULE__{} = graph, opts \\ []) do
if property_map = PropertyMap.from_opts(opts) do
map(graph, Statement.default_property_mapping(property_map))
map(graph, RDF.Statement.default_property_mapping(property_map))
else
map(graph, &Statement.default_term_mapping/1)
map(graph, &RDF.Statement.default_term_mapping/1)
end
end
@ -901,6 +940,8 @@ defmodule RDF.Graph do
`nil` this will be interpreted as an error and will become the overhaul result
of the `map/2` call.
Note: RDF-star statements where the subject or object is a triple will be ignored.
## Examples
iex> RDF.Graph.new([
@ -914,8 +955,8 @@ defmodule RDF.Graph do
...> |> String.split("/")
...> |> List.last()
...> |> String.to_atom()
...> {_, term} ->
...> RDF.Term.value(term)
...> {_, term} ->
...> RDF.Term.value(term)
...> end)
%{
"http://example.com/S1" => %{p: ["Foo"]},
@ -927,51 +968,25 @@ defmodule RDF.Graph do
def map(description, fun)
def map(%__MODULE__{} = graph, fun) do
Map.new(graph.descriptions, fn {subject, description} ->
{
fun.({:subject, subject}),
Description.map(description, fun)
}
Enum.reduce(graph.descriptions, %{}, fn
{subject, _}, map when is_tuple(subject) ->
map
{subject, description}, map ->
case Description.map(description, fun) do
mapped_objects when map_size(mapped_objects) == 0 ->
map
mapped_objects ->
Map.put(
map,
fun.({:subject, subject}),
mapped_objects
)
end
end)
end
@doc """
Creates a graph from another one by limiting its statements to those using one of the given `subjects`.
If `subjects` contains IRIs that are not used in the `graph`, they're simply ignored.
The optional `properties` argument allows to limit also properties of the subject descriptions.
If `nil` is passed as the `subjects`, the subjects will not be limited.
"""
@spec take(
t,
[Statement.coercible_subject()] | Enum.t() | nil,
[Statement.coercible_predicate()] | Enum.t() | nil
) :: t
def take(graph, subjects, properties \\ nil)
def take(%__MODULE__{} = graph, nil, nil), do: graph
def take(%__MODULE__{descriptions: descriptions} = graph, subjects, nil) do
%__MODULE__{
graph
| descriptions: Map.take(descriptions, Enum.map(subjects, &coerce_subject/1))
}
end
def take(%__MODULE__{} = graph, subjects, properties) do
graph = take(graph, subjects, nil)
%__MODULE__{
graph
| descriptions:
Map.new(graph.descriptions, fn {subject, description} ->
{subject, Description.take(description, properties)}
end)
}
end
@doc """
Checks if two `RDF.Graph`s are equal.

View file

@ -71,13 +71,8 @@ defmodule RDF.Star.Statement do
def coerce_subject({_, _, _} = triple, property_map), do: Triple.new(triple, property_map)
def coerce_subject(subject, _), do: RDF.Statement.coerce_subject(subject)
@doc false
@spec coerce_predicate(coercible_predicate) :: predicate
def coerce_predicate(iri), do: RDF.Statement.coerce_predicate(iri)
@doc false
@spec coerce_predicate(coercible_predicate, PropertyMap.t()) :: predicate
def coerce_predicate(term, context), do: RDF.Statement.coerce_predicate(term, context)
defdelegate coerce_predicate(coercible_predicate), to: RDF.Statement
defdelegate coerce_predicate(term, context), to: RDF.Statement
@doc false
@spec coerce_object(coercible_object, PropertyMap.t() | nil) :: object
@ -85,9 +80,7 @@ defmodule RDF.Star.Statement do
def coerce_object({_, _, _} = triple, property_map), do: Triple.new(triple, property_map)
def coerce_object(object, _), do: RDF.Statement.coerce_object(object)
@doc false
@spec coerce_graph_name(coercible_graph_name) :: graph_name
def coerce_graph_name(iri), do: RDF.Statement.coerce_graph_name(iri)
defdelegate coerce_graph_name(iri), to: RDF.Statement
@doc """
Checks if the given tuple is a valid RDF-star statement, i.e. RDF-star triple or quad.
@ -123,4 +116,25 @@ defmodule RDF.Star.Statement do
@spec valid_graph_name?(graph_name | any) :: boolean
def valid_graph_name?(any), do: RDF.Statement.valid_graph_name?(any)
@doc """
Checks if the given tuple is a RDF-star statement annotating a triple on subject or object position.
Note: This function won't check if the given tuple or the annotated triple is valid.
Use `valid?/1` for this purpose.
## Examples
iex> RDF.Star.Statement.annotation?({EX.S, EX.P, EX.O})
false
iex> RDF.Star.Statement.annotation?({EX.AS, EX.AP, {EX.S, EX.P, EX.O}})
true
iex> RDF.Star.Statement.annotation?({{EX.S, EX.P, EX.O}, EX.AP, EX.AO})
true
"""
@spec annotation?(Triple.t() | Quad.t() | any) :: boolean
def annotation?({{_, _, _}, _, _}), do: true
def annotation?({_, _, {_, _, _}}), do: true
def annotation?(_), do: false
end

View file

@ -54,6 +54,12 @@ defmodule RDF.Test.Case do
###############################
# RDF.Statement
@statement {RDF.iri(EX.S), RDF.iri(EX.P), RDF.literal("Foo")}
def statement(), do: @statement
@coercible_statement {EX.S, EX.P, "Foo"}
def coercible_statement(), do: @coercible_statement
@valid_triple {RDF.iri(EX.S), EX.p(), RDF.iri(EX.O)}
def valid_triple(), do: @valid_triple
@ -157,8 +163,10 @@ defmodule RDF.Test.Case do
do: descriptions == %{}
def graph_includes_statement?(graph, {subject, _, _} = statement) do
subject = if is_tuple(subject), do: subject, else: iri(subject)
graph.descriptions
|> Map.get(iri(subject), %{})
|> Map.get(subject, %{})
|> Enum.member?(statement)
end
@ -198,4 +206,25 @@ defmodule RDF.Test.Case do
|> Map.get(iri(graph_context), named_graph(graph_context))
|> graph_includes_statement?({subject, predicate, objects})
end
###############################
# RDF.Star annotations
@star_statement {@statement, EX.ap(), EX.ao()}
def star_statement(), do: @star_statement
@empty_annotation Description.new(@statement)
def empty_annotation(), do: @empty_annotation
@annotation Description.new(@statement, init: {EX.ap(), EX.ao()})
def annotation(), do: @annotation
@object_annotation Description.new(EX.As, init: {EX.ap(), @statement})
def object_annotation(), do: @object_annotation
@graph_with_annotation Graph.new(init: @annotation)
def graph_with_annotation(), do: @graph_with_annotation
@graph_with_annotations Graph.new(init: [@annotation, @object_annotation])
def graph_with_annotations(), do: @graph_with_annotations
end

View file

@ -0,0 +1,237 @@
defmodule RDF.Star.Description.Test do
use RDF.Test.Case
describe "new/1" do
test "with a valid triple as subject" do
assert description_of_subject(
Description.new(statement()),
statement()
)
assert Description.new(statement(), init: {EX.ap(), EX.ao()})
|> description_includes_predication({EX.ap(), EX.ao()})
end
test "with a coercible triple as subject" do
assert description_of_subject(
Description.new(coercible_statement()),
statement()
)
assert Description.new(statement(), init: {EX.ap(), EX.ao()})
|> description_includes_predication({EX.ap(), EX.ao()})
end
end
test "subject/1" do
assert Description.subject(empty_annotation()) == statement()
end
test "change_subject/2" do
changed = Description.change_subject(description(), coercible_statement())
assert changed.subject == statement()
assert Description.change_subject(changed, description().subject) == description()
end
describe "add/3" do
test "with a proper triple as a subject" do
assert empty_annotation()
|> Description.add({statement(), EX.ap(), EX.ao()})
|> description_includes_predication({EX.ap(), EX.ao()})
end
test "with a proper triple as a object" do
assert description()
|> Description.add({EX.Subject, EX.ap(), statement()})
|> description_includes_predication({EX.ap(), statement()})
end
test "with a proper triple as a subject and object" do
assert empty_annotation()
|> Description.add({statement(), EX.ap(), statement()})
|> description_includes_predication({EX.ap(), statement()})
end
test "with a list of proper objects" do
description =
description()
|> Description.add({EX.Subject, EX.ap(), [statement(), {EX.s(), EX.p(), EX.o()}]})
assert description_includes_predication(description, {EX.ap(), statement()})
assert description_includes_predication(description, {EX.ap(), {EX.s(), EX.p(), EX.o()}})
end
test "with a list of predicate-object tuples" do
assert empty_annotation()
|> Description.add([{EX.ap(), statement()}])
|> description_includes_predication({EX.ap(), statement()})
end
test "with a description map" do
assert empty_annotation()
|> Description.add(%{EX.ap() => statement()})
|> description_includes_predication({EX.ap(), statement()})
end
test "with coercible triples" do
assert empty_annotation()
|> Description.add({coercible_statement(), EX.ap(), coercible_statement()})
|> description_includes_predication({EX.ap(), statement()})
end
end
test "put/3" do
assert annotation()
|> Description.put({statement(), EX.ap(), EX.ao2()})
|> description_includes_predication({EX.ap(), EX.ao2()})
assert annotation()
|> Description.put({statement(), EX.ap(), statement()})
|> description_includes_predication({EX.ap(), statement()})
end
test "delete/3" do
assert Description.delete(annotation(), {statement(), EX.ap(), EX.ao()}) ==
empty_annotation()
assert Description.delete(object_annotation(), {EX.As, EX.ap(), statement()}) ==
Description.new(EX.As)
assert Description.delete(object_annotation(), {EX.ap(), statement()}) ==
Description.new(EX.As)
end
test "delete_predicates/2" do
assert Description.delete_predicates(annotation(), EX.ap()) ==
empty_annotation()
assert Description.delete_predicates(object_annotation(), EX.ap()) ==
Description.new(EX.As)
end
test "fetch/2" do
assert Description.fetch(annotation(), EX.ap()) == {:ok, [EX.ao()]}
assert Description.fetch(object_annotation(), EX.ap()) == {:ok, [statement()]}
end
test "get/2" do
assert Description.get(annotation(), EX.ap()) == [EX.ao()]
assert Description.get(object_annotation(), EX.ap()) == [statement()]
end
test "first/2" do
assert Description.first(annotation(), EX.ap()) == EX.ao()
assert Description.first(object_annotation(), EX.ap()) == statement()
end
test "pop/2" do
assert Description.pop(annotation(), EX.ap()) == {[EX.ao()], empty_annotation()}
assert Description.pop(object_annotation(), EX.ap()) ==
{[statement()], Description.new(EX.As)}
end
test "update/4" do
assert (description =
Description.update(empty_annotation(), EX.ap(), statement(), fn _ ->
raise "unexpected"
end)) ==
empty_annotation()
|> Description.add(%{EX.ap() => statement()})
assert Description.update(description, EX.ap(), statement(), fn
[{s, p, _} = statement] ->
assert statement == statement()
[statement, {s, p, EX.O}]
end) ==
empty_annotation()
|> Description.add(%{EX.ap() => statement()})
|> Description.add(%{EX.ap() => {EX.S, EX.P, EX.O}})
end
test "objects/1" do
assert Description.new(statement(), init: {EX.ap(), statement()})
|> Description.objects() == MapSet.new([statement()])
end
test "resources/1" do
assert Description.new(statement(), init: {EX.ap(), statement()})
|> Description.resources() == MapSet.new([statement(), EX.ap()])
end
describe "statements/1" do
test "without the filter_star flag" do
assert Description.new(statement(), init: {EX.ap(), statement()})
|> Description.statements() == [{statement(), EX.ap(), statement()}]
end
test "with the filter_star flag" do
assert Description.new(statement(),
init: [
{EX.ap(), EX.ao()},
{EX.ap(), statement()}
]
)
|> Description.statements(filter_star: true) == []
assert Description.new(EX.s(),
init: [
{EX.p(), EX.o()},
{EX.ap(), statement()}
]
)
|> Description.statements(filter_star: true) == [{EX.s(), EX.p(), EX.o()}]
end
end
test "statement_count/1" do
assert Description.new(statement(), init: {EX.ap(), statement()})
|> Description.statement_count() == 1
end
test "include?/2" do
assert Description.new(statement(), init: {EX.ap(), statement()})
|> Description.include?({statement(), EX.ap(), statement()})
end
test "describes?/2" do
assert Description.describes?(annotation(), statement())
assert Description.describes?(annotation(), coercible_statement())
end
test "values/2" do
assert Description.new(statement(), init: {EX.ap(), statement()})
|> Description.values() == %{}
assert Description.new(EX.s(),
init: [
{EX.p(), ~L"Foo"},
{EX.ap(), statement()}
]
)
|> Description.values() ==
%{RDF.Term.value(EX.p()) => ["Foo"]}
end
test "map/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(statement(), init: {EX.ap(), statement()})
|> Description.map(mapping) == %{}
assert Description.new(EX.s(),
init: [
{EX.p(), ~L"Foo"},
{EX.ap(), statement()}
]
)
|> Description.map(mapping) ==
%{p: ["Foo"]}
end
end

View file

@ -0,0 +1,255 @@
defmodule RDF.Star.Graph.Test do
use RDF.Test.Case
test "new/1" do
assert Graph.new(init: {statement(), EX.ap(), EX.ao()})
|> graph_includes_statement?({statement(), EX.ap(), EX.ao()})
assert Graph.new(init: annotation())
|> graph_includes_statement?({statement(), EX.ap(), EX.ao()})
end
describe "add/3" do
test "with a proper triple as a subject" do
graph =
graph()
|> Graph.add({statement(), EX.ap(), EX.ao1()})
|> Graph.add({statement(), EX.ap(), EX.ao2()})
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao1()})
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao2()})
end
test "with a proper triple as a object" do
graph =
graph()
|> Graph.add({EX.as(), EX.ap(), statement()})
|> Graph.add({EX.as(), EX.ap(), {EX.s(), EX.p(), EX.o2()}})
assert graph_includes_statement?(graph, {EX.as(), EX.ap(), statement()})
assert graph_includes_statement?(graph, {EX.as(), EX.ap(), {EX.s(), EX.p(), EX.o2()}})
end
test "with a proper triple as a subject and object" do
assert graph()
|> Graph.add({statement(), EX.ap(), statement()})
|> graph_includes_statement?({statement(), EX.ap(), statement()})
end
test "with a list of triples" do
graph =
Graph.add(graph(), [
{statement(), EX.ap(), EX.ao()},
{EX.as(), EX.ap(), statement()},
{EX.s(), EX.p(), EX.o()}
])
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao()})
assert graph_includes_statement?(graph, {EX.as(), EX.ap(), statement()})
assert graph_includes_statement?(graph, {EX.s(), EX.p(), EX.o()})
end
test "with a graph map" do
assert graph()
|> Graph.add(%{statement() => %{EX.ap() => statement()}})
|> graph_includes_statement?({statement(), EX.ap(), statement()})
end
test "with coercible triples" do
assert graph()
|> Graph.add({coercible_statement(), EX.ap(), coercible_statement()})
|> graph_includes_statement?({statement(), EX.ap(), statement()})
end
end
describe "put/3" do
test "with a proper triple as a subject" do
graph =
graph()
|> Graph.put({statement(), EX.ap(), EX.ao1()})
|> Graph.put({statement(), EX.ap(), EX.ao2()})
refute graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao1()})
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao2()})
end
test "with a proper triple as a object" do
graph =
graph()
|> Graph.put({EX.as(), EX.ap(), statement()})
|> Graph.put({EX.as(), EX.ap(), {EX.s(), EX.p(), EX.o2()}})
refute graph_includes_statement?(graph, {EX.as(), EX.ap(), statement()})
assert graph_includes_statement?(graph, {EX.as(), EX.ap(), {EX.s(), EX.p(), EX.o2()}})
end
test "with a proper triple as a subject and object" do
assert graph()
|> Graph.put({statement(), EX.ap(), statement()})
|> graph_includes_statement?({statement(), EX.ap(), statement()})
end
test "with a list of triples" do
graph =
Graph.put(graph(), [
{statement(), EX.ap(), EX.ao()},
{EX.as(), EX.ap(), statement()},
{EX.s(), EX.p(), EX.o()}
])
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao()})
assert graph_includes_statement?(graph, {EX.as(), EX.ap(), statement()})
assert graph_includes_statement?(graph, {EX.s(), EX.p(), EX.o()})
end
test "with a graph map" do
assert graph()
|> Graph.put(%{statement() => %{EX.ap() => statement()}})
|> graph_includes_statement?({statement(), EX.ap(), statement()})
end
test "with coercible triples" do
assert graph()
|> Graph.put({coercible_statement(), EX.ap(), coercible_statement()})
|> graph_includes_statement?({statement(), EX.ap(), statement()})
end
end
test "put_properties/3" do
graph =
graph()
|> Graph.put_properties({statement(), EX.ap(), EX.ao1()})
|> Graph.put_properties({statement(), EX.ap(), EX.ao2()})
refute graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao1()})
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao2()})
graph =
graph()
|> Graph.put_properties(Graph.new(init: {statement(), EX.ap(), EX.ao1()}))
|> Graph.put_properties(Graph.new(init: {statement(), EX.ap(), EX.ao2()}))
refute graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao1()})
assert graph_includes_statement?(graph, {statement(), EX.ap(), EX.ao2()})
end
test "delete/3" do
assert graph_with_annotation() |> Graph.delete(star_statement()) == graph()
end
test "delete_description/3" do
assert graph_with_annotation() |> Graph.delete_descriptions(statement()) == graph()
end
test "update/3" do
assert Graph.update(graph(), statement(), annotation(), fn _ -> raise "unexpected" end) ==
graph_with_annotation()
assert graph()
|> Graph.add({statement(), EX.foo(), EX.bar()})
|> Graph.update(statement(), fn _ -> annotation() end) ==
graph_with_annotation()
end
test "fetch/2" do
assert graph_with_annotation() |> Graph.fetch(statement()) == {:ok, annotation()}
end
test "get/3" do
assert graph_with_annotation() |> Graph.get(statement()) == annotation()
end
test "get_and_update/3" do
assert Graph.get_and_update(graph_with_annotation(), statement(), fn description ->
{description, object_annotation()}
end) ==
{annotation(), Graph.new(init: {statement(), EX.ap(), statement()})}
end
test "pop/2" do
assert Graph.pop(graph_with_annotation(), statement()) == {annotation(), graph()}
end
test "subject_count/1" do
assert Graph.subject_count(graph_with_annotations()) == 2
end
test "subjects/1" do
assert Graph.subjects(graph_with_annotations()) == MapSet.new([statement(), RDF.iri(EX.As)])
end
test "objects/1" do
assert Graph.objects(graph_with_annotations()) == MapSet.new([statement(), EX.ao()])
end
describe "statements/1" do
test "without the filter_star flag" do
assert Graph.statements(graph_with_annotations()) == [
star_statement(),
{RDF.iri(EX.As), EX.ap(), statement()}
]
end
test "with the filter_star flag" do
assert Graph.statements(graph_with_annotations(), filter_star: true) == []
assert Graph.statements(graph_with_annotations(), filter_star: true) == []
assert Graph.new(
init: [
{statement(), EX.ap(), EX.ao()},
{statement(), EX.ap(), statement()},
{EX.s(), EX.p(), EX.o()},
{EX.s(), EX.ap(), statement()}
]
)
|> Graph.statements(filter_star: true) == [{EX.s(), EX.p(), EX.o()}]
end
end
test "include?/3" do
assert Graph.include?(graph_with_annotations(), star_statement())
assert Graph.include?(graph_with_annotations(), {EX.As, EX.ap(), statement()})
end
test "describes?/2" do
assert Graph.describes?(graph_with_annotations(), statement())
end
test "values/2" do
assert graph_with_annotations() |> Graph.values() == %{}
assert Graph.new(
init: [
annotation(),
{EX.s(), EX.p(), ~L"Foo"},
{EX.s(), EX.ap(), statement()}
]
)
|> Graph.values() ==
%{RDF.Term.value(EX.s()) => %{RDF.Term.value(EX.p()) => ["Foo"]}}
end
test "map/2" do
mapping = fn
{:predicate, predicate} ->
predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom()
{_, term} ->
RDF.Term.value(term)
end
assert graph_with_annotations() |> Graph.map(mapping) == %{}
assert Graph.new([
annotation(),
{EX.s1(), EX.p(), EX.o1()},
{EX.s2(), EX.p(), EX.o2()},
object_annotation()
])
|> Graph.map(mapping) ==
%{
RDF.Term.value(EX.s1()) => %{p: [RDF.Term.value(EX.o1())]},
RDF.Term.value(EX.s2()) => %{p: [RDF.Term.value(EX.o2())]}
}
end
end