Add support for Turtle-star encoding
This commit is contained in:
parent
695a54159c
commit
e9102252ae
9 changed files with 485 additions and 170 deletions
|
@ -11,6 +11,12 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
|
||||||
|
|
||||||
- support for `RDF.PropertyMap` on `RDF.Statement.new/2` and `RDF.Statement.coerce/2`
|
- support for `RDF.PropertyMap` on `RDF.Statement.new/2` and `RDF.Statement.coerce/2`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- the `RDF.Turtle.Encoder` no longer supports the encoding of `RDF.Dataset`s; you'll have to
|
||||||
|
aggregate a `RDF.Dataset` to a `RDF.Graph` on your own now
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Compare v0.9.4...HEAD](https://github.com/rdf-elixir/rdf-ex/compare/v0.9.4...HEAD)
|
[Compare v0.9.4...HEAD](https://github.com/rdf-elixir/rdf-ex/compare/v0.9.4...HEAD)
|
||||||
|
|
||||||
|
|
|
@ -154,12 +154,7 @@ defmodule RDF.List do
|
||||||
"""
|
"""
|
||||||
@spec valid?(t) :: boolean
|
@spec valid?(t) :: boolean
|
||||||
def valid?(%__MODULE__{head: @rdf_nil}), do: true
|
def valid?(%__MODULE__{head: @rdf_nil}), do: true
|
||||||
|
def valid?(%__MODULE__{} = list), do: Enum.all?(list, &RDF.bnode?(&1.subject))
|
||||||
def valid?(%__MODULE__{} = list) do
|
|
||||||
Enum.all?(list, fn node_description ->
|
|
||||||
RDF.bnode?(node_description.subject)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Checks if a given resource is a RDF list node in a given `RDF.Graph`.
|
Checks if a given resource is a RDF list node in a given `RDF.Graph`.
|
||||||
|
@ -172,23 +167,16 @@ defmodule RDF.List do
|
||||||
"""
|
"""
|
||||||
@spec node?(any, Graph.t()) :: boolean
|
@spec node?(any, Graph.t()) :: boolean
|
||||||
def node?(list_node, graph)
|
def node?(list_node, graph)
|
||||||
|
def node?(@rdf_nil, _), do: true
|
||||||
def node?(@rdf_nil, _),
|
def node?(%BlankNode{} = list_node, graph), do: do_node?(list_node, graph)
|
||||||
do: true
|
def node?(%IRI{} = list_node, graph), do: do_node?(list_node, graph)
|
||||||
|
|
||||||
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),
|
def node?(list_node, graph) when maybe_ns_term(list_node),
|
||||||
do: do_node?(RDF.iri(list_node), graph)
|
do: do_node?(RDF.iri(list_node), graph)
|
||||||
|
|
||||||
def node?(_, _), do: false
|
def node?(_, _), do: false
|
||||||
|
|
||||||
defp do_node?(list_node, graph),
|
defp do_node?(list_node, graph), do: graph |> Graph.description(list_node) |> node?()
|
||||||
do: graph |> Graph.description(list_node) |> node?
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Checks if the given `RDF.Description` describes a RDF list node.
|
Checks if the given `RDF.Description` describes a RDF list node.
|
||||||
|
|
77
lib/rdf/serializations/turtle/star_compact_graph.ex
Normal file
77
lib/rdf/serializations/turtle/star_compact_graph.ex
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
defmodule RDF.Turtle.Star.CompactGraph do
|
||||||
|
@moduledoc !"""
|
||||||
|
A compact graph representation in which annotations are directly stored under
|
||||||
|
the objects of the annotated triples.
|
||||||
|
|
||||||
|
This representation is not meant for direct use, but just for the `RDF.Turtle.Encoder`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias RDF.{Graph, Description}
|
||||||
|
|
||||||
|
def compact(graph) do
|
||||||
|
Enum.reduce(graph.descriptions, graph, fn
|
||||||
|
{{_, _, _} = quoted_triple, _}, compact_graph ->
|
||||||
|
# First check the original graph to see if the quoted triple is asserted.
|
||||||
|
if Graph.include?(graph, quoted_triple) do
|
||||||
|
annotation =
|
||||||
|
compact_graph
|
||||||
|
# We'll have to re-fetch the description, since the compact_graph might already contain
|
||||||
|
# an updated description with an annotation.
|
||||||
|
|> Graph.description(quoted_triple)
|
||||||
|
|> as_annotation()
|
||||||
|
|
||||||
|
compact_graph
|
||||||
|
|> add_annotation(quoted_triple, annotation)
|
||||||
|
|> Graph.delete_descriptions(quoted_triple)
|
||||||
|
else
|
||||||
|
compact_graph
|
||||||
|
end
|
||||||
|
|
||||||
|
_, compact_graph ->
|
||||||
|
compact_graph
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_annotation(compact_graph, {{_, _, _} = quoted_triple, p, o} = triple, annotation) do
|
||||||
|
# Check if the compact graph still contains the annoted triple, we want to put the annotation under.
|
||||||
|
if Graph.describes?(compact_graph, quoted_triple) do
|
||||||
|
do_add_annotation(compact_graph, triple, annotation)
|
||||||
|
else
|
||||||
|
# It's not there anymore, which means the description of the quoted triple was already moved as an annotation.
|
||||||
|
# Next we have to search recursively for the annotation, we want to put the nested annotation under.
|
||||||
|
path = find_annotation_path(compact_graph, quoted_triple, [p, o])
|
||||||
|
do_add_annotation(compact_graph, path, annotation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_annotation(compact_graph, triple, annotation) do
|
||||||
|
do_add_annotation(compact_graph, triple, annotation)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_add_annotation(compact_graph, {s, p, o}, annotation) do
|
||||||
|
update_in(compact_graph, [s], &put_in(&1.predications[p][o], annotation))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_add_annotation(compact_graph, [s | path], annotation) do
|
||||||
|
update_in(compact_graph, [s], &update_annotation_in(&1, path, annotation))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_annotation_in(_, [], annotation), do: annotation
|
||||||
|
|
||||||
|
defp update_annotation_in(description, [p, o | rest], annotation) do
|
||||||
|
%Description{
|
||||||
|
description
|
||||||
|
| predications:
|
||||||
|
update_in(description.predications, [p, o], &update_annotation_in(&1, rest, annotation))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_annotation_path(compact_graph, {s, p, o}, path) do
|
||||||
|
cond do
|
||||||
|
Graph.describes?(compact_graph, s) -> [s, p, o | path]
|
||||||
|
match?({_, _, _}, s) -> find_annotation_path(compact_graph, s, [p, o | path])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp as_annotation(description), do: %{description | subject: nil}
|
||||||
|
end
|
|
@ -26,7 +26,8 @@ defmodule RDF.Turtle.Encoder do
|
||||||
use RDF.Serialization.Encoder
|
use RDF.Serialization.Encoder
|
||||||
|
|
||||||
alias RDF.Turtle.Encoder.State
|
alias RDF.Turtle.Encoder.State
|
||||||
alias RDF.{BlankNode, Dataset, Description, Graph, IRI, XSD, Literal, LangString, PrefixMap}
|
alias RDF.Turtle.Star.CompactGraph
|
||||||
|
alias RDF.{BlankNode, Description, Graph, IRI, XSD, Literal, LangString, PrefixMap}
|
||||||
|
|
||||||
import RDF.NTriples.Encoder, only: [escape_string: 1]
|
import RDF.NTriples.Encoder, only: [escape_string: 1]
|
||||||
|
|
||||||
|
@ -60,18 +61,22 @@ defmodule RDF.Turtle.Encoder do
|
||||||
@ordered_properties MapSet.new(@predicate_order)
|
@ordered_properties MapSet.new(@predicate_order)
|
||||||
|
|
||||||
@impl RDF.Serialization.Encoder
|
@impl RDF.Serialization.Encoder
|
||||||
@spec encode(RDF.Data.t(), keyword) :: {:ok, String.t()} | {:error, any}
|
@spec encode(Graph.t() | Description.t(), keyword) :: {:ok, String.t()} | {:error, any}
|
||||||
def encode(data, opts \\ []) do
|
def encode(data, opts \\ [])
|
||||||
|
|
||||||
|
def encode(%Description{} = description, opts), do: description |> Graph.new() |> encode(opts)
|
||||||
|
|
||||||
|
def encode(%Graph{} = graph, opts) do
|
||||||
base =
|
base =
|
||||||
Keyword.get(opts, :base, Keyword.get(opts, :base_iri))
|
Keyword.get(opts, :base, Keyword.get(opts, :base_iri))
|
||||||
|> base_iri(data)
|
|> base_iri(graph)
|
||||||
|> init_base_iri()
|
|> init_base_iri()
|
||||||
|
|
||||||
prefixes =
|
prefixes =
|
||||||
Keyword.get(opts, :prefixes)
|
Keyword.get(opts, :prefixes)
|
||||||
|> prefixes(data)
|
|> prefixes(graph)
|
||||||
|
|
||||||
{:ok, state} = State.start_link(data, base, prefixes)
|
{:ok, state} = State.start_link(graph, base, prefixes)
|
||||||
|
|
||||||
try do
|
try do
|
||||||
State.preprocess(state)
|
State.preprocess(state)
|
||||||
|
@ -108,16 +113,6 @@ defmodule RDF.Turtle.Encoder do
|
||||||
|
|
||||||
defp prefixes(nil, %Graph{prefixes: prefixes}) when not is_nil(prefixes), do: prefixes
|
defp prefixes(nil, %Graph{prefixes: prefixes}) when not is_nil(prefixes), do: prefixes
|
||||||
|
|
||||||
defp prefixes(nil, %Dataset{} = dataset) do
|
|
||||||
prefixes = Dataset.prefixes(dataset)
|
|
||||||
|
|
||||||
if Enum.empty?(prefixes) do
|
|
||||||
RDF.default_prefixes()
|
|
||||||
else
|
|
||||||
prefixes
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp prefixes(nil, _), do: RDF.default_prefixes()
|
defp prefixes(nil, _), do: RDF.default_prefixes()
|
||||||
defp prefixes(prefixes, _), do: PrefixMap.new(prefixes)
|
defp prefixes(prefixes, _), do: PrefixMap.new(prefixes)
|
||||||
|
|
||||||
|
@ -150,6 +145,7 @@ defmodule RDF.Turtle.Encoder do
|
||||||
indent = indent(opts)
|
indent = indent(opts)
|
||||||
|
|
||||||
State.data(state)
|
State.data(state)
|
||||||
|
|> CompactGraph.compact()
|
||||||
|> RDF.Data.descriptions()
|
|> RDF.Data.descriptions()
|
||||||
|> order_descriptions(state)
|
|> order_descriptions(state)
|
||||||
|> Enum.map(&description_statements(&1, state, Keyword.get(opts, :indent, 0)))
|
|> Enum.map(&description_statements(&1, state, Keyword.get(opts, :indent, 0)))
|
||||||
|
@ -159,30 +155,14 @@ defmodule RDF.Turtle.Encoder do
|
||||||
|
|
||||||
defp order_descriptions(descriptions, state) do
|
defp order_descriptions(descriptions, state) do
|
||||||
base_iri = State.base_iri(state)
|
base_iri = State.base_iri(state)
|
||||||
|
group = Enum.group_by(descriptions, &description_group(&1, base_iri))
|
||||||
group =
|
|
||||||
Enum.group_by(descriptions, fn
|
|
||||||
%Description{subject: ^base_iri} ->
|
|
||||||
:base
|
|
||||||
|
|
||||||
description ->
|
|
||||||
with types when not is_nil(types) <- description.predications[@rdf_type] do
|
|
||||||
Enum.find(@top_classes, :other, fn top_class ->
|
|
||||||
Map.has_key?(types, top_class)
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
_ -> :other
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
ordered_descriptions =
|
ordered_descriptions =
|
||||||
(@top_classes
|
(@top_classes
|
||||||
|> Stream.map(fn top_class -> group[top_class] end)
|
|> Stream.map(&group[&1])
|
||||||
|> Stream.reject(&is_nil/1)
|
|> Stream.reject(&is_nil/1)
|
||||||
|> Stream.map(&sort_description_group/1)
|
|> Enum.flat_map(&sort_descriptions/1)) ++
|
||||||
|> Enum.reduce([], fn class_group, ordered_descriptions ->
|
(group |> Map.get(:other, []) |> sort_descriptions())
|
||||||
ordered_descriptions ++ class_group
|
|
||||||
end)) ++ (group |> Map.get(:other, []) |> sort_description_group())
|
|
||||||
|
|
||||||
case group[:base] do
|
case group[:base] do
|
||||||
[base] -> [base | ordered_descriptions]
|
[base] -> [base | ordered_descriptions]
|
||||||
|
@ -190,23 +170,37 @@ defmodule RDF.Turtle.Encoder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sort_description_group(descriptions) do
|
defp description_group(%{subject: base_iri}, base_iri), do: :base
|
||||||
Enum.sort(descriptions, fn
|
|
||||||
%Description{subject: %IRI{}}, %Description{subject: %BlankNode{}} ->
|
|
||||||
true
|
|
||||||
|
|
||||||
%Description{subject: %BlankNode{}}, %Description{subject: %IRI{}} ->
|
defp description_group(description, _) do
|
||||||
false
|
if types = description.predications[@rdf_type] do
|
||||||
|
Enum.find(@top_classes, :other, &Map.has_key?(types, &1))
|
||||||
%Description{subject: s1}, %Description{subject: s2} ->
|
else
|
||||||
to_string(s1) < to_string(s2)
|
:other
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sort_descriptions(descriptions), do: Enum.sort(descriptions, &description_order/2)
|
||||||
|
|
||||||
|
defp description_order(%{subject: %IRI{}}, %{subject: %BlankNode{}}), do: true
|
||||||
|
defp description_order(%{subject: %BlankNode{}}, %{subject: %IRI{}}), do: false
|
||||||
|
|
||||||
|
defp description_order(%{subject: {s, p, o1}}, %{subject: {s, p, o2}}),
|
||||||
|
do: to_string(o1) < to_string(o2)
|
||||||
|
|
||||||
|
defp description_order(%{subject: {s, p1, _}}, %{subject: {s, p2, _}}),
|
||||||
|
do: to_string(p1) < to_string(p2)
|
||||||
|
|
||||||
|
defp description_order(%{subject: {s1, _, _}}, %{subject: {s2, _, _}}),
|
||||||
|
do: to_string(s1) < to_string(s2)
|
||||||
|
|
||||||
|
defp description_order(%{subject: {_, _, _}}, %{subject: _}), do: false
|
||||||
|
defp description_order(%{subject: _}, %{subject: {_, _, _}}), do: true
|
||||||
|
defp description_order(%{subject: s1}, %{subject: s2}), do: to_string(s1) < to_string(s2)
|
||||||
|
|
||||||
defp description_statements(description, state, nesting) do
|
defp description_statements(description, state, nesting) do
|
||||||
with %BlankNode{} <- description.subject,
|
with %BlankNode{} <- description.subject,
|
||||||
ref_count when ref_count < 2 <-
|
ref_count when ref_count < 2 <- State.bnode_ref_counter(state, description.subject) do
|
||||||
State.bnode_ref_counter(state, description.subject) do
|
|
||||||
unrefed_bnode_subject_term(description, ref_count, state, nesting)
|
unrefed_bnode_subject_term(description, ref_count, state, nesting)
|
||||||
else
|
else
|
||||||
_ -> full_description_statements(description, state, nesting)
|
_ -> full_description_statements(description, state, nesting)
|
||||||
|
@ -226,11 +220,15 @@ defmodule RDF.Turtle.Encoder do
|
||||||
defp blank_node_property_list(description, state, nesting) do
|
defp blank_node_property_list(description, state, nesting) do
|
||||||
indented = nesting + @indentation
|
indented = nesting + @indentation
|
||||||
|
|
||||||
|
if Enum.empty?(description) do
|
||||||
|
"[]"
|
||||||
|
else
|
||||||
"[" <>
|
"[" <>
|
||||||
newline_indent(indented) <>
|
newline_indent(indented) <>
|
||||||
predications(description, state, indented) <>
|
predications(description, state, indented) <>
|
||||||
newline_indent(nesting) <> "]"
|
newline_indent(nesting) <> "]"
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp predications(description, state, nesting) do
|
defp predications(description, state, nesting) do
|
||||||
description.predications
|
description.predications
|
||||||
|
@ -255,12 +253,30 @@ defmodule RDF.Turtle.Encoder do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp predication({predicate, objects}, state, nesting) do
|
defp predication({predicate, objects}, state, nesting) do
|
||||||
term(predicate, state, :predicate, nesting) <>
|
term(predicate, state, :predicate, nesting) <> " " <> objects(objects, state, nesting)
|
||||||
" " <>
|
end
|
||||||
(objects
|
|
||||||
|> Enum.map(fn {object, _} -> term(object, state, :object, nesting) end)
|
defp objects(objects, state, nesting) do
|
||||||
|
{objects, with_annotations} =
|
||||||
|
Enum.map_reduce(objects, false, fn {object, annotation}, with_annotations ->
|
||||||
|
if annotation do
|
||||||
|
{
|
||||||
|
term(object, state, :object, nesting) <>
|
||||||
|
" {| #{predications(annotation, state, nesting + 2 * @indentation)} |}",
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{term(object, state, :object, nesting), with_annotations}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
# TODO: split if the line gets too long
|
# TODO: split if the line gets too long
|
||||||
|> Enum.join(", "))
|
separator =
|
||||||
|
if with_annotations,
|
||||||
|
do: "," <> newline_indent(nesting + @indentation),
|
||||||
|
else: ", "
|
||||||
|
|
||||||
|
Enum.join(objects, separator)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unrefed_bnode_subject_term(bnode_description, ref_count, state, nesting) do
|
defp unrefed_bnode_subject_term(bnode_description, ref_count, state, nesting) do
|
||||||
|
@ -372,6 +388,10 @@ defmodule RDF.Turtle.Encoder do
|
||||||
defp term(%Literal{} = literal, state, _, nesting),
|
defp term(%Literal{} = literal, state, _, nesting),
|
||||||
do: typed_literal_term(literal, state, nesting)
|
do: typed_literal_term(literal, state, nesting)
|
||||||
|
|
||||||
|
defp term({s, p, o}, state, _, nesting) do
|
||||||
|
"<< #{term(s, state, :subject, nesting)} #{term(p, state, :predicate, nesting)} #{term(o, state, :object, nesting)} >>"
|
||||||
|
end
|
||||||
|
|
||||||
defp term(list, state, _, nesting) when is_list(list) do
|
defp term(list, state, _, nesting) when is_list(list) do
|
||||||
"(" <>
|
"(" <>
|
||||||
(list
|
(list
|
||||||
|
|
|
@ -22,9 +22,8 @@ defmodule RDF.Turtle.Encoder.State do
|
||||||
end
|
end
|
||||||
|
|
||||||
def base_iri(state) do
|
def base_iri(state) do
|
||||||
with {:ok, base} <- base(state) do
|
case base(state) do
|
||||||
RDF.iri(base)
|
{:ok, base} -> RDF.iri(base)
|
||||||
else
|
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,14 +31,14 @@ defmodule RDF.Turtle.Encoder.State do
|
||||||
def list_values(head, state), do: Agent.get(state, & &1.list_values[head])
|
def list_values(head, state), do: Agent.get(state, & &1.list_values[head])
|
||||||
|
|
||||||
def preprocess(state) do
|
def preprocess(state) do
|
||||||
with data = data(state),
|
data = data(state)
|
||||||
{bnode_ref_counter, list_parents} = bnode_info(data),
|
{bnode_ref_counter, list_parents} = bnode_info(data)
|
||||||
{list_nodes, list_values} = valid_lists(list_parents, bnode_ref_counter, data) do
|
{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, :bnode_ref_counter, bnode_ref_counter))
|
||||||
Agent.update(state, &Map.put(&1, :list_nodes, list_nodes))
|
Agent.update(state, &Map.put(&1, :list_nodes, list_nodes))
|
||||||
Agent.update(state, &Map.put(&1, :list_values, list_values))
|
Agent.update(state, &Map.put(&1, :list_values, list_values))
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
defp bnode_info(data) do
|
defp bnode_info(data) do
|
||||||
data
|
data
|
||||||
|
@ -47,15 +46,23 @@ defmodule RDF.Turtle.Encoder.State do
|
||||||
|> Enum.reduce(
|
|> Enum.reduce(
|
||||||
{%{}, %{}},
|
{%{}, %{}},
|
||||||
fn %Description{subject: subject} = description, {bnode_ref_counter, list_parents} ->
|
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 =
|
list_parents =
|
||||||
if match?(%BlankNode{}, subject) and
|
if match?(%BlankNode{}, subject) and
|
||||||
to_list?(description, Map.get(bnode_ref_counter, subject, 0)),
|
to_list?(description, Map.get(bnode_ref_counter, subject, 0)),
|
||||||
do: Map.put_new(list_parents, subject, nil),
|
do: Map.put_new(list_parents, subject, nil),
|
||||||
else: list_parents
|
else: list_parents
|
||||||
|
|
||||||
|
bnode_ref_counter = handle_quoted_triples(subject, bnode_ref_counter)
|
||||||
|
|
||||||
Enum.reduce(description.predications, {bnode_ref_counter, list_parents}, fn
|
Enum.reduce(description.predications, {bnode_ref_counter, list_parents}, fn
|
||||||
{predicate, objects}, {bnode_ref_counter, list_parents} ->
|
{predicate, objects}, {bnode_ref_counter, list_parents} ->
|
||||||
Enum.reduce(Map.keys(objects), {bnode_ref_counter, list_parents}, fn
|
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} ->
|
%BlankNode{} = object, {bnode_ref_counter, list_parents} ->
|
||||||
{
|
{
|
||||||
# Note: The following conditional produces imprecise results
|
# Note: The following conditional produces imprecise results
|
||||||
|
@ -83,6 +90,21 @@ defmodule RDF.Turtle.Encoder.State do
|
||||||
)
|
)
|
||||||
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([
|
@list_properties MapSet.new([
|
||||||
RDF.Utils.Bootstrapping.rdf_iri("first"),
|
RDF.Utils.Bootstrapping.rdf_iri("first"),
|
||||||
RDF.Utils.Bootstrapping.rdf_iri("rest")
|
RDF.Utils.Bootstrapping.rdf_iri("rest")
|
||||||
|
@ -94,21 +116,17 @@ defmodule RDF.Turtle.Encoder.State do
|
||||||
Description.predicates(description) |> MapSet.equal?(@list_properties)
|
Description.predicates(description) |> MapSet.equal?(@list_properties)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp to_list?(%Description{} = description, 0),
|
defp to_list?(%Description{} = description, 0), do: RDF.list?(description)
|
||||||
do: RDF.list?(description)
|
defp to_list?(_, _), do: false
|
||||||
|
|
||||||
defp to_list?(_, _),
|
|
||||||
do: false
|
|
||||||
|
|
||||||
defp valid_lists(list_parents, bnode_ref_counter, data) do
|
defp valid_lists(list_parents, bnode_ref_counter, data) do
|
||||||
head_nodes = for {list_node, nil} <- list_parents, do: list_node
|
head_nodes = for {list_node, nil} <- list_parents, do: list_node
|
||||||
|
|
||||||
all_list_nodes =
|
all_list_nodes =
|
||||||
MapSet.new(
|
|
||||||
for {list_node, _} <- list_parents, Map.get(bnode_ref_counter, list_node, 0) < 2 do
|
for {list_node, _} <- list_parents, Map.get(bnode_ref_counter, list_node, 0) < 2 do
|
||||||
list_node
|
list_node
|
||||||
end
|
end
|
||||||
)
|
|> MapSet.new()
|
||||||
|
|
||||||
Enum.reduce(head_nodes, {MapSet.new(), %{}}, fn head_node, {valid_list_nodes, list_values} ->
|
Enum.reduce(head_nodes, {MapSet.new(), %{}}, fn head_node, {valid_list_nodes, list_values} ->
|
||||||
with list when not is_nil(list) <-
|
with list when not is_nil(list) <-
|
||||||
|
|
|
@ -12,7 +12,7 @@ defmodule RDF.Test.Case do
|
||||||
|
|
||||||
using do
|
using do
|
||||||
quote do
|
quote do
|
||||||
alias RDF.{Dataset, Graph, Description, IRI, XSD, PrefixMap, PropertyMap}
|
alias RDF.{Dataset, Graph, Description, IRI, XSD, PrefixMap, PropertyMap, NS}
|
||||||
alias RDF.NS.{RDFS, OWL}
|
alias RDF.NS.{RDFS, OWL}
|
||||||
alias unquote(__MODULE__).{EX, FOAF}
|
alias unquote(__MODULE__).{EX, FOAF}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,11 @@ defmodule RDF.InspectTest do
|
||||||
|> String.trim()) <> "\n>"
|
|> String.trim()) <> "\n>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it encodes the RDF-star graphs ands descriptions in Turtle-star" do
|
||||||
|
{_, triples} = inspect_parts(annotation(), limit: 2)
|
||||||
|
assert triples =~ "<< <http://example.com/S> <http://example.com/P> \"Foo\" >>"
|
||||||
|
end
|
||||||
|
|
||||||
test ":limit option" do
|
test ":limit option" do
|
||||||
{_, triples} = inspect_parts(@test_description, limit: 2)
|
{_, triples} = inspect_parts(@test_description, limit: 2)
|
||||||
|
|
||||||
|
|
|
@ -344,80 +344,6 @@ defmodule RDF.Turtle.EncoderTest do
|
||||||
description |> Graph.new() |> Turtle.Encoder.encode!()
|
description |> Graph.new() |> Turtle.Encoder.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "serializing a dataset" do
|
|
||||||
test "prefixes of the graphs are merged properly" do
|
|
||||||
dataset =
|
|
||||||
RDF.Dataset.new()
|
|
||||||
|> RDF.Dataset.add(
|
|
||||||
Graph.new(
|
|
||||||
[
|
|
||||||
{EX.__base_iri__(), RDF.type(), OWL.Ontology},
|
|
||||||
{EX.S1, RDF.type(), EX.O}
|
|
||||||
],
|
|
||||||
base_iri: EX.__base_iri__(),
|
|
||||||
prefixes: %{
|
|
||||||
rdf: RDF,
|
|
||||||
owl: OWL
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> RDF.Dataset.add(
|
|
||||||
Graph.new(
|
|
||||||
{EX.S3, EX.p(), EX.O},
|
|
||||||
name: EX.Graph1,
|
|
||||||
prefixes: %{
|
|
||||||
ex: EX,
|
|
||||||
rdf: RDF
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> RDF.Dataset.add(
|
|
||||||
Graph.new(
|
|
||||||
{~I<http://other.example.com/S2>, RDF.type(), RDFS.Class},
|
|
||||||
name: EX.Graph2,
|
|
||||||
prefixes: %{
|
|
||||||
ex: "http://other.example.com/",
|
|
||||||
rdf: RDF,
|
|
||||||
rdfs: RDFS
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Turtle.Encoder.encode!(dataset) ==
|
|
||||||
"""
|
|
||||||
@prefix ex: <http://example.org/#> .
|
|
||||||
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
||||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
||||||
|
|
||||||
<http://other.example.com/S2>
|
|
||||||
a rdfs:Class .
|
|
||||||
|
|
||||||
ex:
|
|
||||||
a owl:Ontology .
|
|
||||||
|
|
||||||
ex:S1
|
|
||||||
a ex:O .
|
|
||||||
|
|
||||||
ex:S3
|
|
||||||
ex:p ex:O .
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "when none of the graphs uses prefixes the default prefixes are used" do
|
|
||||||
assert RDF.Dataset.new({EX.S, EX.p(), EX.O})
|
|
||||||
|> Turtle.Encoder.encode!() ==
|
|
||||||
"""
|
|
||||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
||||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
||||||
|
|
||||||
<http://example.org/#S>
|
|
||||||
<http://example.org/#p> <http://example.org/#O> .
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "prefixed_name/2" do
|
describe "prefixed_name/2" do
|
||||||
setup do
|
setup do
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
275
test/unit/turtle_star_encoder_test.exs
Normal file
275
test/unit/turtle_star_encoder_test.exs
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
defmodule RDF.Star.Turtle.EncoderTest do
|
||||||
|
use RDF.Test.Case
|
||||||
|
|
||||||
|
alias RDF.Turtle
|
||||||
|
|
||||||
|
test "quoted triple on subject position" do
|
||||||
|
assert RDF.graph({{EX.s(), EX.p(), EX.o()}, EX.q(), EX.z()}, prefixes: [nil: EX])
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
<< :s :p :o >>
|
||||||
|
:q :z .
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert RDF.graph({{EX.s(), EX.p(), "foo"}, EX.q(), "foo"}, prefixes: [nil: EX])
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
<< :s :p "foo" >>
|
||||||
|
:q "foo" .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blank nodes in quoted triples" do
|
||||||
|
assert RDF.graph({{EX.s(), EX.p(), ~B"foo"}, EX.q(), ~B"foo"}, prefixes: [nil: EX])
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
<< :s :p _:foo >>
|
||||||
|
:q _:foo .
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: _:foo could be encoded as []
|
||||||
|
assert RDF.graph({{~B"foo", EX.p(), ~B"bar"}, EX.q(), ~B"baz"}, prefixes: [nil: EX])
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
<< _:foo :p [] >>
|
||||||
|
:q [] .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "quoted triple on object position" do
|
||||||
|
assert RDF.graph({EX.a(), EX.q(), {EX.s(), EX.p(), EX.o()}}, prefixes: [nil: EX])
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:a
|
||||||
|
:q << :s :p :o >> .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "single annotation" do
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), EX.o()},
|
||||||
|
{{EX.s(), EX.p(), EX.o()}, EX.q(), EX.z()}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p :o {| :q :z |} .
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), "foo"},
|
||||||
|
{{EX.s(), EX.p(), "foo"}, EX.q(), "foo"}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p "foo" {| :q "foo" |} .
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), EX.o1()},
|
||||||
|
{EX.s(), EX.p(), EX.o2()},
|
||||||
|
{{EX.s(), EX.p(), EX.o2()}, EX.a(), EX.b()}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p :o1,
|
||||||
|
:o2 {| :a :b |} .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multiple annotations" do
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), EX.o()},
|
||||||
|
{EX.s(), EX.p2(), EX.o2()},
|
||||||
|
{EX.s(), EX.p2(), EX.o3()},
|
||||||
|
{{EX.s(), EX.p(), EX.o()}, EX.a(), EX.b()},
|
||||||
|
{{EX.s(), EX.p2(), EX.o2()}, EX.a2(), EX.b2()},
|
||||||
|
{{EX.s(), EX.p2(), EX.o3()}, EX.a3(), EX.b3()}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p :o {| :a :b |} ;
|
||||||
|
:p2 :o2 {| :a2 :b2 |},
|
||||||
|
:o3 {| :a3 :b3 |} .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotations with blank nodes" do
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), EX.o()},
|
||||||
|
{~B"foo", EX.graph(), ~I<http://host1/>},
|
||||||
|
{~B"foo", EX.date(), XSD.date("2020-01-20")},
|
||||||
|
{~B"bar", EX.graph(), ~I<http://host2/>},
|
||||||
|
{~B"bar", EX.date(), XSD.date("2020-12-31")},
|
||||||
|
{{EX.s(), EX.p(), EX.o()}, EX.source(), ~B"foo"},
|
||||||
|
{{EX.s(), EX.p(), EX.o()}, EX.source(), ~B"bar"}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX, xsd: NS.XSD]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
@prefix xsd: <#{NS.XSD.__base_iri__()}> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p :o {| :source [
|
||||||
|
:date "2020-12-31"^^xsd:date ;
|
||||||
|
:graph <http://host2/>
|
||||||
|
], [
|
||||||
|
:date "2020-01-20"^^xsd:date ;
|
||||||
|
:graph <http://host1/>
|
||||||
|
] |} .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "nested annotations" do
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), EX.o()},
|
||||||
|
{{EX.s(), EX.p(), EX.o()}, EX.a(), EX.b()},
|
||||||
|
{{{EX.s(), EX.p(), EX.o()}, EX.a(), EX.b()}, EX.a2(), EX.b2()},
|
||||||
|
{{{{EX.s(), EX.p(), EX.o()}, EX.a(), EX.b()}, EX.a2(), EX.b2()}, EX.a3(), EX.b3()}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p :o {| :a :b {| :a2 :b2 {| :a3 :b3 |} |} |} .
|
||||||
|
"""
|
||||||
|
|
||||||
|
# test for a nested annotation where an inner annotation is moved inside the CompactGraph before the outer
|
||||||
|
# Since every map with less than Erlang's configured MAP_SMALL_MAP_LIMIT number of elements behaves
|
||||||
|
# ordered in Erlang, this case won't happen for smaller graphs, so, we're creating a graph with a
|
||||||
|
# sufficiently large number of triples that this case will happen very likely.
|
||||||
|
series = 1..99
|
||||||
|
|
||||||
|
assert """
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
""" <> predications =
|
||||||
|
Enum.flat_map(series, fn i ->
|
||||||
|
[
|
||||||
|
{EX.s(), apply(EX, String.to_atom("p#{i}"), []), i},
|
||||||
|
{{EX.s(), apply(EX, String.to_atom("p#{i}"), []), i}, EX.a(), EX.b()},
|
||||||
|
{{{EX.s(), apply(EX, String.to_atom("p#{i}"), []), i}, EX.a(), EX.b()}, EX.a2(),
|
||||||
|
EX.b2()},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
{{EX.s(), apply(EX, String.to_atom("p#{i}"), []), i}, EX.a(), EX.b()},
|
||||||
|
EX.a2(),
|
||||||
|
EX.b2()
|
||||||
|
},
|
||||||
|
EX.a34(),
|
||||||
|
EX.b3()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
{{EX.s(), apply(EX, String.to_atom("p#{i}"), []), i}, EX.a(), EX.b()},
|
||||||
|
EX.a2(),
|
||||||
|
EX.b2()
|
||||||
|
},
|
||||||
|
EX.a34(),
|
||||||
|
EX.b3()
|
||||||
|
},
|
||||||
|
EX.a34(),
|
||||||
|
EX.b4()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end)
|
||||||
|
|> RDF.graph(prefixes: [nil: EX])
|
||||||
|
|> Turtle.Encoder.encode!()
|
||||||
|
|
||||||
|
Enum.each(series, fn i ->
|
||||||
|
assert predications =~
|
||||||
|
" :p#{i} #{i} {| :a :b {| :a2 :b2 {| :a34 :b3 {| :a34 :b4 |} |} |} |}"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "quoted triple in annotation" do
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), EX.o()},
|
||||||
|
{{EX.s(), EX.p(), EX.o()}, EX.r(), {EX.s1(), EX.p1(), EX.o1()}}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p :o {| :r << :s1 :p1 :o1 >> |} .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotation of a statement with a quoted triple" do
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{{EX.s1(), EX.p1(), EX.o1()}, EX.p(), EX.o()},
|
||||||
|
{{{EX.s1(), EX.p1(), EX.o1()}, EX.p(), EX.o()}, EX.r(), EX.z()}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
<< :s1 :p1 :o1 >>
|
||||||
|
:p :o {| :r :z |} .
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert RDF.graph(
|
||||||
|
[
|
||||||
|
{EX.s(), EX.p(), {EX.s2(), EX.p2(), EX.o2()}},
|
||||||
|
{{EX.s(), EX.p(), {EX.s2(), EX.p2(), EX.o2()}}, EX.r(), EX.z()}
|
||||||
|
],
|
||||||
|
prefixes: [nil: EX]
|
||||||
|
)
|
||||||
|
|> Turtle.Encoder.encode!() ==
|
||||||
|
"""
|
||||||
|
@prefix : <http://example.com/> .
|
||||||
|
|
||||||
|
:s
|
||||||
|
:p << :s2 :p2 :o2 >> {| :r :z |} .
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue