diff --git a/lib/rdf/serializations/ntriples_encoder.ex b/lib/rdf/serializations/ntriples_encoder.ex index ef7cff5..5fff37c 100644 --- a/lib/rdf/serializations/ntriples_encoder.ex +++ b/lib/rdf/serializations/ntriples_encoder.ex @@ -3,26 +3,33 @@ defmodule RDF.NTriples.Encoder do use RDF.Serialization.Encoder - alias RDF.{BlankNode, IRI, XSD, Literal, Statement, Triple, LangString} + alias RDF.{Triple, Term, IRI, BlankNode, Literal, LangString, XSD} @impl RDF.Serialization.Encoder @callback encode(RDF.Data.t(), keyword) :: {:ok, String.t()} | {:error, any} def encode(data, _opts \\ []) do - result = - data - |> Enum.reduce([], &[statement(&1) | &2]) - |> Enum.reverse() - |> Enum.join("\n") + {:ok, + data + |> Enum.reduce([], &[statement(&1) | &2]) + |> Enum.reverse() + |> Enum.join()} + end - {:ok, if(result == "", do: result, else: result <> "\n")} + @impl RDF.Serialization.Encoder + @spec stream(RDF.Data.t(), keyword) :: Enumerable.t() + def stream(data, opts \\ []) do + case Keyword.get(opts, :mode, :string) do + :string -> Stream.map(data, &statement(&1)) + :iodata -> Stream.map(data, &iolist_statement(&1)) + end end @spec statement(Triple.t()) :: String.t() def statement({subject, predicate, object}) do - "#{term(subject)} #{term(predicate)} #{term(object)} ." + "#{term(subject)} #{term(predicate)} #{term(object)} .\n" end - @spec term(Statement.subject() | Statement.predicate() | Statement.object()) :: String.t() + @spec term(Term.t()) :: String.t() def term(%IRI{} = iri) do "<#{to_string(iri)}>" end @@ -42,4 +49,30 @@ defmodule RDF.NTriples.Encoder do def term(%BlankNode{} = bnode) do to_string(bnode) end + + @spec iolist_statement(Triple.t()) :: iolist + def iolist_statement({subject, predicate, object}) do + [iolist_term(subject), " ", iolist_term(predicate), " ", iolist_term(object), " .\n"] + end + + @spec iolist_term(Term.t()) :: String.t() + def iolist_term(%IRI{} = iri) do + ["<", iri.value, ">"] + end + + def iolist_term(%Literal{literal: %LangString{} = lang_string}) do + [~s["], lang_string.value, ~s["@], lang_string.language] + end + + def iolist_term(%Literal{literal: %XSD.String{} = xsd_string}) do + [~s["], xsd_string.value, ~s["]] + end + + def iolist_term(%Literal{} = literal) do + [~s["], Literal.lexical(literal), ~s["^^<], to_string(Literal.datatype_id(literal)), ">"] + end + + def iolist_term(%BlankNode{} = bnode) do + to_string(bnode) + end end diff --git a/test/unit/ntriples_encoder_test.exs b/test/unit/ntriples_encoder_test.exs index cf4850b..c6ff263 100644 --- a/test/unit/ntriples_encoder_test.exs +++ b/test/unit/ntriples_encoder_test.exs @@ -14,6 +14,10 @@ defmodule RDF.NTriples.EncoderTest do defvocab EX, base_iri: "http://example.org/#", terms: [], strict: false + test "stream_support?/0" do + assert NTriples.Encoder.stream_support?() + end + describe "serializing a graph" do test "an empty graph is serialized to an empty string" do assert NTriples.Encoder.encode!(Graph.new()) == "" @@ -68,4 +72,31 @@ defmodule RDF.NTriples.EncoderTest do """ end end + + describe "stream/2" do + graph = + Graph.new([ + {EX.S1, EX.p1(), EX.O1}, + {EX.S2, EX.p2(), RDF.bnode("foo")}, + {EX.S3, EX.p3(), ~L"foo"}, + {EX.S3, EX.p3(), ~L"foo"en} + ]) + + expected_result = """ + . + _:foo . + "foo"@en . + "foo" . + """ + + assert NTriples.Encoder.stream(graph, mode: :string) + |> Enum.to_list() + |> IO.iodata_to_binary() == + expected_result + + assert NTriples.Encoder.stream(graph, mode: :iodata) + |> Enum.to_list() + |> IO.iodata_to_binary() == + expected_result + end end