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" 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