defmodule RTC.Compound do
@moduledoc """
A struct representing an RDF Triple Compound.
An RDF Triple Compound is a set of triples embedded in an RDF graph.
You can create such a set of triples either from scratch with the `new/1`
function, or you can load an already existing compound from an RDF graph with
the `from_rdf/2` function.
You can then use the various functions on this module to get its triples,
sub-compounds, super-compounds and annotations or edit them.
Finally, you can get back the RDF form of the compound with `to_rdf/2`.
If you only want an RDF graph of the contained triples (without the annotations),
you can use the `graph/1` function.
## Asserted and unasserted triples
A compound can contain both asserted and unasserted triples. When creating a
compound with initial triples with the `new/3` function or adding triples with
`add/3` you can specify in which assertion mode the given triples should be
interpreted with the `:assertion_mode` option and one of the values `:asserted`
or `:unasserted`. By default, `:asserted` is assumed, but you can configure the
default value in your application with the following configuration
on your `config.exs` files:
config :rtc, :assertion_mode, :unasserted
All query functions operating over the set of triples and the `delete/3`
and `delete_description/3` functions also support an `:assertion_mode` which
defines which triples should be considered with the following supported
values:
- `:all` (default): consider both asserted and unasserted triples
- `:asserted`: only consider asserted triples
- `:unasserted`: only consider unasserted triples
## Auto-generated ids
Various functions can be used in such a way, that they will create compounds with
a proper resource identifier implicitly. By default, they will create a random blank
node, but the identifier creation behavior can be configured and customized via
`RDF.Resource.Generator`s.
For example, to create UUIDv4 URIs instead, you could use this configuration in your
`config.exs`:
config :rtc, :id,
generator: RDF.IRI.UUID.Generator,
uuid_version: 4,
prefix: "http://example.com/ns/"
See [the guide on resource generators](https://rdf-elixir.dev/rdf-ex/resource-generators.html)
for more information and available generators.
"""
alias RDF.{Statement, Triple, Description, Graph, BlankNode}
# we have no explicit id field, since we're using the subject of the annotations for this
@enforce_keys [:asserted, :annotations]
defstruct annotations: nil,
asserted: nil,
unasserted: Graph.new(),
super_compounds: %{},
sub_compounds: %{}
@type id :: RDF.Resource.t()
@type coercible_id :: Statement.coercible_subject()
@type t :: %__MODULE__{
annotations: Description.t(),
asserted: Graph.t(),
unasserted: Graph.t(),
super_compounds: %{id => Description.t()},
sub_compounds: %{id => t()}
}
@doc """
Returns the default value of the `:element_style` option of `to_rdf/2`.
This value can be configured in your application with the following configuration
on your `config.exs` files:
config :rtc, :element_style, :elements
When no `:element_style` is specified, the `:element_of` style is used by default.
"""
def default_element_style, do: Application.get_env(:rtc, :element_style, :element_of)
@doc """
Returns the default value of the `:assertion_mode` option.
This value can be configured in your application with the following configuration
on your `config.exs` files:
config :rtc, :assertion_mode, :unasserted
When no `:assertion_mode` is specified, the `:asserted` style is used by default.
"""
def default_assertion_mode, do: Application.get_env(:rtc, :assertion_mode, :asserted)
@doc """
Creates a empty compound with an automatically generated id.
See the module documentation for information on auto-generated ids.
"""
@spec new :: t
def new, do: new([])
@doc """
Creates a compound with the given set of triples and an automatically generated id.
If you want to define the id yourself, use `new/2` or `new/3`.
See the module documentation for information on auto-generated ids.
Triples can be provided in any form accepted by `RDF.Graph.new/2`.
When a list of triples is given which contains nested lists of triples,
sub-compounds with auto-generated ids are generated and added for each
of the nested lists.
"""
@spec new(Graph.input()) :: t
def new(triples), do: new(triples, [])
@doc """
Creates a new compound with the given set of triples and the given id.
Triples can be provided in any form accepted by `RDF.Graph.new/2`.
When a list of triples is given which contains nested lists of triples,
sub-compounds with auto-generated ids are generated and added for each
of the nested lists.
If a keyword list is given as the second argument, an id is generated and
delegated to `new/3`. See `new/3` for a description of the available options.
See the module documentation for information on auto-generated ids.
"""
@spec new(Graph.input(), coercible_id() | keyword) :: t
def new(triples, opts) when is_list(opts), do: new(triples, RTC.id(), opts)
def new(triples, compound_id), do: new(triples, compound_id, [])
@doc """
Creates a new compound with the given set of triples and the given id.
Triples can be provided in any form accepted by `RDF.Graph.new/2`.
When a list of triples is given which contains nested lists of triples,
sub-compounds with auto-generated ids are generated and added for each
of the nested lists.
Alternatively, the `sub_compounds` option can be used to provide one or
a list of sub-compounds to be added.
Available options:
- `:assertion_mode`: the assertion mode to be used for the `triples` to be added
(see module documentation section on "Asserted and unasserted triples")
- `:name`: the name of the graph which gets returned by `graph/1` and
`to_rdf/2` (by default, the compound id is used as the graph name or `nil`
when the compound id is a blank node)
- `:prefixes`: the prefix mappings which should used by default when an `RDF.Graph`
is produced from this compound, e.g. with `to_rdf/2`, `graph/2` etc.
Note, that a `rtc` prefix for the RTC vocabulary is added by `to_rdf/2` in
any case, so it usually not required to be defined here, unless you want it
to be added also to the graphs with the element triples like `graph/2`.
- `:base_iri`: a base IRI which should be stored alongside the graph
and will be used for example when serializing in a format with base IRI support
- `:annotations`: allows to set the initial annotations of the compound
as an `RDF.Description` or anything an `RDF.Description` can be built
from. The subject in the input is ignored, so that the `RDF.Description`
of any resource can be used as a blueprint.
- `:sub_compounds`: a single sub-compound or multiple sub-compounds as a list
- `:super_compounds`: a single compound id of a super-compound or multiple
compound ids as a list. One or multiple compounds or `RDF.Description`s of the
annotations can be provided also, in which case only the ids and the annotations
are stored.
"""
@spec new(Graph.input(), coercible_id(), keyword) :: t
def new(triples, compound_id, opts) do
assertion_mode = assertion_mode(opts)
sub_compounds = normalize_sub_compounds(opts)
{triples, sub_compounds} = decompose_nested_triples(triples, sub_compounds, assertion_mode)
{asserted, unasserted} =
case assertion_mode do
:asserted -> {triples, []}
:unasserted -> {[], triples}
end
%__MODULE__{
asserted: init_graph(asserted, compound_id, opts),
unasserted: Graph.new(unasserted),
sub_compounds: Map.new(sub_compounds, &{id(&1), &1}),
annotations: new_annotation(compound_id, Keyword.get(opts, :annotations))
}
|> put_super_compound(Keyword.get(opts, :super_compounds, []))
end
defp normalize_sub_compounds(opts) do
opts
|> Keyword.get(:sub_compounds)
|> List.wrap()
|> ensure_all_compounds!()
end
defp ensure_all_compounds!(compounds, acc \\ [])
defp ensure_all_compounds!([], acc), do: acc
defp ensure_all_compounds!([%__MODULE__{} = compound | rest], acc),
do: ensure_all_compounds!(rest, [compound | acc])
defp ensure_all_compounds!([non_compound | _], _),
do: raise(ArgumentError, "#{inspect(non_compound)} is not a compound")
defp decompose_nested_triples(triples, sub_compounds, assertion_mode) when is_list(triples) do
Enum.reduce(triples, {Graph.new(), List.wrap(sub_compounds)}, fn
nested_triples, {graph, sub_compounds} when is_list(nested_triples) ->
{graph, [new(nested_triples, assertion_mode: assertion_mode) | sub_compounds]}
triple, {graph, sub_compounds} ->
{Graph.add(graph, triple), sub_compounds}
end)
end
defp decompose_nested_triples(triples, sub_compounds, _),
do: {Graph.new(triples), sub_compounds}
defp init_graph(triples, compound_id, opts) do
opts = Keyword.put_new(opts, :name, graph_name(compound_id))
Graph.new(triples, opts)
end
defp graph_name(%BlankNode{}), do: nil
defp graph_name(id), do: id
defp assertion_mode(opts) do
case Keyword.get(opts, :assertion_mode, default_assertion_mode()) do
:asserted -> :asserted
:unasserted -> :unasserted
v -> raise ArgumentError, "unexpected value for :assertion_mode opt: #{inspect(v)}"
end
end
defp assertion_mode_with_all(opts) do
case Keyword.get(opts, :assertion_mode, :all) do
:all -> :all
:asserted -> :asserted
:unasserted -> :unasserted
v -> raise ArgumentError, "unexpected value for :assertion_mode opt: #{inspect(v)}"
end
end
defp new_annotation(compound_id, nil), do: RDF.description(compound_id)
defp new_annotation(compound_id, description),
do: RDF.description(compound_id, init: description)
@doc """
Returns the id of the given `compound`.
"""
@spec id(t) :: id()
def id(%__MODULE__{} = compound), do: compound.annotations.subject
# for compatibility with RDF.Graph API
defdelegate name(compound), to: __MODULE__, as: :id
@doc """
Sets a new id on the given `compound`.
"""
@spec reset_id(t, coercible_id()) :: t()
def reset_id(%__MODULE__{} = compound, id) do
%{
compound
| asserted: Graph.change_name(compound.asserted, graph_name(id)),
annotations: Description.change_subject(compound.annotations, id)
}
end
# for compatibility with the RDF.Graph API
defdelegate change_name(compound, name), to: __MODULE__, as: :reset_id
@doc """
Retrieves the compound with the given `compound_id` from a `RDF.Graph`.
When no compound with the given `compound_id` can be found in the given
`graph`, an empty compound is returned.
"""
@spec from_rdf(Graph.t(), coercible_id()) :: t
def from_rdf(%Graph{} = graph, compound_id) do
do_from_rdf(graph, Statement.coerce_subject(compound_id), [])
end
defp do_from_rdf(graph, compound_id, super_compound_ids) do
if compound_id in super_compound_ids do
raise("circle in sub-compound #{compound_id}")
end
{elements, annotations} =
graph
|> Graph.description(compound_id)
|> Description.pop(RTC.elements())
element_ofs =
graph
|> Graph.query({:triple?, RTC.elementOf(), compound_id})
|> Enum.map(&Map.get(&1, :triple))
{super_compounds, annotations} = Description.pop(annotations, RTC.subCompoundOf())
super_compounds =
super_compounds_from_graph(graph, List.wrap(super_compounds) -- super_compound_ids)
sub_compounds =
graph
|> Graph.query({:sub_compound?, RTC.subCompoundOf(), compound_id})
|> Enum.map(
&do_from_rdf(graph, Map.get(&1, :sub_compound), [compound_id | super_compound_ids])
)
{asserted, unasserted} =
Enum.split_with(List.wrap(elements) ++ element_ofs, &Graph.include?(graph, &1))
asserted
|> new(compound_id,
sub_compounds: sub_compounds,
super_compounds: super_compounds,
annotations: annotations
)
|> add(unasserted, assertion_mode: :unasserted)
end
defp super_compounds_from_graph(graph, super_compound_ids) when is_list(super_compound_ids) do
Enum.map(super_compound_ids, &super_compounds_from_graph(graph, &1))
end
defp super_compounds_from_graph(graph, super_compound_id) do
do_super_compounds_from_graph(
graph,
super_compound_id,
Description.new(super_compound_id),
[]
)
end
defp do_super_compounds_from_graph(graph, super_compound_id, super_compound, super_compound_ids) do
if super_compound_id in super_compound_ids do
raise("circle in sub-compound #{super_compound_id}")
end
super_compound_ids = [super_compound_id | super_compound_ids]
{new_super_compound_ids, annotations} =
if description = graph[super_compound_id] do
description
|> Description.delete_predicates(RTC.elements())
|> Description.pop(RTC.subCompoundOf())
else
{[], []}
end
annotations = Description.add(super_compound, annotations)
if new_super_compound_ids do
Enum.reduce(
new_super_compound_ids,
annotations,
&do_super_compounds_from_graph(graph, &1, &2, super_compound_ids)
)
else
annotations
end
end
if Code.ensure_loaded?(SPARQL.Client) do
@doc """
Retrieves the compound with the given `compound_id` from a SPARQL endpoint.
This function is only available when the `sparql_client` dependency is
added in your `Mixfile`.
When no compound with the given `compound_id` can be found in the given
`graph`, an empty compound is returned.
This function requests the execution of a SPARQL `CONSTRUCT` query without any
further options on the respective `SPARQL.Client` call. You can configure the
options to be used on this request with the `:from_sparql_opts` configuration
on your `config.exs` files, e.g.
config :rtc, :from_sparql_opts,
accept_header: "application/x-turtlestar",
result_format: :turtle
See `SPARQL.Client` for available options.
"""
@spec from_sparql(String.t(), coercible_id()) :: {:ok, t()}
defdelegate from_sparql(endpoint, compound_id), to: RTC.SPARQL, as: :from_endpoint
@doc """
Retrieves the compound with the given `compound_id` from a SPARQL endpoint.
This function is only available when the `sparql_client` dependency is
added in your `Mixfile`.
The `opts` are used as the options for the `CONSTRUCT` query call by the
`SPARQL.Client` against the endpoint. See `SPARQL.Client` for
available options.
When no compound with the given `compound_id` can be found in the given
`graph`, an empty compound is returned.
"""
@spec from_sparql(String.t(), coercible_id(), keyword) :: {:ok, t()} | {:error, any}
defdelegate from_sparql(endpoint, compound_id, opts), to: RTC.SPARQL, as: :from_endpoint
@doc """
Retrieves the compound with the given `compound_id` from a SPARQL endpoint.
As opposed to `from_sparql/3` this function returns the result directly and
raises an error when the SPARQL client call fails.
"""
@spec from_sparql!(String.t(), coercible_id(), keyword) :: t()
def from_sparql!(endpoint, compound_id, opts \\ []) do
case from_sparql(endpoint, compound_id, opts) do
{:ok, results} -> results
{:error, error} -> raise error
end
end
end
@doc """
Creates an RDF-star graph of the given compound with all RTC annotations.
The style for how the assignments of the triples are encoded can be specified
with the `:element_style` keyword and one the following values:
- `:element_of`: Uses the `rtc:elementOf` property to assign each of the triples
individually to the compound. This has the benefit that in the Turtle-star
serializations the [annotation syntax](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#annotation-syntax)
can be used, which doesn't require repeating the assertion and is therefore
more compact.
- `:elements`: Assigns the triples to the compound, by listing them as objects
of the inverse `rtc:elements` property. This has the benefit that the actual
assertion are not pervaded by annotations and the compound resource can be
serialized in an isolated and self-contained manner.
When no `:element_style` is specified, the configurable result of the
`default_element_style/0` function is used as the default.
The following options can be used to customize the returned graph:
- `:name`: the name of the graph to be created. By default, the compound id is
used as the graph name, unless a blank node is used as the compound id,
or the one specified with `:name` on `new/3`.
- `:prefixes`: some prefix mappings which should be added the graph
and will be used for example when serializing in a format with prefix support.
Defaults to the ones specified during the creation of the compound with `new/3`
or the `RDF.default_prefixes/0`. The `rtc` prefix for the RTC vocabulary is
added in any case, so it is not required to be defined here.
- `:base_iri`: a base IRI which should be stored alongside the graph
and will be used for example when serializing in a format with base IRI support
"""
@spec to_rdf(t, keyword) :: Graph.t()
def to_rdf(%__MODULE__{} = compound, opts \\ []) do
compound.asserted
|> apply_graph_opts(opts)
|> setup_to_rdf_graph_prefixes()
|> do_to_rdf(
compound,
Keyword.get(opts, :element_style, default_element_style()),
Keyword.get(opts, :include_super_compounds, false)
)
end
defp do_to_rdf(graph, compound, element_style, include_super_compounds) do
graph =
graph
|> annotate(compound, element_style)
|> Graph.add({id(compound), RTC.subCompoundOf(), super_compounds(compound)})
graph =
if include_super_compounds do
Enum.reduce(compound.super_compounds, graph, fn {_, description}, graph ->
Graph.add(graph, description)
end)
else
graph
end
Enum.reduce(compound.sub_compounds, graph, fn
{sub_compound_id, sub_compound}, acc_graph ->
acc_graph
|> Graph.add(sub_compound.asserted)
|> Graph.add({sub_compound_id, RTC.subCompoundOf(), id(compound)})
|> do_to_rdf(sub_compound, element_style, include_super_compounds)
end)
end
defp apply_graph_opts(graph, opts) do
graph
|> apply_option(opts, :name, &Graph.change_name(&1, &2))
|> apply_option(opts, :base_iri, &Graph.set_base_iri(&1, &2))
|> apply_option(opts, :prefixes, &Graph.add_prefixes(&1, &2))
end
defp apply_option(subject, opts, opt, fun) do
if Keyword.has_key?(opts, opt) do
fun.(subject, Keyword.get(opts, opt))
else
subject
end
end
defp setup_to_rdf_graph_prefixes(%Graph{prefixes: nil} = graph) do
%Graph{graph | prefixes: RDF.default_prefixes(rtc: RTC.NS.RTC)}
end
defp setup_to_rdf_graph_prefixes(graph) do
Graph.add_prefixes(graph, rtc: RTC.NS.RTC)
end
defp annotate(graph, compound, :element_of) do
graph
|> Graph.add(compound.annotations)
|> Graph.add_annotations(compound.asserted, {RTC.elementOf(), id(compound)})
|> Graph.add_annotations(compound.unasserted, {RTC.elementOf(), id(compound)})
end
defp annotate(graph, compound, :elements) do
Graph.add(
graph,
compound.annotations
|> RTC.elements(Graph.triples(compound.asserted))
|> RTC.elements(Graph.triples(compound.unasserted))
)
end
@doc """
Returns an RDF graph of the asserted triples in the compound (incl. its sub-compounds) without the annotations.
The following options can be used to customize the returned graph:
- `:name`: the name of the graph to be created
(by default, the compound id is used as the graph name, unless a blank node is used
as the compound id, or the one specified with `:name` on `new/3`)
- `:prefixes`: some prefix mappings which should be added the graph
and will be used for example when serializing in a format with prefix support.
Defaults to the ones specified during the creation of the compound with `new/3`.
- `:base_iri`: a base IRI which should be stored alongside the graph
and will be used for example when serializing in a format with base IRI support
"""
@spec asserted_graph(t, keyword) :: Graph.t()
def asserted_graph(%__MODULE__{} = compound, opts \\ []) do
compound.asserted
|> apply_graph_opts(opts)
|> Graph.add(do_asserted_graph(compound.sub_compounds))
end
defp do_asserted_graph(sub_compounds) do
Enum.flat_map(sub_compounds, fn {_, compound} ->
[compound.asserted | do_asserted_graph(compound.sub_compounds)]
end)
end
@doc """
Returns an RDF graph of the unasserted triples in the compound (incl. its sub-compounds) without the annotations.
The following options can be used to customize the returned graph:
- `:name`: the name of the graph to be created
(by default, the compound id is used as the graph name, unless a blank node is used
as the compound id, or the one specified with `:name` on `new/3`)
- `:prefixes`: some prefix mappings which should be added the graph
and will be used for example when serializing in a format with prefix support.
Defaults to the ones specified during the creation of the compound with `new/3`.
- `:base_iri`: a base IRI which should be stored alongside the graph
and will be used for example when serializing in a format with base IRI support
"""
@spec unasserted_graph(t, keyword) :: Graph.t()
def unasserted_graph(%__MODULE__{} = compound, opts \\ []) do
compound
|> unasserted_graph_with_proper_metadata()
|> apply_graph_opts(opts)
|> Graph.add(do_unasserted_graph(compound.sub_compounds))
end
defp do_unasserted_graph(sub_compounds) do
Enum.flat_map(sub_compounds, fn {_, compound} ->
[compound.unasserted | do_unasserted_graph(compound.sub_compounds)]
end)
end
defp unasserted_graph_with_proper_metadata(compound) do
%Graph{compound.asserted | descriptions: compound.unasserted.descriptions}
end
@doc """
Returns an RDF graph of all the asserted and unasserted triples in the compound (incl. its sub-compounds) without the annotations.
The following options can be used to customize the returned graph:
- `:name`: the name of the graph to be created
(by default, the compound id is used as the graph name, unless a blank node is used
as the compound id, or the one specified with `:name` on `new/3`)
- `:prefixes`: some prefix mappings which should be added the graph
and will be used for example when serializing in a format with prefix support.
Defaults to the ones specified during the creation of the compound with `new/3`.
- `:base_iri`: a base IRI which should be stored alongside the graph
and will be used for example when serializing in a format with base IRI support
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec graph(t, keyword) :: Graph.t()
def graph(%__MODULE__{} = compound, opts \\ []) do
case assertion_mode_with_all(opts) do
:all ->
compound
|> asserted_graph(opts)
|> Graph.add(unasserted_graph(compound, opts))
:asserted ->
asserted_graph(compound, opts)
:unasserted ->
unasserted_graph(compound, opts)
end
end
@doc """
Returns a list of the triples in the given `compound`.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec triples(t, keyword) :: [Triple.t()]
def triples(%__MODULE__{} = compound, opts \\ []) do
compound
|> graph(opts)
|> Graph.triples()
end
defdelegate statements(compound), to: __MODULE__, as: :triples
@doc """
Gets the description of the given `subject`.
When the subject can not be found the optionally given default value or
`nil` is returned.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}], EX.Compound)
...> |> RTC.Compound.get(EX.S1)
RDF.Description.new(EX.S1, init: {EX.P1, EX.O1})
iex> RTC.Compound.get(RTC.Compound.new(), EX.Foo)
nil
iex> RTC.Compound.get(RTC.Compound.new(), EX.Foo, :bar)
:bar
"""
@spec get(t, Statement.coercible_subject(), any, keyword) :: Description.t() | any
def get(compound, subject, default \\ nil, opts \\ []) do
assertion_mode = assertion_mode_with_all(opts)
case do_get(assertion_mode, compound, subject) ++
do_get_sub(assertion_mode, compound.sub_compounds, subject) do
[] -> default
[description] -> description
[description | rest] -> Enum.into(rest, description)
end
end
defp do_get(:asserted, compound, subject),
do: Graph.get(compound.asserted, subject) |> List.wrap()
defp do_get(:unasserted, compound, subject),
do: Graph.get(compound.unasserted, subject) |> List.wrap()
defp do_get(:all, compound, subject) do
do_get(:asserted, compound, subject) ++ do_get(:unasserted, compound, subject)
end
defp do_get_sub(assertion_mode, sub_compounds, subject) do
Enum.flat_map(sub_compounds, fn {_, compound} ->
do_get(assertion_mode, compound, subject) ++
do_get_sub(assertion_mode, compound.sub_compounds, subject)
end)
end
@doc """
Returns the description of the given subject.
When the subject can not be found an empty description is returned.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}])
...> |> RTC.Compound.description(EX.S1)
RDF.Description.new(EX.S1, init: {EX.P1, EX.O1})
iex> RTC.Compound.new([{EX.S, EX.P1, EX.O1}], sub_compounds: RTC.Compound.new([{EX.S, EX.P2, EX.O2}]))
...> |> RTC.Compound.description(EX.S)
RDF.Description.new(EX.S, init: [{EX.P1, EX.O1}, {EX.P2, EX.O2}])
iex> RTC.Compound.new([])
...> |> RTC.Compound.description(EX.S1)
RDF.Description.new(EX.S1)
"""
@spec description(t, Statement.coercible_subject(), keyword) :: Description.t() | nil
def description(%__MODULE__{} = compound, subject, opts \\ []) do
get(compound, subject, Description.new(subject), opts)
end
@doc """
Fetches the description of the given subject.
When the subject can not be found `:error` is returned.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}], EX.Compound)
...> |> RTC.Compound.fetch(EX.S1)
{:ok, RDF.Description.new(EX.S1, init: {EX.P1, EX.O1})}
iex> RTC.Compound.new() |> RTC.Compound.fetch(EX.foo)
:error
"""
@spec fetch(t, Statement.coercible_subject(), keyword) :: {:ok, Description.t()} | :error
def fetch(%__MODULE__{} = compound, subject, opts \\ []) do
case get(compound, subject, nil, opts) do
nil -> :error
description -> {:ok, description}
end
end
@doc """
Returns if the given `compound` (and of its sub-compounds) does not contain any triples.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec empty?(t, keyword) :: boolean
def empty?(%__MODULE__{} = compound, opts \\ []) do
assertion_mode = assertion_mode_with_all(opts)
(assertion_mode == :unasserted or Graph.empty?(compound.asserted)) and
(assertion_mode == :asserted or Graph.empty?(compound.unasserted)) and
Enum.all?(compound.sub_compounds, fn {_, sub_compound} -> empty?(sub_compound, opts) end)
end
@doc """
Returns the number of triples in the given `compound`.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec triple_count(t, keyword) :: non_neg_integer
def triple_count(%__MODULE__{} = compound, opts \\ []) do
compound
|> graph(opts)
|> Graph.triple_count()
end
defdelegate statement_count(compound), to: __MODULE__, as: :statement_count
@doc """
The set of all subjects used in the triples within a `RTC.Compound` or any of its sub-compounds.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([
...> {EX.S1, EX.p1, EX.O1},
...> {EX.S2, EX.p2, EX.O2},
...> {EX.S1, EX.p2, EX.O3}],
...> EX.Compound)
...> |> RTC.Compound.subjects()
MapSet.new([RDF.iri(EX.S1), RDF.iri(EX.S2)])
"""
@spec subjects(t, keyword) :: MapSet.t()
def subjects(%__MODULE__{} = compound, opts \\ []) do
subjects =
case assertion_mode_with_all(opts) do
:asserted ->
Graph.subjects(compound.asserted)
:unasserted ->
Graph.subjects(compound.unasserted)
:all ->
Graph.subjects(compound.asserted)
|> MapSet.union(Graph.subjects(compound.unasserted))
end
Enum.reduce(compound.sub_compounds, subjects, fn
{_, sub_compound}, subjects -> MapSet.union(subjects, subjects(sub_compound, opts))
end)
end
@doc """
The set of all properties used in the triples within a `RTC.Compound` or any of its sub-compounds.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([
...> {EX.S1, EX.p1, EX.O1},
...> {EX.S2, EX.p2, EX.O2},
...> {EX.S1, EX.p2, EX.O3}],
...> EX.Compound)
...> |> RTC.Compound.predicates()
MapSet.new([EX.p1, EX.p2])
"""
@spec predicates(t, keyword) :: MapSet.t()
def predicates(%__MODULE__{} = compound, opts \\ []) do
predicates =
case assertion_mode_with_all(opts) do
:asserted ->
Graph.predicates(compound.asserted)
:unasserted ->
Graph.predicates(compound.unasserted)
:all ->
Graph.predicates(compound.asserted)
|> MapSet.union(Graph.predicates(compound.unasserted))
end
Enum.reduce(compound.sub_compounds, predicates, fn
{_, sub_compound}, predicates -> MapSet.union(predicates, predicates(sub_compound, opts))
end)
end
@doc """
The set of all resources used in the objects of the triples within a `RTC.Compound` or any of its sub-compounds.
Note: This function does collect only IRIs and BlankNodes, not Literals.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([
...> {EX.S1, EX.p1, EX.O1},
...> {EX.S2, EX.p2, EX.O2},
...> {EX.S3, EX.p1, EX.O2},
...> {EX.S4, EX.p2, RDF.bnode(:bnode)},
...> {EX.S5, EX.p3, "foo"}],
...> EX.Compound)
...> |> RTC.Compound.objects()
MapSet.new([RDF.iri(EX.O1), RDF.iri(EX.O2), RDF.bnode(:bnode)])
"""
@spec objects(t, keyword) :: MapSet.t()
def objects(%__MODULE__{} = compound, opts \\ []) do
objects =
case assertion_mode_with_all(opts) do
:asserted ->
Graph.objects(compound.asserted)
:unasserted ->
Graph.objects(compound.unasserted)
:all ->
Graph.objects(compound.asserted)
|> MapSet.union(Graph.objects(compound.unasserted))
end
Enum.reduce(compound.sub_compounds, objects, fn
{_, sub_compound}, objects -> MapSet.union(objects, objects(sub_compound, opts))
end)
end
@doc """
The set of all resources used in the triples within a `RTC.Compound` or any of its sub-compounds.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([
...> {EX.S1, EX.p1, EX.O1},
...> {EX.S2, EX.p1, EX.O2},
...> {EX.S2, EX.p2, RDF.bnode(:bnode)},
...> {EX.S3, EX.p1, "foo"}],
...> EX.Compound)
...> |> RTC.Compound.resources()
MapSet.new([RDF.iri(EX.S1), RDF.iri(EX.S2), RDF.iri(EX.S3),
RDF.iri(EX.O1), RDF.iri(EX.O2), RDF.bnode(:bnode), EX.p1, EX.p2])
"""
@spec resources(t, keyword) :: MapSet.t()
def resources(%__MODULE__{} = compound, opts \\ []) do
resources =
case assertion_mode_with_all(opts) do
:asserted ->
Graph.resources(compound.asserted)
:unasserted ->
Graph.resources(compound.unasserted)
:all ->
Graph.resources(compound.asserted)
|> MapSet.union(Graph.resources(compound.unasserted))
end
Enum.reduce(compound.sub_compounds, resources, fn
{_, sub_compound}, resources -> MapSet.union(resources, resources(sub_compound, opts))
end)
end
@doc """
Returns a list of the `RDF.Description`s in the given `compound`.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec descriptions(t, keyword) :: [Description.t()]
def descriptions(%__MODULE__{} = compound, opts \\ []) do
compound
|> graph(opts)
|> Graph.descriptions()
end
@doc """
Returns whether the given triples are an element of the given `compound` or any of its sub-compounds.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec include?(t, Graph.input(), keyword) :: boolean
def include?(%__MODULE__{} = compound, input, opts \\ []) do
compound
|> graph(opts)
|> Graph.include?(input)
end
@doc """
Returns whether the given `compound` or any of its sub-compounds contains triples about the given subject.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([{EX.S1, EX.p1, EX.O1}]) |> RTC.Compound.describes?(EX.S1)
true
iex> RTC.Compound.new([{EX.S1, EX.p1, EX.O1}]) |> RTC.Compound.describes?(EX.S2)
false
"""
@spec describes?(t, Statement.coercible_subject(), keyword) :: boolean
def describes?(%__MODULE__{} = compound, subject, opts \\ []) do
assertion_mode = assertion_mode_with_all(opts)
(assertion_mode != :unasserted and Graph.describes?(compound.asserted, subject)) or
(assertion_mode != :asserted and Graph.describes?(compound.unasserted, subject)) or
Enum.any?(compound.sub_compounds, fn {_, sub_compound} ->
describes?(sub_compound, subject, opts)
end)
end
@doc """
Adds `triples` to the given `compound`.
Triples can be provided in any form accepted by `RDF.Graph.add/2`.
Available options:
- `:assertion_mode`: the assertion mode to be used for the `triples` to be added
(see module documentation section on "Asserted and unasserted triples")
"""
@spec add(t, Graph.input(), keyword) :: t
def add(compound, triples, opts \\ []) do
case assertion_mode(opts) do
:asserted ->
compound
|> update_unasserted(&Graph.delete(&1, triples, opts))
|> update_asserted(&Graph.add(&1, triples, opts))
:unasserted ->
compound
|> update_asserted(&Graph.delete(&1, triples, opts))
|> update_unasserted(&Graph.add(&1, triples, opts))
end
end
defp update_asserted(compound, fun) do
%__MODULE__{compound | asserted: fun.(compound.asserted)}
end
defp update_unasserted(compound, fun) do
%__MODULE__{compound | unasserted: fun.(compound.unasserted)}
end
@doc """
Deletes `triples` from the given `compound`.
Triples can be provided in any form accepted by `RDF.Graph.delete/2`.
If a triple occurs in one or more sub-compounds, it gets deleted from all of them.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec delete(t, Graph.input(), keyword) :: t
def delete(%__MODULE__{} = compound, triples, opts \\ []) do
assertion_mode = assertion_mode_with_all(opts)
compound =
if assertion_mode in [:all, :asserted] do
update_asserted(compound, &Graph.delete(&1, triples, opts))
else
compound
end
compound =
if assertion_mode in [:all, :unasserted] do
update_unasserted(compound, &Graph.delete(&1, triples, opts))
else
compound
end
%__MODULE__{
compound
| sub_compounds:
Map.new(compound.sub_compounds, fn {id, sub_compound} ->
{id, delete(sub_compound, triples, opts)}
end)
}
end
@doc """
Deletes all triples with the given `subjects` from the given `compound`.
If a triple occurs in one or more sub-compounds, it gets deleted from all of them.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
"""
@spec delete_descriptions(
t,
Statement.coercible_subject() | [Statement.coercible_subject()],
keyword
) ::
t
def delete_descriptions(%__MODULE__{} = compound, subjects, opts \\ []) do
assertion_mode = assertion_mode_with_all(opts)
compound =
if assertion_mode in [:all, :asserted] do
update_asserted(compound, &Graph.delete_descriptions(&1, subjects, opts))
else
compound
end
compound =
if assertion_mode in [:all, :unasserted] do
update_unasserted(compound, &Graph.delete_descriptions(&1, subjects, opts))
else
compound
end
%__MODULE__{
compound
| sub_compounds:
Map.new(compound.sub_compounds, fn {id, sub_compound} ->
{id, delete_descriptions(sub_compound, subjects, opts)}
end)
}
end
@doc """
Pops the description of the given subject.
Removes the description of the given `subject` from `compound` and
all of its sub-compounds.
Returns a tuple containing the description of the given subject
and the updated compound without this description.
`nil` is returned instead of the description if `compound` does
not contain a description of the given `subject`.
Supported options:
- `:assertion_mode`: see module documentation section on "Asserted and unasserted triples"
## Examples
iex> RTC.Compound.new([{EX.S1, EX.P1, EX.O1}, {EX.S2, EX.P2, EX.O2}], EX.Compound)
...> |> RTC.Compound.pop(EX.S1)
{
RDF.Description.new(EX.S1, init: {EX.P1, EX.O1}),
RTC.Compound.new({EX.S2, EX.P2, EX.O2}, EX.Compound)
}
iex> RTC.Compound.new({EX.S, EX.P, EX.O}, EX.Compound)
...> |> RTC.Compound.pop(EX.Missing)
{nil, RTC.Compound.new({EX.S, EX.P, EX.O}, EX.Compound)}
"""
@spec pop(t, Statement.coercible_subject(), keyword) :: {Description.t() | nil, t}
def pop(%__MODULE__{} = compound, subject, opts \\ []) do
subject = RDF.coerce_subject(subject)
assertion_mode = assertion_mode_with_all(opts)
{description, compound} =
if assertion_mode in [:all, :asserted] do
case Graph.pop(compound.asserted, subject) do
{nil, _} ->
{nil, compound}
{asserted_description, new_asserted} ->
{asserted_description, %__MODULE__{compound | asserted: new_asserted}}
end
else
{nil, compound}
end
{description, compound} =
if assertion_mode in [:all, :unasserted] do
case Graph.pop(compound.unasserted, subject) do
{nil, _} ->
{description, compound}
{unasserted_description, new_unasserted} ->
{
if(description,
do: Description.add(description, unasserted_description),
else: unasserted_description
),
%__MODULE__{compound | unasserted: new_unasserted}
}
end
else
{description, compound}
end
{description, new_sub_compounds} =
Enum.reduce(compound.sub_compounds, {description, compound.sub_compounds}, fn
{id, sub_compound}, {description, new_sub_compounds} ->
case pop(sub_compound, subject, opts) do
{nil, _} ->
{description, new_sub_compounds}
{sub_description, new_sub_compound} ->
{
if(description,
do: Description.add(description, sub_description),
else: sub_description
),
Map.put(new_sub_compounds, id, new_sub_compound)
}
end
end)
{description, %__MODULE__{compound | sub_compounds: new_sub_compounds}}
end
@doc """
Pops an arbitrary triple from the given `compound` or any of its sub-compounds.
"""
@spec pop(t) :: {Triple.t() | nil, t}
def pop(%__MODULE__{} = compound) do
cond do
not Graph.empty?(compound.asserted) ->
{triple, graph} = Graph.pop(compound.asserted)
{triple, %{compound | asserted: graph}}
not Graph.empty?(compound.unasserted) ->
{triple, graph} = Graph.pop(compound.unasserted)
{triple, %{compound | unasserted: graph}}
true ->
compound.sub_compounds
|> Enum.find_value(fn {_id, sub_compound} ->
case pop(sub_compound) do
{nil, _} -> nil
{triple, sub_compound} -> {triple, sub_compound}
end
end)
|> case do
nil -> {nil, compound}
{triple, sub_compound} -> {triple, put_sub_compound(compound, sub_compound)}
end
end
end
@doc """
Returns a list of the sub-compounds of the given `compound`.
"""
@spec sub_compounds(t) :: [t]
def sub_compounds(%__MODULE__{} = compound) do
Enum.map(compound.sub_compounds, fn {_, sub_compound} ->
put_super_compound(sub_compound, compound)
end)
end
@doc """
Returns the sub-compound of the given `compound` with the given `sub_compound_id`.
It will search the whole sub-compound tree for the requested `sub_compound_id`
If no compound with the `sub_compound_id` can be found, `nil` is returned.
"""
@spec sub_compound(t, coercible_id) :: t | nil
def sub_compound(%__MODULE__{} = compound, sub_compound_id) do
sub_compound_id = Statement.coerce_subject(sub_compound_id)
Enum.find_value(compound.sub_compounds, fn
{^sub_compound_id, sub_compound} -> put_super_compound(sub_compound, compound)
{_, sub_compound} -> sub_compound(sub_compound, sub_compound_id)
end)
end
@doc """
Adds a sub-compound to the given `compound`.
If a sub-compound with the same id already exists, it gets overwritten.
When just triples are passed instead of compound, a compound with an
auto-generated id is created implicitly.
"""
@spec put_sub_compound(t, t | Graph.input()) :: t
def put_sub_compound(compound, sub_compounds)
def put_sub_compound(%__MODULE__{} = compound, %__MODULE__{} = sub_compound) do
%__MODULE__{
compound
| sub_compounds:
Map.put(
compound.sub_compounds,
id(sub_compound),
delete_super_compound(sub_compound, compound)
)
}
end
def put_sub_compound(compound, triples), do: put_sub_compound(compound, new(triples))
@doc """
Deletes a sub-compound from the given `compound`.
The `sub_compound` to be deleted can be specified by id or given directly.
Note however, that the elements of the sub-compound to be deleted are not
taken into consideration, just its id is used to address the sub-compound
to be deleted.
"""
@spec delete_sub_compound(t, t | coercible_id) :: t
def delete_sub_compound(compound, sub_compound)
def delete_sub_compound(%__MODULE__{} = compound, %__MODULE__{} = sub_compound) do
delete_sub_compound(compound, id(sub_compound))
end
def delete_sub_compound(%__MODULE__{} = compound, sub_compound_id) do
%__MODULE__{
compound
| sub_compounds:
Map.delete(compound.sub_compounds, Statement.coerce_subject(sub_compound_id))
}
end
@doc """
Returns a list of the ids of the super-compounds of the given `compound`.
## Example
iex> RTC.Compound.new({EX.S, EX.p, EX.O}, EX.Compound, super_compounds: EX.SuperCompound)
...> |> RTC.Compound.super_compounds()
[RDF.iri(EX.SuperCompound)]
"""
@spec super_compounds(t) :: [id]
def super_compounds(%__MODULE__{} = compound), do: Map.keys(compound.super_compounds)
@doc """
Adds a super-compound to the given `compound`.
The super-compound can be given as a compound identifier, a `RDF.Description` or a
`RTC.Compound`. In case of a compound only its id and annotations are relevant, which
will be returned when inherited annotations are requested. A `RDF.Description` is
interpreted as the annotations of the super-compound.
> #### Warning {: .warning}
>
> The annotations are just used for the purpose of showing inherited annotations.
> They won't be rendered in `to_rdf/2`. So, you can't use this function to change the
> annotations of super-compounds.
> You'll have to load a super-compound with `from_rdf/2` and change the annotations on
> this compound.
If a super-compound with the same id already exists, it gets overwritten.
"""
@spec put_super_compound(
t,
coercible_id | t | Description.t() | [coercible_id | t | Description.t()]
) :: t
def put_super_compound(compound, sub_compounds)
def put_super_compound(%__MODULE__{} = compound, %Description{} = description) do
%__MODULE__{
compound
| super_compounds: Map.put(compound.super_compounds, description.subject, description)
}
end
def put_super_compound(compound, super_compounds) when is_list(super_compounds),
do: Enum.reduce(super_compounds, compound, &put_super_compound(&2, &1))
def put_super_compound(compound, %__MODULE__{annotations: annotations}),
do: put_super_compound(compound, annotations)
def put_super_compound(compound, super_compound_id),
do: put_super_compound(compound, Description.new(super_compound_id))
@doc """
Deletes a super-compound from the given `compound`.
The `super_compound` to be deleted can be specified by id or given directly.
Note however, that the elements of the super-compound to be deleted are not
taken into consideration, just its id is used to address the super-compound
to be deleted.
"""
@spec delete_super_compound(t, t | coercible_id) :: t
def delete_super_compound(compound, super_compound)
def delete_super_compound(%__MODULE__{} = compound, %__MODULE__{} = super_compound) do
delete_super_compound(compound, id(super_compound))
end
def delete_super_compound(%__MODULE__{} = compound, super_compound_id) do
%__MODULE__{
compound
| super_compounds:
Map.delete(compound.super_compounds, Statement.coerce_subject(super_compound_id))
}
end
@doc """
Returns the annotations of the given `compound`.
Supported options:
- `:inherited` - controls if inherited annotations should be included or
only the direct annotations of the given compound should be returned.
Default: `true`
"""
@spec annotations(t) :: Description.t()
def annotations(%__MODULE__{} = compound, opts \\ []) do
if Keyword.get(opts, :inherited, true) do
for {_, inherited} <- compound.super_compounds, into: compound.annotations do
inherited
end
else
compound.annotations
end
end
@doc """
Returns the merged annotations of all super-compounds of the given `compound`.
"""
@spec inherited_annotations(t) :: Description.t()
def inherited_annotations(%__MODULE__{} = compound) do
for {_, inherited} <- compound.super_compounds, into: RDF.description(id(compound)) do
inherited
end
end
@doc """
Returns the annotations of the given super-compound of `compound`.
"""
@spec inherited_annotations(t, coercible_id) :: Description.t()
def inherited_annotations(compound, super_compound_id)
def inherited_annotations(%__MODULE__{} = compound, super_compound_id),
do: compound.super_compounds[Statement.coerce_subject(super_compound_id)]
@doc """
Adds statements to the annotations of the given `compound`.
"""
@spec add_annotations(t, Description.input()) :: t
def add_annotations(%__MODULE__{} = compound, annotations) do
%__MODULE__{compound | annotations: Description.add(compound.annotations, annotations)}
end
@doc """
Deletes statements from the annotations of the given `compound`.
Statements not part of the annotations are simply ignored.
"""
@spec delete_annotations(t, Description.input()) :: t
def delete_annotations(%__MODULE__{} = compound, annotations) do
%__MODULE__{compound | annotations: Description.delete(compound.annotations, annotations)}
end
defimpl Enumerable do
alias RTC.Compound
def reduce(%Compound{} = compound, acc, fun),
do: compound |> Compound.graph() |> Enumerable.reduce(acc, fun)
def member?(%Compound{} = compound, triple), do: {:ok, Compound.include?(compound, triple)}
def count(_), do: {:error, __MODULE__}
def slice(_), do: {:error, __MODULE__}
end
defimpl RDF.Data do
alias RTC.Compound
alias RDF.{Description, Graph, Dataset, Statement}
def merge(compound, data, opts \\ []) do
graph_name =
case data do
{_, _, _, graph_name} -> graph_name
%Graph{name: graph_name} -> graph_name
%Dataset{} -> compound.asserted.name
_ -> nil
end
compound
|> Compound.graph(name: graph_name)
|> RDF.Data.merge(data, opts)
end
def delete(compound, input, _opts \\ []), do: Compound.delete(compound, input)
def pop(compound), do: Compound.pop(compound)
def empty?(compound), do: Compound.empty?(compound)
def include?(compound, input, _opts \\ []), do: Compound.include?(compound, input)
def describes?(compound, subject), do: Compound.describes?(compound, subject)
def description(compound, subject), do: Compound.description(compound, subject)
def descriptions(compound), do: Compound.descriptions(compound)
def statements(compound), do: Compound.statements(compound)
def subjects(compound), do: Compound.subjects(compound)
def predicates(compound), do: Compound.predicates(compound)
def objects(compound), do: Compound.objects(compound)
def resources(compound), do: Compound.resources(compound)
def subject_count(compound), do: compound |> subjects() |> MapSet.size()
def statement_count(compound), do: Compound.triple_count(compound)
def values(compound, opts \\ []), do: compound |> Compound.graph() |> Graph.values(opts)
def map(compound, fun), do: compound |> Compound.graph() |> Graph.values(fun)
def equal?(compound, %Compound{} = other),
do: compound |> Compound.graph() |> RDF.Data.equal?(Compound.graph(other))
def equal?(compound, data), do: compound |> Compound.graph() |> RDF.Data.equal?(data)
end
defimpl Inspect do
alias RTC.Compound
def inspect(compound, opts) do
if opts.structs do
try do
graph =
Compound.to_rdf(compound,
element_style: :elements,
include_super_compounds: true
)
id = Compound.id(compound)
graph_name =
if graph.name != id do
", graph_name: #{inspect(graph.name)}"
end
header = "#RTC.Compound<id: #{inspect(id)}#{graph_name}"
body = Kernel.inspect(graph, custom_options: [content_only: true])
"#{header}\n#{body}\n>"
rescue
caught_exception ->
message =
"got #{inspect(caught_exception.__struct__)} with message " <>
"#{inspect(Exception.message(caught_exception))} while inspecting RTC.Compound #{Compound.id(compound)}"
exception = Inspect.Error.exception(message: message)
if opts.safe do
Inspect.inspect(exception, opts)
else
reraise(exception, __STACKTRACE__)
end
end
else
Inspect.Map.inspect(compound, opts)
end
end
end
end