rdf-ex/lib/rdf/list.ex

239 lines
6.8 KiB
Elixir
Raw Normal View History

defmodule RDF.List do
@moduledoc """
2017-07-31 21:21:09 +00:00
A structure for RDF lists.
see
- <https://www.w3.org/TR/rdf-schema/#ch_collectionvocab>
- <https://www.w3.org/TR/rdf11-mt/#rdf-collections>
"""
2020-03-02 01:07:31 +00:00
alias RDF.{BlankNode, Description, Graph, IRI}
2020-05-06 16:04:19 +00:00
import RDF.Guards
2020-02-28 17:51:48 +00:00
@type t :: %__MODULE__{
2020-06-29 08:37:42 +00:00
head: IRI.t(),
graph: Graph.t()
}
2020-02-28 17:51:48 +00:00
@enforce_keys [:head]
defstruct [:head, :graph]
@rdf_nil RDF.Utils.Bootstrapping.rdf_iri("nil")
@doc """
2017-07-31 21:21:09 +00:00
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
"""
2020-06-29 08:37:42 +00:00
@spec new(IRI.coercible(), Graph.t()) :: t
2017-07-31 21:21:09 +00:00
def new(head, graph)
2020-05-06 16:04:19 +00:00
def new(head, graph) when maybe_ns_term(head),
do: new(RDF.iri(head), graph)
2017-07-31 21:21:09 +00:00
def new(head, graph) do
with list = %__MODULE__{head: head, graph: graph} do
2017-07-31 21:21:09 +00:00
if well_formed?(list) do
list
end
end
end
defp well_formed?(list) do
2020-06-29 08:37:42 +00:00
Enum.reduce_while(list, MapSet.new(), fn node_description, preceding_nodes ->
2017-07-31 21:21:09 +00:00
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
2017-07-31 21:21:09 +00:00
@doc """
Creates a `RDF.List` from a native Elixir list or any other `Enumerable` with coercible RDF values.
2017-07-31 21:21:09 +00:00
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).
2017-07-31 21:21:09 +00:00
Note: When the given `Enumerable` is empty, the `name` option will be ignored -
the head node of the empty list is always `RDF.nil`.
"""
2020-06-29 08:37:42 +00:00
@spec from(Enumerable.t(), keyword) :: t
2017-07-31 21:21:09 +00:00
def from(list, opts \\ []) do
2020-06-29 08:37:42 +00:00
with head = Keyword.get(opts, :head, RDF.bnode()),
graph = Keyword.get(opts, :graph, RDF.graph()),
{head, graph} = do_from(list, head, graph, opts) do
%__MODULE__{head: head, graph: graph}
2017-07-31 21:21:09 +00:00
end
end
2017-07-31 21:21:09 +00:00
defp do_from([], _, graph, _) do
2020-06-29 08:37:42 +00:00
{RDF.nil(), graph}
end
2020-05-06 16:04:19 +00:00
defp do_from(list, head, graph, opts) when maybe_ns_term(head) do
do_from(list, RDF.iri!(head), graph, opts)
end
2017-07-31 21:21:09 +00:00
defp do_from([list | rest], head, graph, opts) when is_list(list) do
2020-06-29 08:37:42 +00:00
with {nested_list_node, graph} = do_from(list, RDF.bnode(), graph, opts) do
2017-07-31 21:21:09 +00:00
do_from([nested_list_node | rest], head, graph, opts)
end
end
2017-07-31 21:21:09 +00:00
defp do_from([first | rest], head, graph, opts) do
2020-06-29 08:37:42 +00:00
with {next, graph} = do_from(rest, RDF.bnode(), graph, opts) do
{
head,
2020-06-29 08:37:42 +00:00
Graph.add(
graph,
head
|> RDF.first(first)
|> RDF.rest(next)
)
}
end
end
2017-07-31 21:21:09 +00:00
defp do_from(enumerable, head, graph, opts) do
enumerable
|> Enum.into([])
|> do_from(head, graph, opts)
end
2017-07-31 21:21:09 +00:00
@doc """
The values of a `RDF.List` as an Elixir list.
2017-07-31 21:21:09 +00:00
Nested lists are converted recursively.
"""
2020-06-29 08:37:42 +00:00
@spec values(t) :: Enumerable.t()
def values(%__MODULE__{graph: graph} = list) do
2020-06-29 08:37:42 +00:00
Enum.map(list, fn node_description ->
value = Description.first(node_description, RDF.first())
2017-07-31 21:21:09 +00:00
if node?(value, graph) do
value
|> new(graph)
|> values
else
value
end
2020-06-29 08:37:42 +00:00
end)
end
2017-07-31 21:21:09 +00:00
@doc """
2018-03-19 00:50:05 +00:00
The RDF nodes constituting a `RDF.List` as an Elixir list.
2017-07-31 21:21:09 +00:00
"""
2020-06-29 08:37:42 +00:00
@spec nodes(t) :: [BlankNode.t()]
def nodes(%__MODULE__{} = list) do
2020-06-29 08:37:42 +00:00
Enum.map(list, fn node_description -> node_description.subject end)
2017-07-31 21:21:09 +00:00
end
@doc """
Checks if a list is the empty list.
"""
2020-03-02 01:07:31 +00:00
@spec empty?(t) :: boolean
def empty?(%__MODULE__{head: @rdf_nil}), do: true
2020-06-29 08:37:42 +00:00
def empty?(%__MODULE__{}), do: false
2017-07-31 21:21:09 +00:00
@doc """
Checks if the given list consists of list nodes which are all blank nodes.
"""
2020-03-02 01:07:31 +00:00
@spec valid?(t) :: boolean
def valid?(%__MODULE__{head: @rdf_nil}), do: true
2021-10-09 14:40:07 +00:00
def valid?(%__MODULE__{} = list), do: Enum.all?(list, &RDF.bnode?(&1.subject))
@doc """
2017-07-31 21:21:09 +00:00
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.
2017-07-31 21:21:09 +00:00
Note: This function doesn't indicate if the list is valid.
2018-03-19 00:50:05 +00:00
See `new/2` and `valid?/2` for validations.
"""
2020-06-29 08:37:42 +00:00
@spec node?(any, Graph.t()) :: boolean
def node?(list_node, graph)
2021-10-09 14:40:07 +00:00
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)
2020-05-06 16:04:19 +00:00
def node?(list_node, graph) when maybe_ns_term(list_node),
do: do_node?(RDF.iri(list_node), graph)
def node?(_, _), do: false
2021-10-09 14:40:07 +00:00
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
2020-06-29 08:37:42 +00:00
Map.has_key?(predications, RDF.first()) and
Map.has_key?(predications, RDF.rest())
end
2017-07-31 21:21:09 +00:00
defimpl Enumerable do
@rdf_nil RDF.Utils.Bootstrapping.rdf_iri("nil")
2017-07-31 21:21:09 +00:00
2020-06-29 08:37:42 +00:00
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
2017-07-31 21:21:09 +00:00
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),
2017-07-31 21:21:09 +00:00
do: do_reduce(list, acc, fun)
def reduce(_, _, _), do: {:halted, nil}
2020-06-29 08:37:42 +00:00
defp do_reduce(%RDF.List{head: head, graph: graph}, {:cont, acc}, fun) do
2017-07-31 21:21:09 +00:00
with description when not is_nil(description) <-
2020-06-29 08:37:42 +00:00
Graph.description(graph, head),
[_] <- Description.get(description, RDF.first()),
[rest] <- Description.get(description, RDF.rest()),
acc = fun.(description, acc) do
2017-07-31 21:21:09 +00:00
if rest == @rdf_nil do
case acc do
{:cont, acc} -> {:done, acc}
# TODO: Is the :suspend case handled properly
2020-06-29 08:37:42 +00:00
_ -> reduce(%RDF.List{head: rest, graph: graph}, acc, fun)
2017-07-31 21:21:09 +00:00
end
else
reduce(%RDF.List{head: rest, graph: graph}, acc, fun)
end
else
nil ->
{:halted, nil}
2020-06-29 08:37:42 +00:00
2017-07-31 21:21:09 +00:00
values when is_list(values) ->
{:halted, nil}
end
end
2020-06-29 08:37:42 +00:00
def member?(_, _), do: {:error, __MODULE__}
def count(_), do: {:error, __MODULE__}
def slice(_), do: {:error, __MODULE__}
2017-07-31 21:21:09 +00:00
end
end