rdf-ex/lib/rdf/description.ex
2021-11-15 22:01:22 +01:00

917 lines
29 KiB
Elixir

defmodule RDF.Description do
@moduledoc """
A set of RDF triples about the same subject.
`RDF.Description` implements:
- Elixir's `Access` behaviour
- Elixir's `Enumerable` protocol
- Elixir's `Inspect` protocol
- the `RDF.Data` protocol
"""
@enforce_keys [:subject]
defstruct subject: nil, predications: %{}
@behaviour Access
alias RDF.PropertyMap
alias RDF.Star.{Statement, Triple}
@type t :: %__MODULE__{
subject: Statement.subject(),
predications: predications
}
@type predications :: %{Statement.predicate() => %{Statement.object() => nil}}
@type input ::
Statement.coercible_t()
| {
Statement.coercible_predicate(),
Statement.coercible_object() | [Statement.coercible_object()]
}
| %{
Statement.coercible_predicate() =>
Statement.coercible_object() | [Statement.coercible_object()]
}
| [
Statement.coercible_t()
| {
Statement.coercible_predicate(),
Statement.coercible_object() | [Statement.coercible_object()]
}
| t
]
| t
@doc """
Creates an `RDF.Description` about the given subject.
The created `RDF.Description` can be initialized with any form of data which
`add/2` understands with the `:init` option. Additionally a function returning
the initialization data in any of these forms can be as the `:init` value.
## Examples
RDF.Description.new(EX.S)
RDF.Description.new(EX.S, init: {EX.S, EX.p, EX.O})
RDF.Description.new(EX.S, init: {EX.p, [EX.O1, EX.O2]})
RDF.Description.new(EX.S, init: [{EX.p1, EX.O1}, {EX.p2, EX.O2}])
RDF.Description.new(EX.S, init: RDF.Description.new(EX.S, init: {EX.P, EX.O}))
RDF.Description.new(EX.S, init: fn -> {EX.p, EX.O} end)
"""
@spec new(Statement.coercible_subject() | t, keyword) :: t
def new(subject, opts \\ [])
def new(%__MODULE__{} = description, opts), do: new(description.subject, opts)
def new(subject, opts) do
{data, opts} = Keyword.pop(opts, :init)
%__MODULE__{subject: RDF.coerce_subject(subject)}
|> init(data, opts)
end
defp init(description, nil, _), do: description
defp init(description, fun, opts) when is_function(fun), do: add(description, fun.(), opts)
defp init(description, data, opts), do: add(description, data, opts)
@doc """
Returns the subject IRI or blank node of a description.
"""
@spec subject(t) :: Statement.subject()
def subject(%__MODULE__{} = description), do: description.subject
@doc """
Changes the subject of a description.
"""
@spec change_subject(t, Statement.coercible_subject()) :: t
def change_subject(%__MODULE__{} = description, new_subject) do
%__MODULE__{description | subject: RDF.coerce_subject(new_subject)}
end
@doc """
Add statements to a `RDF.Description`.
Note: When the statements to be added are given as another `RDF.Description`,
the subject must not match subject of the description to which the statements
are added. As opposed to that `RDF.Data.merge/2` will produce a `RDF.Graph`
containing both descriptions.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.P1, EX.O1})
...> |> RDF.Description.add({EX.P2, EX.O2})
RDF.Description.new(EX.S, init: [{EX.P1, EX.O1}, {EX.P2, EX.O2}])
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O1})
...> |> RDF.Description.add({EX.P, [EX.O2, EX.O3]})
RDF.Description.new(EX.S, init: [{EX.P, EX.O1}, {EX.P, EX.O2}, {EX.P, EX.O3}])
"""
@spec add(t, input, keyword) :: t
def add(description, input, opts \\ [])
def add(%__MODULE__{} = description, {subject, predicate, objects, _}, opts) do
add(description, {subject, predicate, objects}, opts)
end
def add(%__MODULE__{} = description, {subject, predicate, objects}, opts) do
if RDF.coerce_subject(subject) == description.subject do
add(description, {predicate, objects}, opts)
else
description
end
end
def add(%__MODULE__{} = description, {predicate, objects}, opts) do
normalized_objects =
objects
|> List.wrap()
|> Map.new(&{RDF.coerce_object(&1), nil})
if Enum.empty?(normalized_objects) do
description
else
%__MODULE__{
description
| predications:
Map.update(
description.predications,
RDF.coerce_predicate(predicate, PropertyMap.from_opts(opts)),
normalized_objects,
&Map.merge(&1, normalized_objects)
)
}
end
end
# This implementation is actually unnecessary as the implementation with the is_map clause
# would work perfectly fine with RDF.Descriptions Enumerable implementation.
# It exists only for performance reasons, since this version is roughly twice as fast.
def add(%__MODULE__{} = description, %__MODULE__{} = input_description, _opts) do
%__MODULE__{
description
| predications:
Map.merge(
description.predications,
input_description.predications,
fn _predicate, objects, new_objects ->
Map.merge(objects, new_objects)
end
)
}
end
if Version.match?(System.version(), "~> 1.10") do
def add(description, input, opts)
when is_list(input) or (is_map(input) and not is_struct(input)) do
Enum.reduce(input, description, &add(&2, &1, opts))
end
else
def add(_, %_{}, _), do: raise(ArgumentError, "structs are not allowed as input")
def add(description, input, opts) when is_list(input) or is_map(input) do
Enum.reduce(input, description, &add(&2, &1, opts))
end
end
@doc """
Adds statements to a `RDF.Description` and overwrites all existing statements with already used predicates.
Note: As it is a destructive function this function is more strict in its handling of
`RDF.Description`s than `add/3`. The subject of a `RDF.Description` to be put must
match. If you want to overwrite existing statements with those from the description of
another subject, you'll have to explicitly change the subject with `change_subject/2`
first before using `put/3`.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O1})
...> |> RDF.Description.put({EX.P, EX.O2})
RDF.Description.new(EX.S, init: {EX.P, EX.O2})
"""
@spec put(t, input, keyword) :: t
def put(description, input, opts \\ [])
def put(
%__MODULE__{subject: subject} = description,
%__MODULE__{subject: subject} = input,
_opts
) do
%__MODULE__{
description
| predications:
Enum.reduce(
input.predications,
description.predications,
fn {predicate, objects}, predications ->
Map.put(predications, predicate, objects)
end
)
}
end
def put(%__MODULE__{} = description, %__MODULE__{}, _opts), do: description
def put(%__MODULE__{} = description, input, opts) do
put(description, description.subject |> new() |> add(input, opts), opts)
end
@doc """
Deletes statements from a `RDF.Description`.
Note: When the statements to be deleted are given as another `RDF.Description`,
the subject must not match subject of the description from which the statements
are deleted. If you want to delete only a matching description subject, you can
use `RDF.Data.delete/2`.
"""
@spec delete(t, input, keyword) :: t
def delete(description, input, opts \\ [])
def delete(%__MODULE__{} = description, {subject, predicate, objects}, opts) do
if RDF.coerce_subject(subject) == description.subject do
delete(description, {predicate, objects}, opts)
else
description
end
end
def delete(%__MODULE__{} = description, {subject, predicate, objects, _}, opts) do
delete(description, {subject, predicate, objects}, opts)
end
def delete(%__MODULE__{} = description, {predicate, objects}, opts) do
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(&RDF.coerce_object/1)
rest = Map.drop(current_objects, normalized_objects)
%__MODULE__{
description
| predications:
if Enum.empty?(rest) do
Map.delete(description.predications, predicate)
else
Map.put(description.predications, predicate, rest)
end
}
else
description
end
end
# This implementation is actually unnecessary as the implementation with the is_map clause
# would work perfectly fine with RDF.Descriptions Enumerable implementation.
# It exists only for performance reasons.
def delete(%__MODULE__{} = description, %__MODULE__{} = input_description, _opts) do
predications = description.predications
%__MODULE__{
description
| predications:
Enum.reduce(
input_description.predications,
predications,
fn {predicate, objects}, predications ->
if current_objects = Map.get(description.predications, predicate) do
rest = Map.drop(current_objects, Map.keys(objects))
if Enum.empty?(rest) do
Map.delete(predications, predicate)
else
Map.put(predications, predicate, rest)
end
else
predications
end
end
)
}
end
if Version.match?(System.version(), "~> 1.10") do
def delete(description, input, opts)
when is_list(input) or (is_map(input) and not is_struct(input)) do
Enum.reduce(input, description, &delete(&2, &1, opts))
end
else
def delete(_, %_{}, _), do: raise(ArgumentError, "structs are not allowed as input")
def delete(description, input, opts) when is_list(input) or is_map(input) do
Enum.reduce(input, description, &delete(&2, &1, opts))
end
end
@doc """
Deletes all statements with the given properties.
"""
@spec delete_predicates(t, Statement.coercible_predicate() | [Statement.coercible_predicate()]) ::
t
def delete_predicates(description, properties)
def delete_predicates(%__MODULE__{} = description, properties) when is_list(properties) do
Enum.reduce(properties, description, &delete_predicates(&2, &1))
end
def delete_predicates(%__MODULE__{} = description, property) do
%__MODULE__{
description
| predications: Map.delete(description.predications, RDF.coerce_predicate(property))
}
end
@doc """
Fetches the objects for the given predicate of a Description.
When the predicate can not be found `:error` is returned.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.p, EX.O}) |> RDF.Description.fetch(EX.p)
{:ok, [RDF.iri(EX.O)]}
iex> RDF.Description.new(EX.S, init: [{EX.P, EX.O1}, {EX.P, EX.O2}])
...> |> RDF.Description.fetch(EX.P)
{:ok, [RDF.iri(EX.O1), RDF.iri(EX.O2)]}
iex> RDF.Description.new(EX.S) |> RDF.Description.fetch(EX.foo)
:error
"""
@impl Access
@spec fetch(t, Statement.coercible_predicate()) :: {:ok, [Statement.object()]} | :error
def fetch(%__MODULE__{} = description, predicate) do
with {:ok, objects} <-
Access.fetch(description.predications, RDF.coerce_predicate(predicate)) do
{:ok, Map.keys(objects)}
end
end
@doc """
Gets the objects for the given predicate of a Description.
When the predicate can not be found, the optionally given default value or `nil` is returned.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O}) |> RDF.Description.get(EX.P)
[RDF.iri(EX.O)]
iex> RDF.Description.new(EX.S) |> RDF.Description.get(EX.foo)
nil
iex> RDF.Description.new(EX.S) |> RDF.Description.get(EX.foo, :bar)
:bar
"""
@spec get(t, Statement.coercible_predicate(), any) :: [Statement.object()] | any
def get(%__MODULE__{} = description, predicate, default \\ nil) do
case fetch(description, predicate) do
{:ok, value} -> value
:error -> default
end
end
@doc """
Gets a single object for the given predicate of a Description.
When the predicate can not be found, the optionally given default value or `nil` is returned.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O}) |> RDF.Description.first(EX.P)
RDF.iri(EX.O)
iex> RDF.Description.new(EX.S) |> RDF.Description.first(EX.foo)
nil
"""
@spec first(t, Statement.coercible_predicate()) :: Statement.object() | nil
def first(%__MODULE__{} = description, predicate) do
description
|> get(predicate, [])
|> List.first()
end
@doc """
Updates the objects of the `predicate` in `description` with the given function.
If `predicate` is present in `description` with `objects` as value,
`fun` is invoked with argument `objects` and its result is used as the new
list of objects of `predicate`. If `predicate` is not present in `description`,
`initial` is inserted as the objects of `predicate`. The initial value will
not be passed through the update function.
The initial value and the returned objects by the update function will automatically
coerced to proper RDF object values before added.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.p, EX.O})
...> |> RDF.Description.update(EX.p, fn objects -> [EX.O2 | objects] end)
RDF.Description.new(EX.S, init: [{EX.p, EX.O}, {EX.p, EX.O2}])
iex> RDF.Description.new(EX.S)
...> |> RDF.Description.update(EX.p, EX.O, fn _ -> EX.O2 end)
RDF.Description.new(EX.S, init: {EX.p, EX.O})
"""
@spec update(
t,
Statement.coercible_predicate(),
Statement.coercible_object() | nil,
([Statement.Object] -> [Statement.Object])
) :: t
def update(%__MODULE__{} = description, predicate, initial \\ nil, fun) do
predicate = RDF.coerce_predicate(predicate)
case get(description, predicate) do
nil when is_nil(initial) ->
description
nil ->
put(description, {predicate, initial})
objects ->
objects
|> fun.()
|> List.wrap()
|> case do
[] -> delete_predicates(description, predicate)
objects -> put(description, {predicate, objects})
end
end
end
@doc """
Gets and updates the objects of the given predicate of a Description, in a single pass.
Invokes the passed function on the objects of the given predicate; this
function should return either `{objects_to_return, new_object}` or `:pop`.
If the passed function returns `{objects_to_return, new_objects}`, the return
value of `get_and_update` is `{objects_to_return, new_description}` where
`new_description` is the input `Description` updated with `new_objects` for
the given predicate.
If the passed function returns `:pop` the objects for the given predicate are
removed and a `{removed_objects, new_description}` tuple gets returned.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O})
...> |> RDF.Description.get_and_update(EX.P, fn current_objects ->
...> {current_objects, EX.New}
...> end)
{[RDF.iri(EX.O)], RDF.Description.new(EX.S, init: {EX.P, EX.New})}
iex> RDF.Graph.new([{EX.S, EX.P1, EX.O1}, {EX.S, EX.P2, EX.O2}])
...> |> RDF.Graph.description(EX.S)
...> |> RDF.Description.get_and_update(EX.P1, fn _ -> :pop end)
{[RDF.iri(EX.O1)], RDF.Description.new(EX.S, init: {EX.P2, EX.O2})}
"""
@impl Access
@spec get_and_update(
t,
Statement.coercible_predicate(),
([Statement.Object] -> {[Statement.Object], t} | :pop)
) :: {[Statement.Object], t}
def get_and_update(%__MODULE__{} = description, predicate, fun) do
triple_predicate = RDF.coerce_predicate(predicate)
case fun.(get(description, triple_predicate)) do
{objects_to_return, new_objects} ->
{objects_to_return, put(description, {triple_predicate, new_objects})}
:pop ->
pop(description, triple_predicate)
end
end
@doc """
Pops an arbitrary triple from a `RDF.Description`.
"""
@spec pop(t) :: {Triple.t() | [Statement.Object] | nil, t}
def pop(description)
def pop(%__MODULE__{predications: predications} = description)
when map_size(predications) == 0,
do: {nil, description}
def pop(%__MODULE__{predications: predications} = description) do
[{predicate, objects}] = Enum.take(predications, 1)
[{object, _}] = Enum.take(objects, 1)
popped =
if Enum.count(objects) == 1,
do: elem(Map.pop(predications, predicate), 1),
else: elem(pop_in(predications, [predicate, object]), 1)
{
{description.subject, predicate, object},
%__MODULE__{description | predications: popped}
}
end
@doc """
Pops the objects of the given predicate of a Description.
When the predicate can not be found the optionally given default value or `nil` is returned.
## Examples
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O})
...> |> RDF.Description.pop(EX.P)
{[RDF.iri(EX.O)], RDF.Description.new(EX.S)}
iex> RDF.Description.new(EX.S, init: {EX.P, EX.O})
...> |> RDF.Description.pop(EX.Missing)
{nil, RDF.Description.new(EX.S, init: {EX.P, EX.O})}
"""
@impl Access
def pop(%__MODULE__{} = description, predicate) do
case Access.pop(description.predications, RDF.coerce_predicate(predicate)) do
{nil, _} ->
{nil, description}
{objects, new_predications} ->
{
Map.keys(objects),
%__MODULE__{description | predications: new_predications}
}
end
end
@doc """
The set of all properties used in the predicates within a `RDF.Description`.
## Examples
iex> RDF.Description.new(EX.S1, init: [
...> {EX.p1, EX.O1},
...> {EX.p2, EX.O2},
...> {EX.p2, EX.O3}])
...> |> RDF.Description.predicates()
MapSet.new([EX.p1, EX.p2])
"""
@spec predicates(t) :: MapSet.t()
def predicates(%__MODULE__{} = description) do
description.predications |> Map.keys() |> MapSet.new()
end
@doc """
The set of all resources used in the objects within a `RDF.Description`.
Note: This function does collect only IRIs and BlankNodes, not Literals.
## Examples
iex> RDF.Description.new(EX.S1, init: [
...> {EX.p1, EX.O1},
...> {EX.p2, EX.O2},
...> {EX.p3, EX.O2},
...> {EX.p4, RDF.bnode(:bnode)},
...> {EX.p3, "foo"}])
...> |> RDF.Description.objects()
MapSet.new([RDF.iri(EX.O1), RDF.iri(EX.O2), RDF.bnode(:bnode)])
"""
@spec objects(t) :: MapSet.t()
def objects(%__MODULE__{} = description) do
objects(description, &RDF.resource?/1)
end
@doc """
The set of all resources used in the objects within a `RDF.Description` satisfying the given filter criterion.
"""
@spec objects(t, (Statement.object() -> boolean)) :: MapSet.t()
def objects(%__MODULE__{} = description, filter_fn) do
Enum.reduce(description.predications, MapSet.new(), fn {_, objects}, acc ->
objects
|> Map.keys()
|> Enum.filter(filter_fn)
|> MapSet.new()
|> MapSet.union(acc)
end)
end
@doc """
The set of all resources used within a `RDF.Description`.
## Examples
iex> RDF.Description.new(EX.S1, init: [
...> {EX.p1, EX.O1},
...> {EX.p2, EX.O2},
...> {EX.p1, EX.O2},
...> {EX.p2, RDF.bnode(:bnode)},
...> {EX.p3, "foo"}])
...> |> RDF.Description.resources()
MapSet.new([RDF.iri(EX.O1), RDF.iri(EX.O2), RDF.bnode(:bnode), EX.p1, EX.p2, EX.p3])
"""
@spec resources(t) :: MapSet.t()
def resources(%__MODULE__{} = description) do
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.
The default value of the `:filter_star` flag is `false`.
"""
@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, opts \\ []), to: __MODULE__, as: :triples
@doc """
Returns the number of statements of a `RDF.Description`.
"""
@spec statement_count(t) :: non_neg_integer
def statement_count(%__MODULE__{} = description) do
Enum.reduce(description.predications, 0, fn {_, objects}, count ->
count + Enum.count(objects)
end)
end
defdelegate count(description), to: __MODULE__, as: :statement_count
@doc """
Checks if the given `input` statements exist within `description`.
"""
@spec include?(t, input, keyword) :: boolean
def include?(description, input, opts \\ [])
def include?(%__MODULE__{} = description, {subject, predicate, objects}, opts) do
RDF.coerce_subject(subject) == description.subject &&
include?(description, {predicate, objects}, opts)
end
def include?(%__MODULE__{} = description, {subject, predicate, objects, _}, opts) do
include?(description, {subject, predicate, objects}, opts)
end
def include?(%__MODULE__{} = description, {predicate, objects}, opts) do
if existing_objects =
description.predications[RDF.coerce_predicate(predicate, PropertyMap.from_opts(opts))] do
objects
|> List.wrap()
|> Enum.map(&RDF.coerce_object/1)
|> Enum.all?(fn object -> Map.has_key?(existing_objects, object) end)
else
false
end
end
def include?(
%__MODULE__{subject: subject, predications: predications},
%__MODULE__{subject: subject} = input,
_opts
) do
Enum.all?(input.predications, fn {predicate, objects} ->
if existing_objects = predications[predicate] do
Enum.all?(objects, fn {object, _} ->
Map.has_key?(existing_objects, object)
end)
else
false
end
end)
end
def include?(%__MODULE__{}, %__MODULE__{}, _), do: false
if Version.match?(System.version(), "~> 1.10") do
def include?(description, input, opts)
when is_list(input) or (is_map(input) and not is_struct(input)) do
Enum.all?(input, &include?(description, &1, opts))
end
else
def include?(_, %_{}, _), do: raise(ArgumentError, "structs are not allowed as input")
def include?(description, input, opts) when is_list(input) or is_map(input) do
Enum.all?(input, &include?(description, &1, opts))
end
end
@doc """
Checks if a `RDF.Description` has the given resource as subject.
## Examples
iex> RDF.Description.new(EX.S1, init: {EX.p1, EX.O1})
...> |> RDF.Description.describes?(EX.S1)
true
iex> RDF.Description.new(EX.S1, init: {EX.p1, EX.O1})
...> |> RDF.Description.describes?(EX.S2)
false
"""
@spec describes?(t, Statement.subject()) :: boolean
def describes?(%__MODULE__{subject: subject}, other_subject) do
subject == RDF.coerce_subject(other_subject)
end
@doc """
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
`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`.
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"})
...> |> RDF.Description.values()
%{"http://example.com/p" => ["Foo"]}
iex> RDF.Description.new(~I<http://example.com/S>, init: {~I<http://example.com/p>, ~L"Foo"})
...> |> RDF.Description.values(context: %{p: ~I<http://example.com/p>})
%{p: ["Foo"]}
"""
@spec values(t, keyword) :: map
def values(%__MODULE__{} = description, opts \\ []) do
if property_map = PropertyMap.from_opts(opts) do
map(description, RDF.Statement.default_property_mapping(property_map))
else
map(description, &RDF.Statement.default_term_mapping/1)
end
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.
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"})
...> |> RDF.Description.map(fn
...> {:predicate, predicate} ->
...> predicate
...> |> to_string()
...> |> String.split("/")
...> |> List.last()
...> |> String.to_atom()
...> {_, term} ->
...> RDF.Term.value(term)
...> end)
%{p: ["Foo"]}
"""
@spec map(t, Statement.term_mapping()) :: map
def map(description, fun)
def map(%__MODULE__{} = description, fun) do
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
@doc """
Creates a description from another one by limiting its statements to those using one of the given `predicates`.
If `predicates` contains properties that are not used in the `description`, they're simply ignored.
If `nil` is passed, the description is left untouched.
"""
@spec take(t, [Statement.coercible_predicate()] | Enum.t() | nil) :: t
def take(description, predicates)
def take(%__MODULE__{} = description, nil), do: description
def take(%__MODULE__{} = description, predicates) do
%__MODULE__{
description
| predications:
Map.take(description.predications, Enum.map(predicates, &RDF.coerce_predicate/1))
}
end
@doc """
Removes all objects from a description which are quoted triples.
"""
@spec without_quoted_triples(t) :: t
def without_quoted_triples(%__MODULE__{} = description) do
%__MODULE__{
description
| predications:
Enum.reduce(description.predications, description.predications, fn
{predicate, objects}, predications ->
original_object_count = Enum.count(predications)
filtered_objects =
Enum.reject(objects, &match?({quoted_triple, _} when is_tuple(quoted_triple), &1))
case Enum.count(filtered_objects) do
0 -> Map.delete(predications, predicate)
^original_object_count -> predications
_ -> Map.put(predications, predicate, Map.new(filtered_objects))
end
end)
}
end
@doc """
Checks if two `RDF.Description`s are equal.
Two `RDF.Description`s are considered to be equal if they contain the same triples.
"""
@spec equal?(t, t) :: boolean
def equal?(description1, description2)
def equal?(%__MODULE__{} = description1, %__MODULE__{} = description2) do
description1 == description2
end
def equal?(_, _), do: false
defimpl Enumerable do
alias RDF.Description
def member?(desc, triple), do: {:ok, Description.include?(desc, triple)}
def count(desc), do: {:ok, Description.statement_count(desc)}
if Version.match?(System.version(), "~> 1.10") do
def slice(desc) do
size = Description.statement_count(desc)
{:ok, size, &Enumerable.List.slice(Description.triples(desc), &1, &2, size)}
end
else
def slice(_), do: {:error, __MODULE__}
end
def reduce(desc, acc, fun) do
desc
|> Description.triples()
|> Enumerable.List.reduce(acc, fun)
end
end
defimpl Collectable do
alias RDF.Description
def into(original) do
collector_fun = fn
description, {:cont, list} when is_list(list) ->
Description.add(description, List.to_tuple(list))
description, {:cont, elem} ->
Description.add(description, elem)
description, :done ->
description
_description, :halt ->
:ok
end
{original, collector_fun}
end
end
end