rdf-ex/lib/rdf/serializations/turtle_encoder_state.ex

135 lines
4.5 KiB
Elixir

defmodule RDF.Turtle.Encoder.State do
@moduledoc false
alias RDF.{BlankNode, Description}
def start_link(data, base, prefixes) do
Agent.start_link(fn -> %{data: data, base: base, prefixes: prefixes} end)
end
def stop(state) do
Agent.stop(state)
end
def data(state), do: Agent.get(state, &(&1.data))
def base(state), do: Agent.get(state, &(&1.base))
def prefixes(state), do: Agent.get(state, &(&1.prefixes))
def list_nodes(state), do: Agent.get(state, &(&1.list_nodes))
def bnode_ref_counter(state), do: Agent.get(state, &(&1.bnode_ref_counter))
def bnode_ref_counter(state, bnode) do
bnode_ref_counter(state) |> Map.get(bnode, 0)
end
def base_iri(state) do
with {:ok, base} <- base(state) do
RDF.iri(base)
else
_ -> nil
end
end
def list_values(head, state), do: Agent.get(state, &(&1.list_values[head]))
def preprocess(state) do
with data = data(state),
{bnode_ref_counter, list_parents} = bnode_info(data),
{list_nodes, list_values} = valid_lists(list_parents, bnode_ref_counter, data)
do
Agent.update(state, &Map.put(&1, :bnode_ref_counter, bnode_ref_counter))
Agent.update(state, &Map.put(&1, :list_nodes, list_nodes))
Agent.update(state, &Map.put(&1, :list_values, list_values))
end
end
defp bnode_info(data) do
data
|> RDF.Data.descriptions
|> Enum.reduce({%{}, %{}},
fn %Description{subject: subject} = description,
{bnode_ref_counter, list_parents} ->
list_parents =
if match?(%BlankNode{}, subject) and
to_list?(description, Map.get(bnode_ref_counter, subject, 0)),
do: Map.put_new(list_parents, subject, nil),
else: list_parents
Enum.reduce(description.predications, {bnode_ref_counter, list_parents}, fn
({predicate, objects}, {bnode_ref_counter, list_parents}) ->
Enum.reduce(Map.keys(objects), {bnode_ref_counter, list_parents}, fn
(%BlankNode{} = object, {bnode_ref_counter, list_parents}) ->
{
# Note: The following conditional produces imprecise results
# (sometimes the occurrence in the subject counts, sometimes it doesn't),
# but is sufficient for the current purpose of handling the
# case of a statement with the same subject and object bnode.
Map.update(bnode_ref_counter, object,
(if subject == object, do: 2, else: 1), &(&1 + 1)),
if predicate == RDF.rest do
Map.put_new(list_parents, object, subject)
else
list_parents
end
}
(_, {bnode_ref_counter, list_parents}) ->
{bnode_ref_counter, list_parents}
end)
end)
end)
end
@list_properties MapSet.new([
RDF.Utils.Bootstrapping.rdf_iri("first"),
RDF.Utils.Bootstrapping.rdf_iri("rest")
])
@dialyzer {:nowarn_function, to_list?: 2}
defp to_list?(%Description{} = description, 1) do
Description.count(description) == 2 and
Description.predicates(description) |> MapSet.equal?(@list_properties)
end
defp to_list?(%Description{} = description, 0),
do: RDF.list?(description)
defp to_list?(_, _),
do: false
defp valid_lists(list_parents, bnode_ref_counter, data) do
head_nodes = for {list_node, nil} <- list_parents, do: list_node
all_list_nodes = MapSet.new(
for {list_node, _} <- list_parents, Map.get(bnode_ref_counter, list_node, 0) < 2 do
list_node
end)
Enum.reduce head_nodes, {MapSet.new, %{}},
fn head_node, {valid_list_nodes, list_values} ->
with list when not is_nil(list) <-
RDF.List.new(head_node, data),
list_nodes =
RDF.List.nodes(list),
true <-
Enum.all?(list_nodes, fn
%BlankNode{} = list_node ->
MapSet.member?(all_list_nodes, list_node)
_ ->
false
end)
do
{
Enum.reduce(list_nodes, valid_list_nodes, fn list_node, valid_list_nodes ->
MapSet.put(valid_list_nodes, list_node)
end),
Map.put(list_values, head_node, RDF.List.values(list)),
}
else
_ -> {valid_list_nodes, list_values}
end
end
end
end