diff --git a/CHANGELOG.md b/CHANGELOG.md index eaba01a..fc5fbb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/rdf/list.ex b/lib/rdf/list.ex index 4d370da..a185204 100644 --- a/lib/rdf/list.ex +++ b/lib/rdf/list.ex @@ -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. diff --git a/lib/rdf/serializations/turtle/star_compact_graph.ex b/lib/rdf/serializations/turtle/star_compact_graph.ex new file mode 100644 index 0000000..9b966d7 --- /dev/null +++ b/lib/rdf/serializations/turtle/star_compact_graph.ex @@ -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 diff --git a/lib/rdf/serializations/turtle_encoder.ex b/lib/rdf/serializations/turtle_encoder.ex index b596746..167500b 100644 --- a/lib/rdf/serializations/turtle_encoder.ex +++ b/lib/rdf/serializations/turtle_encoder.ex @@ -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 diff --git a/lib/rdf/serializations/turtle_encoder_state.ex b/lib/rdf/serializations/turtle_encoder_state.ex index f604be4..2632dc5 100644 --- a/lib/rdf/serializations/turtle_encoder_state.ex +++ b/lib/rdf/serializations/turtle_encoder_state.ex @@ -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) <- diff --git a/test/support/rdf_case.ex b/test/support/rdf_case.ex index ad19e55..31e6d24 100644 --- a/test/support/rdf_case.ex +++ b/test/support/rdf_case.ex @@ -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} diff --git a/test/unit/inspect_test.exs b/test/unit/inspect_test.exs index e2aab37..66c3f81 100644 --- a/test/unit/inspect_test.exs +++ b/test/unit/inspect_test.exs @@ -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 =~ "<< \"Foo\" >>" + end + test ":limit option" do {_, triples} = inspect_parts(@test_description, limit: 2) diff --git a/test/unit/turtle_encoder_test.exs b/test/unit/turtle_encoder_test.exs index 2403ae4..4a761c2 100644 --- a/test/unit/turtle_encoder_test.exs +++ b/test/unit/turtle_encoder_test.exs @@ -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, RDF.type(), RDFS.Class}, - name: EX.Graph2, - prefixes: %{ - ex: "http://other.example.com/", - rdf: RDF, - rdfs: RDFS - } - ) - ) - - assert Turtle.Encoder.encode!(dataset) == - """ - @prefix ex: . - @prefix owl: . - @prefix rdf: . - @prefix rdfs: . - - - 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: . - @prefix rdfs: . - @prefix xsd: . - - - . - """ - end - end - describe "prefixed_name/2" do setup do {:ok, diff --git a/test/unit/turtle_star_encoder_test.exs b/test/unit/turtle_star_encoder_test.exs new file mode 100644 index 0000000..e31d2ab --- /dev/null +++ b/test/unit/turtle_star_encoder_test.exs @@ -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 : . + + << :s :p :o >> + :q :z . + """ + + assert RDF.graph({{EX.s(), EX.p(), "foo"}, EX.q(), "foo"}, prefixes: [nil: EX]) + |> Turtle.Encoder.encode!() == + """ + @prefix : . + + << :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 : . + + << :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 : . + + << _: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 : . + + :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 : . + + :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 : . + + :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 : . + + :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 : . + + :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}, + {~B"foo", EX.date(), XSD.date("2020-01-20")}, + {~B"bar", EX.graph(), ~I}, + {~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 : . + @prefix xsd: <#{NS.XSD.__base_iri__()}> . + + :s + :p :o {| :source [ + :date "2020-12-31"^^xsd:date ; + :graph + ], [ + :date "2020-01-20"^^xsd:date ; + :graph + ] |} . + """ + 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 : . + + :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 : . + + :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 : . + + :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 : . + + << :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 : . + + :s + :p << :s2 :p2 :o2 >> {| :r :z |} . + """ + end +end