diff --git a/lib/rdf.ex b/lib/rdf.ex index 952e574..5a679a8 100644 --- a/lib/rdf.ex +++ b/lib/rdf.ex @@ -60,10 +60,7 @@ defmodule RDF do ## Examples iex> RDF.literal(42) - %RDF.Literal{value: 42, language: nil, - datatype: %URI{authority: "www.w3.org", fragment: "integer", - host: "www.w3.org", path: "/2001/XMLSchema", port: 80, - query: nil, scheme: "http", userinfo: nil}} + %RDF.Literal{value: 42, lexical: "42", datatype: XSD.integer} """ def literal(value) diff --git a/lib/rdf/datatype.ex b/lib/rdf/datatype.ex index 5db37d2..b1e5481 100644 --- a/lib/rdf/datatype.ex +++ b/lib/rdf/datatype.ex @@ -1,11 +1,17 @@ defmodule RDF.Datatype do + alias RDF.Literal alias RDF.Datatype.NS.XSD @callback id :: URI.t @callback convert(any, keyword) :: any - @callback build_literal(any, keyword) :: RDF.Literal.t + @callback build_literal_by_value(binary, keyword) :: RDF.Literal.t + @callback build_literal_by_lexical(binary, keyword) :: RDF.Literal.t + @callback build_literal_(binary, any, keyword) :: RDF.Literal.t + + @callback canonicalize(RDF.Literal.t | any) :: binary + # TODO: This mapping should be created dynamically and be extendable, to allow user-defined datatypes ... @@ -21,7 +27,11 @@ defmodule RDF.Datatype do # XSD.dateTime => RDF.DateTime, } - def for(id), do: @mapping[id] + def ids, do: Map.keys(@mapping) + def modules, do: Map.values(@mapping) + + def get(%Literal{datatype: id}), do: get(id) + def get(id), do: @mapping[id] defmacro __using__(opts) do @@ -35,18 +45,56 @@ defmodule RDF.Datatype do @id unquote(id) def id, do: @id + def new(value, opts \\ %{}) def new(value, opts) when is_list(opts), do: new(value, Map.new(opts)) + def new(value, %{lexical: lexical} = opts), + do: build_literal(lexical, value, opts) + + def new(nil, %{lexical: lexical} = opts), + do: build_literal_by_lexical(lexical, opts) + + def new(value, opts) when is_binary(value), + do: build_literal_by_lexical(value, opts) + def new(value, opts), - do: build_literal(convert(value, opts), opts) + do: build_literal_by_value(convert(value, opts), opts) - def build_literal(value, _), - do: %Literal{value: value, datatype: @id} +# TODO: def new!(value, opts \\ %{}) - defoverridable [build_literal: 2] + + def build_literal_by_value(value, opts) do + build_literal(canonicalize(value), value, opts) + end + + def build_literal_by_lexical(lexical, opts) do + build_literal(lexical, convert(lexical, opts), opts) + end + + def build_literal(lexical, value, _) do + %Literal{lexical: lexical, value: value, datatype: @id} + end + + + def canonicalize(%Literal{value: value, lexical: nil}), + do: canonicalize(value) + + def canonicalize(%Literal{lexical: lexical}), + do: lexical + + def canonicalize(value), + do: to_string(value) + + + defoverridable [ + build_literal_by_value: 2, + build_literal_by_lexical: 2, + build_literal: 3, + canonicalize: 1 + ] end end diff --git a/lib/rdf/datatypes/lang_string.ex b/lib/rdf/datatypes/lang_string.ex index a529b17..0ef6a26 100644 --- a/lib/rdf/datatypes/lang_string.ex +++ b/lib/rdf/datatypes/lang_string.ex @@ -3,11 +3,13 @@ defmodule RDF.LangString do def convert(value, _) when is_binary(value), do: value - def build_literal(value, %{language: language} = opts) do - %Literal{value: value, datatype: @id, language: String.downcase(language)} + def build_literal_by_lexical(lexical, %{language: language} = opts) do + %Literal{ + lexical: lexical, value: lexical, datatype: @id, + language: String.downcase(language)} end - def build_literal(value, opts) do + def build_literal_by_lexical(value, opts) do raise ArgumentError, "datatype of rdf:langString requires a language" end diff --git a/lib/rdf/inspect.ex b/lib/rdf/inspect.ex index ac0d306..18786e5 100644 --- a/lib/rdf/inspect.ex +++ b/lib/rdf/inspect.ex @@ -38,13 +38,13 @@ defmodule RDF.InspectHelper do end defimpl Inspect, for: RDF.Literal do - def inspect(%RDF.Literal{value: value, language: language}, _opts) + def inspect(%RDF.Literal{lexical: lexical, language: language}, _opts) when not is_nil(language) do - "%RDF.Literal{value: #{inspect value}, language: #{inspect language}}" + "%RDF.Literal{lexical: #{inspect lexical}, language: #{inspect language}}" end - def inspect(%RDF.Literal{value: value, datatype: datatype}, _opts) do - "%RDF.Literal{value: #{inspect value}, datatype: ~I<#{datatype}>}" + def inspect(%RDF.Literal{lexical: lexical, datatype: datatype}, _opts) do + "%RDF.Literal{lexical: #{inspect lexical}, datatype: ~I<#{datatype}>}" end end diff --git a/lib/rdf/literal.ex b/lib/rdf/literal.ex index 3d1bfcf..48421f1 100644 --- a/lib/rdf/literal.ex +++ b/lib/rdf/literal.ex @@ -2,12 +2,17 @@ defmodule RDF.Literal do @moduledoc """ RDF literals are leaf nodes of a RDF graph containing raw data, like strings and numbers. """ - defstruct [:value, :datatype, :language] + defstruct [:lexical, :value, :datatype, :language] @type t :: module alias RDF.Datatype.NS.XSD + # to be able to pattern-match on plain types + @xsd_string XSD.string + @lang_string RDF.langString + @plain_types [@xsd_string, @lang_string] + @doc """ Creates a new `RDF.Literal` of the given value and tries to infer an appropriate XSD datatype. @@ -31,7 +36,7 @@ defmodule RDF.Literal do # Examples iex> RDF.Literal.new(42) - %RDF.Literal{value: 42, datatype: XSD.integer} + %RDF.Literal{value: 42, lexical: "42", datatype: XSD.integer} """ def new(value) @@ -70,7 +75,7 @@ defmodule RDF.Literal do do: new(value, Map.delete(opts, :language)) # Should we raise a warning? def new(value, %{datatype: %URI{} = id} = opts) do - case RDF.Datatype.for(id) do + case RDF.Datatype.get(id) do nil -> %RDF.Literal{value: value, datatype: id} literal_type -> literal_type.new(value, opts) end @@ -90,18 +95,16 @@ defmodule RDF.Literal do see """ - def simple?(%RDF.Literal{datatype: datatype}) do - datatype == XSD.string - end + def simple?(%RDF.Literal{datatype: @xsd_string}), do: true + def simple?(foo), do: false @doc """ Checks if a literal is a language-tagged literal. see """ - def has_language?(%RDF.Literal{datatype: datatype}) do - datatype == RDF.langString - end + def has_language?(%RDF.Literal{datatype: @lang_string}), do: true + def has_language?(_), do: false @doc """ Checks if a literal is a datatyped literal. @@ -122,14 +125,22 @@ defmodule RDF.Literal do see """ - def plain?(%RDF.Literal{datatype: datatype}) do - datatype in [RDF.langString, XSD.string] - end + def plain?(%RDF.Literal{datatype: datatype}) + when datatype in @plain_types, do: true + def plain?(_), do: false + def typed?(literal), do: not plain?(literal) + +# end defimpl String.Chars, for: RDF.Literal do - def to_string(%RDF.Literal{value: value}) do + # TODO: remove this when time types were implemented? + def to_string(%RDF.Literal{lexical: nil, value: value}) do Kernel.to_string(value) end + + def to_string(%RDF.Literal{lexical: lexical}) do + lexical + end end diff --git a/test/unit/dataset_test.exs b/test/unit/dataset_test.exs index 3562ad3..6a565c3 100644 --- a/test/unit/dataset_test.exs +++ b/test/unit/dataset_test.exs @@ -281,8 +281,7 @@ defmodule RDF.DatasetTest do end @tag skip: "TODO" - test "a list of Graphs" do - end + test "a list of Graphs" test "duplicates are ignored" do ds = Dataset.add(dataset(), {EX.Subject, EX.predicate, EX.Object, EX.GraphName}) diff --git a/test/unit/literal_test.exs b/test/unit/literal_test.exs index 56f4f6d..e341dcc 100644 --- a/test/unit/literal_test.exs +++ b/test/unit/literal_test.exs @@ -4,92 +4,69 @@ defmodule RDF.LiteralTest do import RDF.Sigils import RDF.TestLiterals - alias RDF.{Literal} + alias RDF.Literal alias RDF.NS.XSD doctest RDF.Literal describe "construction by type inference" do - test "creating an string literal" do - string_literal = Literal.new("foo") - assert string_literal.value == "foo" - assert string_literal.datatype == XSD.string + test "string" do + assert Literal.new("foo") == RDF.String.new("foo") end - test "creating an integer by type inference" do - int_literal = Literal.new(42) - assert int_literal.value == 42 - assert int_literal.datatype == XSD.integer + test "integer" do + assert Literal.new(42) == RDF.Integer.new(42) end - test "creating a boolean by type inference" do - int_literal = Literal.new(true) - assert int_literal.value == true - assert int_literal.datatype == XSD.boolean + test "double" do + assert Literal.new(3.14) == RDF.Double.new(3.14) + end - int_literal = Literal.new(false) - assert int_literal.value == false - assert int_literal.datatype == XSD.boolean + test "boolean" do + assert Literal.new(true) == RDF.Boolean.new(true) + assert Literal.new(false) == RDF.Boolean.new(false) + end + + @tag skip: "TODO" + test "when options without datatype given" + end + + describe "typed construction" do + test "boolean" do + assert Literal.new(true, datatype: XSD.boolean) == RDF.Boolean.new(true) + assert Literal.new(false, datatype: XSD.boolean) == RDF.Boolean.new(false) + assert Literal.new("true", datatype: XSD.boolean) == RDF.Boolean.new("true") + assert Literal.new("false", datatype: XSD.boolean) == RDF.Boolean.new("false") + end + + test "integer" do + assert Literal.new(42, datatype: XSD.integer) == RDF.Integer.new(42) + assert Literal.new("42", datatype: XSD.integer) == RDF.Integer.new("42") + end + + + + test "unknown datatype" do + literal = Literal.new("custom typed value", datatype: "http://example/dt") + assert literal.value == "custom typed value" + assert literal.datatype == ~I end end - describe "construction with an explicit unknown datatype" do - literal = Literal.new("custom typed value", datatype: "http://example/dt") - assert literal.value == "custom typed value" - assert literal.datatype == ~I - end - describe "construction with an explicit known (XSD) datatype" do - test "creating a boolean" do - bool_literal = Literal.new("true", datatype: XSD.boolean) - assert bool_literal.value == true - assert bool_literal.datatype == XSD.boolean - - bool_literal = Literal.new(true, datatype: XSD.boolean) - assert bool_literal.value == true - assert bool_literal.datatype == XSD.boolean - - bool_literal = Literal.new("false", datatype: XSD.boolean) - assert bool_literal.value == false - assert bool_literal.datatype == XSD.boolean - - bool_literal = Literal.new(false, datatype: XSD.boolean) - assert bool_literal.value == false - assert bool_literal.datatype == XSD.boolean - end - - test "creating an integer" do - int_literal = Literal.new(42, datatype: XSD.integer) - assert int_literal.value == 42 - assert int_literal.datatype == XSD.integer - - int_literal = Literal.new("42", datatype: XSD.integer) - assert int_literal.value == 42 - assert int_literal.datatype == XSD.integer - - int_literal = Literal.new(true, datatype: XSD.integer) - assert int_literal.value == 1 - assert int_literal.datatype == XSD.integer - int_literal = Literal.new(false, datatype: XSD.integer) - assert int_literal.value == 0 - assert int_literal.datatype == XSD.integer - end - - end - - describe "language-tags" do - test "creating a string literal with a language tag" do + describe "language tagged construction" do + test "string literal with a language tag" do literal = Literal.new("Eule", language: "de") - assert literal.value == "Eule" + assert literal.value == "Eule" assert literal.datatype == RDF.langString assert literal.language == "de" end - test "when given a language, but the value is not a string, language is ignored" do + test "language is ignored on non-string literals" do literal = Literal.new(1, language: "de") - assert literal.value == 1 + assert literal.value == 1 assert literal.datatype == XSD.integer refute literal.language end diff --git a/test/unit/rdf_test.exs b/test/unit/rdf_test.exs index a7c87e6..c253230 100644 --- a/test/unit/rdf_test.exs +++ b/test/unit/rdf_test.exs @@ -4,6 +4,8 @@ defmodule RDF.CoreTest do use RDF.Vocabulary.Namespace defvocab EX, base_uri: "http://example.com/", terms: [], strict: false + alias RDF.NS.XSD + doctest RDF # alias RDF.{Triple, Literal, BlankNode}