rdf-ex/lib/rdf/list.ex
2020-02-28 18:51:48 +01:00

250 lines
6.6 KiB
Elixir

defmodule RDF.List do
@moduledoc """
A structure for RDF lists.
see
- <https://www.w3.org/TR/rdf-schema/#ch_collectionvocab>
- <https://www.w3.org/TR/rdf11-mt/#rdf-collections>
"""
alias RDF.{Graph, Description, IRI, BlankNode}
@type t :: %__MODULE__{
head: RDF.IRI.t,
graph: RDF.Graph.t
}
@enforce_keys [:head]
defstruct [:head, :graph]
@rdf_nil RDF.nil
@doc """
Creates a `RDF.List` for a given RDF list node of a given `RDF.Graph`.
If the given node does not refer to a well-formed list in the graph, `nil` is
returned. A well-formed list
- consists of list nodes which have exactly one `rdf:first` and `rdf:rest`
statement each
- does not contain cycles, i.e. `rdf:rest` statements don't refer to
preceding list nodes
"""
def new(head, graph)
def new(head, graph) when is_atom(head) and head not in ~w[true false nil]a,
do: new(RDF.iri(head), graph)
def new(head, graph) do
with list = %RDF.List{head: head, graph: graph} do
if well_formed?(list) do
list
end
end
end
defp well_formed?(list) do
Enum.reduce_while(list, MapSet.new, fn node_description, preceding_nodes ->
with head = node_description.subject do
if MapSet.member?(preceding_nodes, head) do
{:halt, false}
else
{:cont, MapSet.put(preceding_nodes, head)}
end
end
end) && true
end
@doc """
Creates a `RDF.List` from a native Elixir list or any other `Enumerable` with coercible RDF values.
By default the statements constituting the `Enumerable` are added to an empty graph. An
already existing graph to which the statements are added can be specified with
the `graph` option.
The name of the head node can be specified with the `head` option
(default: `RDF.bnode()`, i.e. an arbitrary unique name).
Note: When the given `Enumerable` is empty, the `name` option will be ignored -
the head node of the empty list is always `RDF.nil`.
"""
def from(list, opts \\ []) do
with head = Keyword.get(opts, :head, RDF.bnode),
graph = Keyword.get(opts, :graph, RDF.graph),
{head, graph} = do_from(list, head, graph, opts)
do
%RDF.List{head: head, graph: graph}
end
end
defp do_from([], _, graph, _) do
{RDF.nil, graph}
end
defp do_from(list, head, graph, opts) when is_atom(head) do
do_from(list, RDF.iri!(head), graph, opts)
end
defp do_from([list | rest], head, graph, opts) when is_list(list) do
with {nested_list_node, graph} = do_from(list, RDF.bnode, graph, opts) do
do_from([nested_list_node | rest], head, graph, opts)
end
end
defp do_from([first | rest], head, graph, opts) do
with {next, graph} = do_from(rest, RDF.bnode, graph, opts) do
{
head,
Graph.add(graph,
head
|> RDF.first(first)
|> RDF.rest(next)
)
}
end
end
defp do_from(enumerable, head, graph, opts) do
enumerable
|> Enum.into([])
|> do_from(head, graph, opts)
end
@doc """
The values of a `RDF.List` as an Elixir list.
Nested lists are converted recursively.
"""
def values(%RDF.List{graph: graph} = list) do
Enum.map list, fn node_description ->
value = Description.first(node_description, RDF.first)
if node?(value, graph) do
value
|> new(graph)
|> values
else
value
end
end
end
@doc """
The RDF nodes constituting a `RDF.List` as an Elixir list.
"""
def nodes(%RDF.List{} = list) do
Enum.map list, fn node_description -> node_description.subject end
end
@doc """
Checks if a list is the empty list.
"""
def empty?(%RDF.List{head: @rdf_nil}), do: true
def empty?(_), do: false
@doc """
Checks if the given list consists of list nodes which are all blank nodes.
"""
def valid?(%RDF.List{head: @rdf_nil}), do: true
def valid?(list) do
Enum.all? list, fn node_description ->
RDF.bnode?(node_description.subject)
end
end
@doc """
Checks if a given resource is a RDF list node in a given `RDF.Graph`.
Although, technically a resource is a list, if it uses at least one `rdf:first`
or `rdf:rest`, we pragmatically require the usage of both.
Note: This function doesn't indicate if the list is valid.
See `new/2` and `valid?/2` for validations.
"""
def node?(list_node, graph)
def node?(@rdf_nil, _),
do: true
def node?(%BlankNode{} = list_node, graph),
do: do_node?(list_node, graph)
def node?(%IRI{} = list_node, graph),
do: do_node?(list_node, graph)
def node?(list_node, graph)
when is_atom(list_node) and list_node not in ~w[true false nil]a,
do: do_node?(RDF.iri(list_node), graph)
def node?(_, _), do: false
defp do_node?(list_node, graph),
do: graph |> Graph.description(list_node) |> node?
@doc """
Checks if the given `RDF.Description` describes a RDF list node.
"""
def node?(description)
def node?(nil), do: false
def node?(%Description{predications: predications}) do
Map.has_key?(predications, RDF.first) and
Map.has_key?(predications, RDF.rest)
end
defimpl Enumerable do
@rdf_nil RDF.nil
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)}
def reduce(%RDF.List{head: @rdf_nil}, {:cont, acc}, _fun),
do: {:done, acc}
def reduce(%RDF.List{head: %BlankNode{}} = list, acc, fun),
do: do_reduce(list, acc, fun)
def reduce(%RDF.List{head: %IRI{}} = list, acc, fun),
do: do_reduce(list, acc, fun)
def reduce(_, _, _), do: {:halted, nil}
defp do_reduce(%RDF.List{head: head, graph: graph},
{:cont, acc}, fun) do
with description when not is_nil(description) <-
Graph.description(graph, head),
[_] <- Description.get(description, RDF.first),
[rest] <- Description.get(description, RDF.rest),
acc = fun.(description, acc)
do
if rest == @rdf_nil do
case acc do
{:cont, acc} -> {:done, acc}
# TODO: Is the :suspend case handled properly
_ -> reduce(%RDF.List{head: rest, graph: graph}, acc, fun)
end
else
reduce(%RDF.List{head: rest, graph: graph}, acc, fun)
end
else
nil ->
{:halted, nil}
values when is_list(values) ->
{:halted, nil}
end
end
def member?(_, _), do: {:error, __MODULE__}
def count(_), do: {:error, __MODULE__}
def slice(_), do: {:error, __MODULE__}
end
end