rdf-ex/lib/rdf/serializations/turtle_encoder_state.ex
2021-10-09 16:40:07 +02:00

154 lines
5.3 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
case base(state) do
{:ok, base} -> RDF.iri(base)
_ -> nil
end
end
def list_values(head, state), do: Agent.get(state, & &1.list_values[head])
def preprocess(state) do
data = data(state)
{bnode_ref_counter, list_parents} = bnode_info(data)
{list_nodes, list_values} = valid_lists(list_parents, bnode_ref_counter, data)
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
defp bnode_info(data) do
data
|> RDF.Data.descriptions()
|> Enum.reduce(
{%{}, %{}},
fn %Description{subject: subject} = description, {bnode_ref_counter, list_parents} ->
# We don't count blank node subjects, because when a blank node only occurs as a subject in
# multiple triples, we still can and want to use the square bracket syntax for its encoding.
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
bnode_ref_counter = handle_quoted_triples(subject, bnode_ref_counter)
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
{_, _, _} = quoted_triple, {bnode_ref_counter, list_parents} ->
{handle_quoted_triples(quoted_triple, bnode_ref_counter), list_parents}
%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
defp handle_quoted_triples({s, _, o}, bnode_ref_counter) do
bnode_ref_counter =
case s do
%BlankNode{} -> Map.update(bnode_ref_counter, s, 1, &(&1 + 1))
_ -> bnode_ref_counter
end
case o do
%BlankNode{} -> Map.update(bnode_ref_counter, o, 1, &(&1 + 1))
_ -> bnode_ref_counter
end
end
defp handle_quoted_triples(_, bnode_ref_counter), do: bnode_ref_counter
@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 =
for {list_node, _} <- list_parents, Map.get(bnode_ref_counter, list_node, 0) < 2 do
list_node
end
|> MapSet.new()
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