diff --git a/lib/rdf/serializations/ntriples_decoder.ex b/lib/rdf/serializations/ntriples_decoder.ex index d8a1918..fd63ef9 100644 --- a/lib/rdf/serializations/ntriples_decoder.ex +++ b/lib/rdf/serializations/ntriples_decoder.ex @@ -9,18 +9,39 @@ defmodule RDF.NTriples.Decoder do @impl RDF.Serialization.Decoder @spec decode(String.t(), keyword) :: {:ok, Graph.t()} | {:error, any} - def decode(content, _opts \\ []) do - with {:ok, tokens, _} <- tokenize(content), - {:ok, ast} <- parse(tokens) do + def decode(string, _opts \\ []) do + with {:ok, ast} <- do_decode(string, true) do {:ok, build_graph(ast)} + end + end + + @impl RDF.Serialization.Decoder + @spec decode_from_stream(Enumerable.t(), keyword) :: Graph.t() + def decode_from_stream(stream, _opts \\ []) do + Enum.reduce(stream, Graph.new(), fn line, graph -> + case do_decode(line, false) do + {:ok, []} -> graph + {:ok, [[triple]]} -> Graph.add(graph, triple) + {:error, error} -> raise error + end + end) + end + + defp do_decode(string, error_with_line_number) do + with {:ok, tokens, _} <- tokenize(string) do + parse(tokens) else {:error, {error_line, :ntriples_lexer, error_descriptor}, _error_line_again} -> {:error, - "N-Triple scanner error on line #{error_line}: #{error_description(error_descriptor)}"} + "N-Triple scanner error#{if error_with_line_number, do: " on line #{error_line}"}: #{ + error_description(error_descriptor) + }"} {:error, {error_line, :ntriples_parser, error_descriptor}} -> {:error, - "N-Triple parser error on line #{error_line}: #{error_description(error_descriptor)}"} + "N-Triple parser error#{if error_with_line_number, do: " on line #{error_line}"}: #{ + error_description(error_descriptor) + }"} end end @@ -28,7 +49,9 @@ defmodule RDF.NTriples.Decoder do defp parse(tokens), do: tokens |> :ntriples_parser.parse() - defp build_graph(ast) do - Enum.reduce(ast, Graph.new(), &Graph.add(&2, &1)) + defp build_graph([]), do: Graph.new() + + defp build_graph([triples]) do + Enum.reduce(triples, Graph.new(), &Graph.add(&2, &1)) end end diff --git a/test/support/rdf_case.ex b/test/support/rdf_case.ex index 67dddff..1f8e2c0 100644 --- a/test/support/rdf_case.ex +++ b/test/support/rdf_case.ex @@ -34,6 +34,11 @@ defmodule RDF.Test.Case do end end + def string_to_stream(string) do + {:ok, pid} = StringIO.open(string) + IO.binstream(pid, :line) + end + ############################### # RDF.Description diff --git a/test/unit/ntriples_decoder_test.exs b/test/unit/ntriples_decoder_test.exs index d5e75a1..4590960 100644 --- a/test/unit/ntriples_decoder_test.exs +++ b/test/unit/ntriples_decoder_test.exs @@ -3,6 +3,7 @@ defmodule RDF.NTriples.DecoderTest do doctest RDF.NTriples.Decoder + alias RDF.NTriples.Decoder alias RDF.Graph use RDF.Vocabulary.Namespace @@ -11,29 +12,36 @@ defmodule RDF.NTriples.DecoderTest do defvocab P, base_iri: "http://www.perceive.net/schemas/relationship/", terms: [], strict: false + import RDF.Sigils + import RDF.Test.Case, only: [string_to_stream: 1] + + test "stream_support?/0" do + assert Decoder.stream_support?() + end + test "an empty string is deserialized to an empty graph" do - assert RDF.NTriples.Decoder.decode!("") == Graph.new() - assert RDF.NTriples.Decoder.decode!(" \n\r\r\n ") == Graph.new() + assert Decoder.decode!("") == Graph.new() + assert Decoder.decode!(" \n\r\r\n ") == Graph.new() end test "decoding comments" do - assert RDF.NTriples.Decoder.decode!("# just a comment") == Graph.new() + assert Decoder.decode!("# just a comment") == Graph.new() - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" _:1 . # a comment """) == Graph.new({EX.S, EX.p(), RDF.bnode("1")}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" # a comment . """) == Graph.new({EX.S, EX.p(), EX.O}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . # a comment """) == Graph.new({EX.S, EX.p(), EX.O}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" # Header line 1 # Header line 2 . @@ -48,17 +56,17 @@ defmodule RDF.NTriples.DecoderTest do end test "empty lines" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . """) == Graph.new({EX.spiderman(), P.enemyOf(), EX.green_goblin()}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . """) == Graph.new({EX.spiderman(), P.enemyOf(), EX.green_goblin()}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . @@ -73,45 +81,45 @@ defmodule RDF.NTriples.DecoderTest do end test "decoding a single triple with iris" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . """) == Graph.new({EX.spiderman(), P.enemyOf(), EX.green_goblin()}) end test "decoding a single triple with a blank node" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" _:foo . """) == Graph.new({RDF.bnode("foo"), EX.p(), EX.O}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" _:1 . """) == Graph.new({EX.S, EX.p(), RDF.bnode("1")}) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" _:foo _:bar . """) == Graph.new({RDF.bnode("foo"), EX.p(), RDF.bnode("bar")}) end test "decoding a single triple with an untyped string literal" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" "Peter Parker" . """) == Graph.new({EX.spiderman(), P.realname(), RDF.literal("Peter Parker")}) end test "decoding a single triple with a typed literal" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" "42"^^ . """) == Graph.new({EX.spiderman(), EX.p(), RDF.literal(42)}) end test "decoding a single triple with a language tagged literal" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" "foo"@en . """) == Graph.new({EX.S, EX.p(), RDF.literal("foo", language: "en")}) end test "decoding multiple triples" do - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . . """) == @@ -120,9 +128,10 @@ defmodule RDF.NTriples.DecoderTest do {EX.S1, EX.p2(), EX.O2} ]) - assert RDF.NTriples.Decoder.decode!(""" + assert Decoder.decode!(""" . . + . """) == Graph.new([ @@ -131,4 +140,22 @@ defmodule RDF.NTriples.DecoderTest do {EX.S2, EX.p3(), EX.O3} ]) end + + test "decode_from_stream/2" do + assert """ + . + + _:foo . + + + "foo"@en . + """ + |> string_to_stream() + |> Decoder.decode_from_stream() == + Graph.new([ + {EX.S1, EX.p1(), EX.O1}, + {EX.S1, EX.p2(), ~B"foo"}, + {EX.S2, EX.p3(), ~L"foo"en} + ]) + end end