Add support for Turtle-star encoding

This commit is contained in:
Marcel Otto 2021-10-09 16:40:07 +02:00
parent 695a54159c
commit e9102252ae
9 changed files with 485 additions and 170 deletions

View file

@ -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`
### 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)

View file

@ -154,12 +154,7 @@ defmodule RDF.List do
"""
@spec valid?(t) :: boolean
def valid?(%__MODULE__{head: @rdf_nil}), do: true
def valid?(%__MODULE__{} = list) do
Enum.all?(list, fn node_description ->
RDF.bnode?(node_description.subject)
end)
end
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`.
@ -172,23 +167,16 @@ defmodule RDF.List do
"""
@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?(@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?
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.

View 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

View file

@ -26,7 +26,8 @@ defmodule RDF.Turtle.Encoder do
use RDF.Serialization.Encoder
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]
@ -60,18 +61,22 @@ defmodule RDF.Turtle.Encoder do
@ordered_properties MapSet.new(@predicate_order)
@impl RDF.Serialization.Encoder
@spec encode(RDF.Data.t(), keyword) :: {:ok, String.t()} | {:error, any}
def encode(data, opts \\ []) do
@spec encode(Graph.t() | Description.t(), keyword) :: {:ok, String.t()} | {:error, any}
def encode(data, opts \\ [])
def encode(%Description{} = description, opts), do: description |> Graph.new() |> encode(opts)
def encode(%Graph{} = graph, opts) do
base =
Keyword.get(opts, :base, Keyword.get(opts, :base_iri))
|> base_iri(data)
|> base_iri(graph)
|> init_base_iri()
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
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, %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(prefixes, _), do: PrefixMap.new(prefixes)
@ -150,6 +145,7 @@ defmodule RDF.Turtle.Encoder do
indent = indent(opts)
State.data(state)
|> CompactGraph.compact()
|> RDF.Data.descriptions()
|> order_descriptions(state)
|> 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
base_iri = State.base_iri(state)
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)
group = Enum.group_by(descriptions, &description_group(&1, base_iri))
ordered_descriptions =
(@top_classes
|> Stream.map(fn top_class -> group[top_class] end)
|> Stream.map(&group[&1])
|> Stream.reject(&is_nil/1)
|> Stream.map(&sort_description_group/1)
|> Enum.reduce([], fn class_group, ordered_descriptions ->
ordered_descriptions ++ class_group
end)) ++ (group |> Map.get(:other, []) |> sort_description_group())
|> Enum.flat_map(&sort_descriptions/1)) ++
(group |> Map.get(:other, []) |> sort_descriptions())
case group[:base] do
[base] -> [base | ordered_descriptions]
@ -190,23 +170,37 @@ defmodule RDF.Turtle.Encoder do
end
end
defp sort_description_group(descriptions) do
Enum.sort(descriptions, fn
%Description{subject: %IRI{}}, %Description{subject: %BlankNode{}} ->
true
defp description_group(%{subject: base_iri}, base_iri), do: :base
%Description{subject: %BlankNode{}}, %Description{subject: %IRI{}} ->
false
%Description{subject: s1}, %Description{subject: s2} ->
to_string(s1) < to_string(s2)
end)
defp description_group(description, _) do
if types = description.predications[@rdf_type] do
Enum.find(@top_classes, :other, &Map.has_key?(types, &1))
else
:other
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
with %BlankNode{} <- description.subject,
ref_count when ref_count < 2 <-
State.bnode_ref_counter(state, description.subject) do
ref_count when ref_count < 2 <- State.bnode_ref_counter(state, description.subject) do
unrefed_bnode_subject_term(description, ref_count, state, nesting)
else
_ -> full_description_statements(description, state, nesting)
@ -226,10 +220,14 @@ defmodule RDF.Turtle.Encoder do
defp blank_node_property_list(description, state, nesting) do
indented = nesting + @indentation
"[" <>
newline_indent(indented) <>
predications(description, state, indented) <>
newline_indent(nesting) <> "]"
if Enum.empty?(description) do
"[]"
else
"[" <>
newline_indent(indented) <>
predications(description, state, indented) <>
newline_indent(nesting) <> "]"
end
end
defp predications(description, state, nesting) do
@ -255,12 +253,30 @@ defmodule RDF.Turtle.Encoder do
end
defp predication({predicate, objects}, state, nesting) do
term(predicate, state, :predicate, nesting) <>
" " <>
(objects
|> Enum.map(fn {object, _} -> term(object, state, :object, nesting) end)
# TODO: split if the line gets too long
|> Enum.join(", "))
term(predicate, state, :predicate, nesting) <> " " <> objects(objects, state, 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
separator =
if with_annotations,
do: "," <> newline_indent(nesting + @indentation),
else: ", "
Enum.join(objects, separator)
end
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),
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
"(" <>
(list

View file

@ -22,9 +22,8 @@ defmodule RDF.Turtle.Encoder.State do
end
def base_iri(state) do
with {:ok, base} <- base(state) do
RDF.iri(base)
else
case base(state) do
{:ok, base} -> RDF.iri(base)
_ -> nil
end
end
@ -32,13 +31,13 @@ defmodule RDF.Turtle.Encoder.State do
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
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
@ -47,15 +46,23 @@ defmodule RDF.Turtle.Encoder.State do
|> 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
@ -83,6 +90,21 @@ defmodule RDF.Turtle.Encoder.State do
)
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")
@ -94,21 +116,17 @@ defmodule RDF.Turtle.Encoder.State do
Description.predicates(description) |> MapSet.equal?(@list_properties)
end
defp to_list?(%Description{} = description, 0),
do: RDF.list?(description)
defp to_list?(_, _),
do: false
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
)
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) <-

View file

@ -12,7 +12,7 @@ defmodule RDF.Test.Case do
using 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 unquote(__MODULE__).{EX, FOAF}

View file

@ -34,6 +34,11 @@ defmodule RDF.InspectTest do
|> String.trim()) <> "\n>"
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
{_, triples} = inspect_parts(@test_description, limit: 2)

View file

@ -344,80 +344,6 @@ defmodule RDF.Turtle.EncoderTest do
description |> Graph.new() |> Turtle.Encoder.encode!()
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
setup do
{:ok,

View 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