diff --git a/CHANGELOG.md b/CHANGELOG.md index 121d3e3..732ceea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,13 @@ This project adheres to [Semantic Versioning](http://semver.org/) and ### Added - `RDF.Serialization.Format`s define a `name` atom -- The following functions to access available `RDF.Serialization.Format`s: +- all `RDF.Serialization.Reader` and `RDF.Serialization.Writer` functions are now + available on the `RDF.Serialization` module (or aliased on the top-level `RDF` + module) and the format can be specified instead of a `RDF.Serialization.Format` + argument, via the `format` or `media_type` option or in case of `*_file` + functions, without explicit specification of the format, but inferred from file + name extension instead; see the updated README section about RDF serializations +- the following functions to access available `RDF.Serialization.Format`s: - `RDF.Serialization.formats/0` - `RDF.Serialization.available_formats/0` - `RDF.Serialization.format/1` diff --git a/lib/rdf.ex b/lib/rdf.ex index 72b4a72..ad77841 100644 --- a/lib/rdf.ex +++ b/lib/rdf.ex @@ -20,8 +20,9 @@ defmodule RDF do - `RDF.Dataset` - `RDF.Data` - `RDF.List` - - the foundations for the definition of RDF serialization formats - - `RDF.Serialization` + - functions for working with RDF serializations: `RDF.Serialization` + - behaviours for the definition of RDF serialization formats + - `RDF.Serialization.Format` - `RDF.Serialization.Decoder` - `RDF.Serialization.Encoder` - and the implementation of various RDF serialization formats @@ -38,6 +39,16 @@ defmodule RDF do alias RDF.{IRI, Namespace, Literal, BlankNode, Triple, Quad, Description, Graph, Dataset} + defdelegate read_string(content, opts), to: RDF.Serialization + defdelegate read_string!(content, opts), to: RDF.Serialization + defdelegate read_file(filename, opts \\ []), to: RDF.Serialization + defdelegate read_file!(filename, opts \\ []), to: RDF.Serialization + defdelegate write_string(content, opts), to: RDF.Serialization + defdelegate write_string!(content, opts), to: RDF.Serialization + defdelegate write_file(filename, opts \\ []), to: RDF.Serialization + defdelegate write_file!(filename, opts \\ []), to: RDF.Serialization + + @doc """ Checks if the given value is a RDF resource. diff --git a/lib/rdf/serialization/serialization.ex b/lib/rdf/serialization/serialization.ex index ec087f8..d7be716 100644 --- a/lib/rdf/serialization/serialization.ex +++ b/lib/rdf/serialization/serialization.ex @@ -90,9 +90,15 @@ defmodule RDF.Serialization do iex> RDF.Serialization.format_by_extension("ttl") RDF.Turtle + iex> RDF.Serialization.format_by_extension(".ttl") + RDF.Turtle iex> RDF.Serialization.format_by_extension("jsonld") nil # unless json_ld is defined as a dependency of the application """ + def format_by_extension(extension) + + def format_by_extension("." <> extension), do: format_by_extension(extension) + def format_by_extension(extension) do format_where(fn format -> format.extension == extension end) end @@ -102,4 +108,169 @@ defmodule RDF.Serialization do |> Stream.filter(&Code.ensure_loaded?/1) |> Enum.find(fun) end + + + @doc """ + Reads and decodes a serialized graph or dataset from a string. + + The format must be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. + + It returns an `{:ok, data}` tuple, with `data` being the deserialized graph or + dataset, or `{:error, reason}` if an error occurs. + """ + def read_string(content, opts) do + with {:ok, format} <- string_format(opts) do + format.read_string(content, opts) + end + end + + @doc """ + Reads and decodes a serialized graph or dataset from a string. + + The format must be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. + + As opposed to `read_string`, it raises an exception if an error occurs. + """ + def read_string!(content, opts) do + with {:ok, format} <- string_format(opts) do + format.read_string!(content, opts) + else + {:error, error} -> raise error + end + end + + @doc """ + Reads and decodes a serialized graph or dataset from a file. + + The format can be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. If none of these are + given, the format gets inferred from the extension of the given file name. + + It returns an `{:ok, data}` tuple, with `data` being the deserialized graph or + dataset, or `{:error, reason}` if an error occurs. + """ + def read_file(file, opts \\ []) do + with {:ok, format} <- file_format(file, opts) do + format.read_file(file, opts) + end + end + + @doc """ + Reads and decodes a serialized graph or dataset from a file. + + The format can be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. If none of these are + given, the format gets inferred from the extension of the given file name. + + As opposed to `read_file`, it raises an exception if an error occurs. + """ + def read_file!(file, opts \\ []) do + with {:ok, format} <- file_format(file, opts) do + format.read_file!(file, opts) + else + {:error, error} -> raise error + end + end + + @doc """ + Encodes and writes a graph or dataset to a string. + + The format must be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. + + It returns an `{:ok, string}` tuple, with `string` being the serialized graph or + dataset, or `{:error, reason}` if an error occurs. + """ + def write_string(data, opts) do + with {:ok, format} <- string_format(opts) do + format.write_string(data, opts) + end + end + + @doc """ + Encodes and writes a graph or dataset to a string. + + The format must be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. + + As opposed to `write_string`, it raises an exception if an error occurs. + """ + def write_string!(data, opts) do + with {:ok, format} <- string_format(opts) do + format.write_string!(data, opts) + else + {:error, error} -> raise error + end + end + + @doc """ + Encodes and writes a graph or dataset to a file. + + The format can be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. If none of these are + given, the format gets inferred from the extension of the given file name. + + Other available serialization-independent options: + + - `:force` - If not set to `true`, an error is raised when the given file + already exists (default: `false`) + - `:file_mode` - A list with the Elixir `File.open` modes to be used fior writing + (default: `[:utf8, :write]`) + + It returns `:ok` if successfull or `{:error, reason}` if an error occurs. + """ + def write_file(data, path, opts \\ []) do + with {:ok, format} <- file_format(path, opts) do + format.write_file(data, path, opts) + end + end + + @doc """ + Encodes and writes a graph or dataset to a file. + + The format can be specified with the `format` option and a format name or the + `media_type` option and the media type of the format. If none of these are + given, the format gets inferred from the extension of the given file name. + + See `write_file` for a list of other available options. + + As opposed to `write_file`, it raises an exception if an error occurs. + """ + def write_file!(data, path, opts \\ []) do + with {:ok, format} <- file_format(path, opts) do + format.write_file!(data, path, opts) + else + {:error, error} -> raise error + end + end + + + defp string_format(opts) do + if format = + (opts |> Keyword.get(:format) |> format()) || + (opts |> Keyword.get(:media_type) |> format_by_media_type()) + do + {:ok, format} + else + {:error, "unable to detect serialization format"} + end + end + + defp file_format(filename, opts) do + case string_format(opts) do + {:ok, format} -> {:ok, format} + _ -> format_by_file_name(filename) + end + end + + defp format_by_file_name(filename) do + if format = filename |> Path.extname() |> format_by_extension() do + {:ok, format} + else + {:error, "unable to detect serialization format"} + end + end + end diff --git a/test/data/cbd.ttl b/test/data/cbd.ttl new file mode 100644 index 0000000..9a1678c --- /dev/null +++ b/test/data/cbd.ttl @@ -0,0 +1,47 @@ + @prefix dc: . + @prefix dc11: . + @prefix foaf: . + @prefix owl: . + @prefix rdf: . + @prefix rdfs: . + @prefix xsd: . + + ; + . + dc11:extent "1234"; + dc11:format "image/jpeg"; + a foaf:Image . + foaf:mbox a rdf:Property, + owl:InverseFunctionalProperty . + dc11:creator "June Doe (june@example.com)"; + dc11:format "application/pdf"; + dc11:language "en"; + dc11:publisher "Examples-R-Us"; + dc11:rights "Copyright (C) 2004 Examples-R-Us. All rights reserved."; + dc11:title "Another Great Book"; + dc:issued "2004-05-03"^^xsd:date; + rdfs:seeAlso . + dc11:contributor [ a foaf:Person; + foaf:name "Jane Doe"]; + dc11:creator [ a foaf:Person; + foaf:img ; + foaf:mbox "john@example.com"; + foaf:name "John Doe"; + foaf:phone ]; + dc11:format "application/pdf"; + dc11:language "en"; + dc11:publisher "Examples-R-Us"; + dc11:rights "Copyright (C) 2004 Examples-R-Us. All rights reserved."; + dc11:title "A Really Great Book"; + dc:issued "2004-01-19"^^xsd:date; + rdfs:seeAlso . + [ rdf:object "image/jpeg"; + rdf:predicate dc11:format; + rdf:subject foaf:Image; + a rdf:Statement; + rdfs:isDefinedBy ] . + [ rdf:object "application/pdf"; + rdf:predicate dc11:format; + rdf:subject ; + a rdf:Statement; + rdfs:isDefinedBy ] . diff --git a/test/unit/serialization/serialization_test.exs b/test/unit/serialization/serialization_test.exs index 3b3914f..3fd4ee8 100644 --- a/test/unit/serialization/serialization_test.exs +++ b/test/unit/serialization/serialization_test.exs @@ -2,4 +2,205 @@ defmodule RDF.SerializationTest do use ExUnit.Case doctest RDF.Serialization + + use RDF.Vocabulary.Namespace + + defvocab EX, + base_iri: "http://example.org/", + terms: [], strict: false + + @example_turtle_file "test/data/cbd.ttl" + @example_turtle_string """ + @prefix ex: . + ex:Aaron ex:Person . + """ + + @example_graph RDF.Graph.new [{EX.S, EX.p, EX.O}] + @example_graph_turtle """ + @prefix : <#{to_string(EX. __base_iri__)}> . + + :S + :p :O . + """ + + defp file(name), do: System.tmp_dir!() |> Path.join(name) + + + describe "read_string/2" do + test "with correct format name" do + assert {:ok, %RDF.Graph{}} = + RDF.Serialization.read_string(@example_turtle_string, format: :turtle) + end + + test "with wrong format name" do + assert {:error, "N-Triple scanner error" <> _} = + RDF.Serialization.read_string(@example_turtle_string, format: :ntriples) + end + + test "with invalid format name" do + assert {:error, "unable to detect serialization format"} == + RDF.Serialization.read_string(@example_turtle_string, format: :foo) + end + + test "with media_type" do + assert {:ok, %RDF.Graph{}} = + RDF.Serialization.read_string(@example_turtle_string, media_type: "text/turtle") + end + end + + describe "read_string!/2" do + test "with correct format name" do + assert %RDF.Graph{} = + RDF.Serialization.read_string!(@example_turtle_string, format: :turtle) + end + + test "with wrong format name" do + assert_raise RuntimeError, ~r/^N-Triple scanner error.*/, fn -> + RDF.Serialization.read_string!(@example_turtle_string, format: :ntriples) + end + end + + test "with invalid format name" do + assert_raise RuntimeError, "unable to detect serialization format", fn -> + RDF.Serialization.read_string!(@example_turtle_string, format: :foo) + end + end + + test "with media_type" do + assert %RDF.Graph{} = + RDF.Serialization.read_string!(@example_turtle_string, media_type: "text/turtle") + end + end + + describe "read_file/2" do + test "without arguments, i.e. via correct file extension" do + assert {:ok, %RDF.Graph{}} = RDF.Serialization.read_file(@example_turtle_file) + end + + test "with correct format name" do + assert {:ok, %RDF.Graph{}} = + RDF.Serialization.read_file(@example_turtle_file, format: :turtle) + end + + test "with wrong format name" do + assert {:error, "N-Triple scanner error" <> _} = + RDF.Serialization.read_file(@example_turtle_file, format: :ntriples) + end + + test "with invalid format name, but correct file extension" do + assert {:ok, %RDF.Graph{}} = RDF.Serialization.read_file(@example_turtle_file, format: :foo) + end + + test "with media_type" do + assert {:ok, %RDF.Graph{}} = + RDF.Serialization.read_file(@example_turtle_file, media_type: "text/turtle") + end + end + + describe "read_file!/2" do + test "without arguments, i.e. via correct file extension" do + assert %RDF.Graph{} = RDF.Serialization.read_file!(@example_turtle_file) + end + + test "with correct format name" do + assert %RDF.Graph{} = + RDF.Serialization.read_file!(@example_turtle_file, format: :turtle) + end + + test "with wrong format name" do + assert_raise RuntimeError, ~r/^N-Triple scanner error.*/, fn -> + RDF.Serialization.read_file!(@example_turtle_file, format: :ntriples) + end + end + + test "with media_type name" do + assert %RDF.Graph{} = + RDF.Serialization.read_file!(@example_turtle_file, media_type: "text/turtle") + end + end + + describe "write_string/2" do + test "with name of available format" do + assert RDF.Serialization.write_string(@example_graph, format: :turtle, + prefixes: %{"" => EX. __base_iri__}) == + {:ok, @example_graph_turtle} + end + + test "with invalid format name" do + assert RDF.Serialization.write_string(@example_graph, format: :foo, + prefixes: %{"" => EX. __base_iri__}) == + {:error, "unable to detect serialization format"} + end + + test "with media type" do + assert RDF.Serialization.write_string(@example_graph, media_type: "text/turtle", + prefixes: %{"" => EX. __base_iri__}) == + {:ok, @example_graph_turtle} + end + end + + describe "write_string!/2" do + test "with name of available format" do + assert RDF.Serialization.write_string!(@example_graph, format: :turtle, + prefixes: %{"" => EX. __base_iri__}) == + @example_graph_turtle + end + + test "with invalid format name" do + assert_raise RuntimeError, "unable to detect serialization format", fn -> + RDF.Serialization.write_string!(@example_graph, format: :foo, + prefixes: %{"" => EX. __base_iri__}) + end + end + + test "with media type" do + assert RDF.Serialization.write_string!(@example_graph, media_type: "text/turtle", + prefixes: %{"" => EX. __base_iri__}) == + @example_graph_turtle + end + end + + describe "write_file/2" do + test "without arguments, i.e. via file extension" do + file = file("write_file_test.ttl") + if File.exists?(file), do: File.rm(file) + assert RDF.Serialization.write_file(@example_graph, file, + prefixes: %{"" => EX. __base_iri__}) == :ok + assert File.exists?(file) + assert File.read!(file) == @example_graph_turtle + File.rm(file) + end + + test "with format name" do + file = file("write_file_test.nt") + if File.exists?(file), do: File.rm(file) + assert RDF.Serialization.write_file(@example_graph, file, format: :turtle, + prefixes: %{"" => EX. __base_iri__}) == :ok + assert File.exists?(file) + assert File.read!(file) == @example_graph_turtle + File.rm(file) + end + end + + describe "write_file!/2" do + test "without arguments, i.e. via file extension" do + file = file("write_file_test.ttl") + if File.exists?(file), do: File.rm(file) + assert RDF.Serialization.write_file!(@example_graph, file, + prefixes: %{"" => EX. __base_iri__}) == :ok + assert File.exists?(file) + assert File.read!(file) == @example_graph_turtle + File.rm(file) + end + + test "with format name" do + file = file("write_file_test.nt") + if File.exists?(file), do: File.rm(file) + assert RDF.Serialization.write_file!(@example_graph, file, format: :turtle, + prefixes: %{"" => EX. __base_iri__}) == :ok + assert File.exists?(file) + assert File.read!(file) == @example_graph_turtle + File.rm(file) + end + end end