defmodule RDF.List do @moduledoc """ A structure for RDF lists. see - - """ alias RDF.{BlankNode, Description, Graph, IRI} import RDF.Guards @type t :: %__MODULE__{ head: IRI.t(), graph: Graph.t() } @enforce_keys [:head] defstruct [:head, :graph] @rdf_nil RDF.Utils.Bootstrapping.rdf_iri("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 """ @spec new(IRI.coercible(), Graph.t()) :: t def new(head, graph) def new(head, graph) when maybe_ns_term(head), do: new(RDF.iri(head), graph) def new(head, graph) do with list = %__MODULE__{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`. """ @spec from(Enumerable.t(), keyword) :: t 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 %__MODULE__{head: head, graph: graph} end end defp do_from([], _, graph, _) do {RDF.nil(), graph} end defp do_from(list, head, graph, opts) when maybe_ns_term(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. """ @spec values(t) :: Enumerable.t() def values(%__MODULE__{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. """ @spec nodes(t) :: [BlankNode.t()] def nodes(%__MODULE__{} = list) do Enum.map(list, fn node_description -> node_description.subject end) end @doc """ Checks if a list is the empty list. """ @spec empty?(t) :: boolean def empty?(%__MODULE__{head: @rdf_nil}), do: true def empty?(%__MODULE__{}), do: false @doc """ Checks if the given list consists of list nodes which are all blank nodes. """ @spec valid?(t) :: boolean def valid?(%__MODULE__{head: @rdf_nil}), do: true def valid?(%__MODULE__{} = list), do: Enum.all?(list, &RDF.bnode?(&1.subject)) @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. """ @spec node?(any, Graph.t()) :: boolean 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 maybe_ns_term(list_node), 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.Utils.Bootstrapping.rdf_iri("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.get(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