core: yet another approach for RDF.Literal

- we now only the store the lexical form when it's non-canonical
- Literal validation and canonicalization
- a general RDF.Datatype.Test.Case
- also tested datatype implementations for booleans, integers, string and langStrings
- use literal sigils in Inspect implementation of Literals when possible
This commit is contained in:
Marcel Otto 2017-04-23 23:41:29 +02:00
parent d812998fb0
commit 2b9aa62d69
16 changed files with 645 additions and 84 deletions

View file

@ -60,7 +60,7 @@ defmodule RDF do
## Examples
iex> RDF.literal(42)
%RDF.Literal{value: 42, lexical: "42", datatype: XSD.integer}
%RDF.Literal{value: 42, datatype: XSD.integer}
"""
def literal(value)

View file

@ -4,14 +4,31 @@ defmodule RDF.Datatype do
@callback id :: URI.t
@callback lexical(RDF.Literal.t) :: any
@callback canonical_lexical(any) :: binary
@callback canonical(RDF.Literal.t) :: RDF.Literal.t
@doc """
Converts a value into a proper native value.
If an invalid value is given an implementation should call `super`, which
by default currently just returns `nil`.
Note: If a value is valid is determined by the lexical space of the implemented
datatype, not by the Elixir semantics. For example, although `42`
is a falsy value according to the Elixir semantics, this is not an element
of the lexical value space of an `xsd:boolean`, so the `RDF.Boolean`
implementation of this datatype calls `super`.
"""
@callback convert(any, keyword) :: any
@callback valid?(RDF.Literal.t) :: boolean
@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
@callback build_literal(any, binary, keyword) :: RDF.Literal.t
# TODO: This mapping should be created dynamically and be extendable, to allow user-defined datatypes ...
@ -50,50 +67,87 @@ defmodule RDF.Datatype do
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_by_value(convert(value, opts), opts)
do: build_literal_by_value(value, opts)
# TODO: def new!(value, opts \\ %{})
def new!(value, opts \\ %{}) do
literal = new(value, opts)
if valid?(literal) do
literal
else
raise ArgumentError, "#{inspect value} is not a valid #{inspect __MODULE__}"
end
end
def build_literal_by_value(value, opts) do
build_literal(canonicalize(value), value, opts)
case convert(value, opts) do
nil ->
build_literal(nil, canonical_lexical(value), opts)
converted_value ->
build_literal(converted_value, nil, opts)
end
end
def build_literal_by_lexical(lexical, opts) do
build_literal(lexical, convert(lexical, opts), opts)
case convert(lexical, opts) do
nil ->
build_literal(nil, lexical, opts)
value ->
if opts[:canonicalize] || lexical == canonical_lexical(value) do
build_literal(value, nil, opts)
else
build_literal(value, lexical, opts)
end
end
end
def build_literal(lexical, value, _) do
%Literal{lexical: lexical, value: value, datatype: @id}
def build_literal(value, lexical, %{canonicalize: true} = opts) do
build_literal(value, lexical, Map.delete(opts, :canonicalize))
|> canonical
end
def build_literal(value, lexical, opts) do
%Literal{value: value, uncanonical_lexical: lexical, datatype: @id}
end
def canonicalize(%Literal{value: value, lexical: nil}),
do: canonicalize(value)
def convert(value, _), do: nil
def canonicalize(%Literal{lexical: lexical}),
do: lexical
def canonicalize(value),
do: to_string(value)
def lexical(%RDF.Literal{value: value, uncanonical_lexical: nil}) do
canonical_lexical(value)
end
def lexical(%RDF.Literal{uncanonical_lexical: lexical}) do
lexical
end
def canonical_lexical(value), do: to_string(value)
def canonical(%Literal{value: nil} = literal), do: literal
def canonical(%Literal{uncanonical_lexical: nil} = literal), do: literal
def canonical(%Literal{} = literal) do
%Literal{literal | uncanonical_lexical: nil}
end
def valid?(%Literal{value: nil}), do: false
def valid?(%Literal{datatype: @id}), do: true
def valid?(_), do: false
defoverridable [
build_literal_by_value: 2,
build_literal_by_lexical: 2,
build_literal: 3,
canonicalize: 1
lexical: 1,
canonical_lexical: 1,
convert: 2,
valid?: 1,
]
end
end

View file

@ -1,8 +1,23 @@
defmodule RDF.Boolean do
use RDF.Datatype, id: RDF.Datatype.NS.XSD.boolean
def convert(value, _) when is_boolean(value), do: value
def convert(value, _) when is_integer(value), do: value == 1
def convert(value, _) when is_binary(value), do: String.downcase(value) == "true"
def convert(value, opts) when is_binary(value) do
with normalized_value = String.downcase(value) do
cond do
normalized_value in ~W[true 1] -> true
normalized_value in ~W[false 0] -> false
true ->
super(value, opts)
end
end
end
def convert(1, _), do: true
def convert(0, _), do: false
def convert(value, opts), do: super(value, opts)
end

View file

@ -1,9 +1,17 @@
defmodule RDF.Integer do
use RDF.Datatype, id: RDF.Datatype.NS.XSD.integer
def convert(value, _) when is_integer(value), do: value
def convert(value, _) when is_binary(value), do: String.to_integer(value)
def convert(false, _), do: 0
def convert(true, _), do: 1
def convert(value, opts) when is_binary(value) do
case Integer.parse(value) do
{integer, ""} -> integer
{integer, _} -> super(value, opts)
:error -> super(value, opts)
end
end
def convert(value, opts), do: super(value, opts)
end

View file

@ -1,15 +1,19 @@
defmodule RDF.LangString do
use RDF.Datatype, id: RDF.langString
def convert(value, _) when is_binary(value), do: value
def build_literal_by_lexical(lexical, %{language: language} = opts) do
%Literal{
lexical: lexical, value: lexical, datatype: @id,
language: String.downcase(language)}
def convert(value, _), do: to_string(value)
def valid?(%Literal{language: nil}), do: false
def valid?(literal), do: super(literal)
def build_literal(value, lexical, %{language: language} = opts) do
%Literal{super(value, lexical, opts) | language: String.downcase(language)}
end
def build_literal_by_lexical(value, opts) do
def build_literal(_, _, _) do
raise ArgumentError, "datatype of rdf:langString requires a language"
end

View file

@ -1,6 +1,12 @@
defmodule RDF.String do
use RDF.Datatype, id: RDF.Datatype.NS.XSD.string
def convert(value, _), do: to_string(value)
def build_literal_by_lexical(lexical, opts) do
build_literal(lexical, nil, opts)
end
end

View file

@ -38,13 +38,21 @@ defmodule RDF.InspectHelper do
end
defimpl Inspect, for: RDF.Literal do
def inspect(%RDF.Literal{lexical: lexical, language: language}, _opts)
when not is_nil(language) do
"%RDF.Literal{lexical: #{inspect lexical}, language: #{inspect language}}"
def inspect(%RDF.Literal{value: value, language: language}, _opts) when not is_nil(language) do
~s[~L"#{value}"#{language}]
end
def inspect(%RDF.Literal{lexical: lexical, datatype: datatype}, _opts) do
"%RDF.Literal{lexical: #{inspect lexical}, datatype: ~I<#{datatype}>}"
def inspect(%RDF.Literal{value: value, uncanonical_lexical: lexical, datatype: datatype}, _opts)
when not is_nil(lexical) do
"%RDF.Literal{value: #{inspect value}, lexical: #{inspect lexical}, datatype: ~I<#{datatype}>}"
end
def inspect(%RDF.Literal{value: value, datatype: datatype}, _opts) do
if datatype == RDF.NS.XSD.string do
~s[~L"#{value}"]
else
"%RDF.Literal{value: #{inspect value}, datatype: ~I<#{datatype}>}"
end
end
end

View file

@ -2,7 +2,7 @@ defmodule RDF.Literal do
@moduledoc """
RDF literals are leaf nodes of a RDF graph containing raw data, like strings and numbers.
"""
defstruct [:lexical, :value, :datatype, :language]
defstruct [:value, :uncanonical_lexical, :datatype, :language]
@type t :: module
@ -23,7 +23,7 @@ defmodule RDF.Literal do
| Elixir type | XSD datatype |
| :-------------- | :----------- |
| `string` | `string` |
| `string` | `string` |
| `boolean` | `boolean` |
| `integer` | `integer` |
| `float` | `double` |
@ -36,7 +36,7 @@ defmodule RDF.Literal do
# Examples
iex> RDF.Literal.new(42)
%RDF.Literal{value: 42, lexical: "42", datatype: XSD.integer}
%RDF.Literal{value: 42, datatype: XSD.integer}
"""
def new(value)
@ -63,21 +63,22 @@ defmodule RDF.Literal do
def new(value, opts) when is_list(opts),
do: new(value, Map.new(opts))
def new(value, %{language: language} = opts) when not is_nil(language) and is_binary(value) do
if not opts[:datatype] in [nil, RDF.langString] do
raise ArgumentError, "datatype with language must be rdf:langString"
def new(value, %{language: language} = opts) when not is_nil(language) do
if is_binary(value) do
if opts[:datatype] in [nil, RDF.langString] do
RDF.LangString.new(value, opts)
else
raise ArgumentError, "datatype with language must be rdf:langString"
end
else
RDF.LangString.new(value, opts)
new(value, Map.delete(opts, :language)) # Should we raise a warning?
end
end
def new(value, %{language: language} = opts) when not is_nil(language),
do: new(value, Map.delete(opts, :language)) # Should we raise a warning?
def new(value, %{datatype: %URI{} = id} = opts) do
case RDF.Datatype.get(id) do
nil -> %RDF.Literal{value: value, datatype: id}
literal_type -> literal_type.new(value, opts)
nil -> %RDF.Literal{value: value, datatype: id}
datatype -> datatype.new(value, opts)
end
end
@ -88,6 +89,37 @@ defmodule RDF.Literal do
do: new(value)
def lexical(%RDF.Literal{value: value, uncanonical_lexical: nil, datatype: id} = literal) do
case RDF.Datatype.get(id) do
nil -> to_string(value)
datatype -> datatype.lexical(literal)
end
end
def lexical(%RDF.Literal{uncanonical_lexical: lexical}), do: lexical
def canonical(%RDF.Literal{uncanonical_lexical: nil} = literal), do: literal
def canonical(%RDF.Literal{datatype: id} = literal) do
case RDF.Datatype.get(id) do
nil -> literal
datatype -> datatype.canonical(literal)
end
end
def canonical?(%RDF.Literal{uncanonical_lexical: nil}), do: true
def canonical?(_), do: false
def valid?(%RDF.Literal{datatype: id} = literal) do
case RDF.Datatype.get(id) do
nil -> true
datatype -> datatype.valid?(literal)
end
end
@doc """
Checks if a literal is a simple literal.
@ -98,6 +130,7 @@ defmodule RDF.Literal do
def simple?(%RDF.Literal{datatype: @xsd_string}), do: true
def simple?(foo), do: false
@doc """
Checks if a literal is a language-tagged literal.
@ -106,6 +139,7 @@ defmodule RDF.Literal do
def has_language?(%RDF.Literal{datatype: @lang_string}), do: true
def has_language?(_), do: false
@doc """
Checks if a literal is a datatyped literal.
@ -117,6 +151,7 @@ defmodule RDF.Literal do
not plain?(literal) and not has_language?(literal)
end
@doc """
Checks if a literal is a plain literal.
@ -131,16 +166,10 @@ defmodule RDF.Literal do
def typed?(literal), do: not plain?(literal)
#
end
defimpl String.Chars, for: RDF.Literal 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
def to_string(literal) do
RDF.Literal.lexical(literal)
end
end

View file

@ -35,10 +35,10 @@ defmodule RDF.Sigils do
defmacro sigil_L(value, language)
defmacro sigil_L({:<<>>, _, [value]}, []) when is_binary(value) do
Macro.escape(RDF.Literal.new(value))
Macro.escape(RDF.String.new(value))
end
defmacro sigil_L({:<<>>, _, [value]}, language) when is_binary(value) do
Macro.escape(RDF.Literal.new(value, %{language: to_string(language)}))
Macro.escape(RDF.LangString.new(value, %{language: to_string(language)}))
end
end

View file

@ -12,10 +12,10 @@ defmodule RDF.Test.Case do
using do
quote do
alias RDF.{Dataset, Graph, Description}
alias RDF.Test.Case.EX
alias unquote(__MODULE__).EX
import RDF, only: [uri: 1, literal: 1, bnode: 1]
import RDF.Test.Case
import unquote(__MODULE__)
end
end

View file

@ -0,0 +1,180 @@
defmodule RDF.Datatype.Test.Case do
use ExUnit.CaseTemplate
use RDF.Vocabulary.Namespace
defvocab EX,
base_uri: "http://example.com/",
terms: [], strict: false
alias RDF.{Literal, Datatype}
using(opts) do
datatype = Keyword.fetch!(opts, :datatype)
datatype_id = Keyword.fetch!(opts, :id)
valid = Keyword.get(opts, :valid)
invalid = Keyword.get(opts, :invalid)
quote do
alias RDF.{Literal, Datatype}
alias RDF.NS.XSD
alias unquote(datatype)
alias unquote(__MODULE__).EX
import unquote(__MODULE__)
@moduletag datatype: unquote(datatype)
if unquote(valid) do
@valid unquote(valid)
@invalid unquote(invalid)
describe "general new" do
Enum.each @valid, fn {input, {value, lexical, _}} ->
expected_literal =
%Literal{value: value, uncanonical_lexical: lexical, datatype: unquote(datatype_id), language: nil}
@tag example: %{input: input, output: expected_literal}
test "valid: #{unquote(datatype)}.new(#{inspect input}) == #{inspect expected_literal}",
%{example: example} do
assert unquote(datatype).new(example.input) == example.output
end
end
Enum.each @invalid, fn value ->
expected_literal =
%Literal{uncanonical_lexical: to_string(value), datatype: unquote(datatype_id), language: nil}
@tag example: %{input: value, output: expected_literal}
test "invalid: #{unquote(datatype)}.new(#{inspect value}) == #{inspect expected_literal}",
%{example: example} do
assert unquote(datatype).new(example.input) == example.output
end
end
# valid value with canonical option
Enum.each @valid, fn {input, {value, _, _}} ->
expected_literal =
%Literal{value: value, datatype: unquote(datatype_id), language: nil}
@tag example: %{input: input, output: expected_literal}
test "valid: #{unquote(datatype)}.new(#{inspect input}, canonicalize: true) == #{inspect expected_literal}",
%{example: example} do
assert unquote(datatype).new(example.input, canonicalize: true) == example.output
end
end
# invalid value with canonical option
Enum.each @invalid, fn value ->
expected_literal =
%Literal{uncanonical_lexical: to_string(value), datatype: unquote(datatype_id), language: nil}
@tag example: %{input: value, output: expected_literal}
test "invalid: #{unquote(datatype)}.new(#{inspect value}, canonicalize: true) == #{inspect expected_literal}",
%{example: example} do
assert unquote(datatype).new(example.input, canonicalize: true) == example.output
end
end
test "datatype option is ignored" do
Enum.each Datatype.ids, fn id ->
Enum.each @valid, fn {input, _} ->
assert unquote(datatype).new(input, datatype: id) == unquote(datatype).new(input)
end
end
end
test "language option is ignored" do
Enum.each @valid, fn {input, _} ->
assert unquote(datatype).new(input, language: "en") == unquote(datatype).new(input)
end
end
end
describe "general new!" do
test "with valid values, it behaves the same as new" do
Enum.each @valid, fn {input, _} ->
assert unquote(datatype).new!(input) == unquote(datatype).new(input)
assert unquote(datatype).new!(input, datatype: unquote(datatype_id)) == unquote(datatype).new(input)
assert unquote(datatype).new!(input, canonicalize: true) == unquote(datatype).new(input, canonicalize: true)
end
end
test "with invalid values, it raises an error" do
Enum.each @invalid, fn value ->
assert_raise ArgumentError, fn -> unquote(datatype).new!(value) end
assert_raise ArgumentError, fn -> unquote(datatype).new!(value, canonicalize: true) end
end
end
end
describe "general lexical" do
Enum.each @valid, fn {input, {_, lexical, canonicalized}} ->
lexical = lexical || canonicalized
@tag example: %{input: input, lexical: lexical}
test "of valid #{unquote(datatype)}.new(#{inspect input}) == #{inspect lexical}",
%{example: example} do
assert (unquote(datatype).new(example.input) |> Literal.lexical) == example.lexical
end
end
Enum.each @invalid, fn value ->
lexical = to_string(value)
@tag example: %{input: value, lexical: lexical}
test "of invalid #{unquote(datatype)}.new(#{inspect value}) == #{inspect lexical}",
%{example: example} do
assert (unquote(datatype).new(example.input) |> Literal.lexical) == example.lexical
end
end
end
describe "general canonicalization" do
Enum.each @valid, fn {input, {value, _, _}} ->
expected_literal =
%Literal{value: value, datatype: unquote(datatype_id), language: nil}
@tag example: %{input: input, output: expected_literal}
test "#{unquote(datatype)} #{inspect input} is canonicalized #{inspect expected_literal}",
%{example: example} do
assert (unquote(datatype).new(example.input) |> Literal.canonical) == example.output
end
end
Enum.each @valid, fn {input, {_, _, canonicalized}} ->
@tag example: %{input: input, canonicalized: canonicalized}
test "lexical of canonicalized #{unquote(datatype)} #{inspect input} is #{inspect canonicalized}",
%{example: example} do
assert (unquote(datatype).new(example.input) |> Literal.canonical |> Literal.lexical) ==
example.canonicalized
end
end
test "does not change the Literal when it is invalid" do
Enum.each @invalid, fn value ->
assert (unquote(datatype).new(value) |> Literal.canonical) == unquote(datatype).new(value)
end
end
end
describe "general validation" do
Enum.each Map.keys(@valid), fn value ->
@tag value: value
test "#{inspect value} as a #{unquote(datatype)} is valid", %{value: value} do
assert Literal.valid? unquote(datatype).new(value)
end
end
Enum.each @invalid, fn value ->
@tag value: value
test "#{inspect value} as a #{unquote(datatype)} is invalid", %{value: value} do
refute Literal.valid? unquote(datatype).new(value)
end
end
end
end
end
end
end

View file

@ -0,0 +1,45 @@
defmodule RDF.BooleanTest do
use RDF.Datatype.Test.Case, datatype: RDF.Boolean, id: RDF.NS.XSD.boolean,
valid: %{
# input => { value , lexical , canonicalized }
true => { true , nil , "true" },
false => { false , nil , "false" },
0 => { false , nil , "false" },
1 => { true , nil , "true" },
"true" => { true , nil , "true" },
"false" => { false , nil , "false" },
"tRuE" => { true , "tRuE" , "true" },
"FaLsE" => { false , "FaLsE" , "false" },
"0" => { false , "0" , "false" },
"1" => { true , "1" , "true" },
},
invalid: ~w(foo 10) ++ [42, 3.14, "true false", "true foo"]
describe "equality" do
test "two literals are equal when they have the same datatype and lexical form" do
[
{true , "true" },
{false , "false"},
{1 , "true" },
{0 , "false"},
]
|> Enum.each(fn {l, r} ->
assert Boolean.new(l) == Boolean.new(r)
end)
end
test "two literals with same value but different lexical form are not equal" do
[
{"True" , "true" },
{"FALSE" , "false"},
{"1" , "true" },
{"0" , "false"},
]
|> Enum.each(fn {l, r} ->
assert Boolean.new(l) != Boolean.new(r)
end)
end
end
end

View file

@ -0,0 +1,41 @@
defmodule RDF.IntegerTest do
use RDF.Datatype.Test.Case, datatype: RDF.Integer, id: RDF.NS.XSD.integer,
valid: %{
# input => { value , lexical , canonicalized }
0 => { 0 , nil , "0" },
1 => { 1 , nil , "1" },
"0" => { 0 , nil , "0" },
"1" => { 1 , nil , "1" },
"01" => { 1 , "01" , "1" },
"0123" => { 123 , "0123" , "123" },
+1 => { 1 , nil , "1" },
-1 => { -1 , nil , "-1" },
"+1" => { 1 , "+1" , "1" },
"-1" => { -1 , nil , "-1" },
},
invalid: ~w(foo 10.1 12xyz) ++ [true, false, 3.14, "1 2", "foo 1", "1 foo"]
describe "equality" do
test "two literals are equal when they have the same datatype and lexical form" do
[
{"1" , 1},
{"-42" , -42},
]
|> Enum.each(fn {l, r} ->
assert Integer.new(l) == Integer.new(r)
end)
end
test "two literals with same value but different lexical form are not equal" do
[
{"01" , 1},
{"+42" , 42},
]
|> Enum.each(fn {l, r} ->
assert Integer.new(l) != Integer.new(r)
end)
end
end
end

View file

@ -0,0 +1,123 @@
defmodule RDF.LangStringTest do
use RDF.Datatype.Test.Case, datatype: RDF.LangString, id: RDF.langString
@valid %{
# input => { language , value , lexical , canonicalized }
"foo" => { "en" , "foo" , nil , "foo" },
0 => { "en" , "0" , nil , "0" },
42 => { "en" , "42" , nil , "42" },
3.14 => { "en" , "3.14" , nil , "3.14" },
true => { "en" , "true" , nil , "true" },
false => { "en" , "false" , nil , "false" },
}
describe "new" do
Enum.each @valid, fn {input, {language, value, lexical, _}} ->
expected_literal =
%Literal{value: value, uncanonical_lexical: lexical, datatype: RDF.langString, language: language}
@tag example: %{input: input, language: language, output: expected_literal}
test "valid: LangString.new(#{inspect input}) == #{inspect expected_literal}",
%{example: example} do
assert LangString.new(example.input, language: example.language) == example.output
end
end
# valid value with canonical option
Enum.each @valid, fn {input, {language, value, _, _}} ->
expected_literal =
%Literal{value: value, datatype: RDF.langString, language: language}
@tag example: %{input: input, language: language, output: expected_literal}
test "valid: LangString.new(#{inspect input}, canonicalize: true) == #{inspect expected_literal}",
%{example: example} do
assert LangString.new(example.input, language: example.language, canonicalize: true) == example.output
end
end
test "datatype option is ignored" do
Enum.each Datatype.ids, fn id ->
Enum.each @valid, fn {input, _} ->
assert LangString.new(input, language: "en", datatype: id) == LangString.new(input, language: "en")
end
end
end
test "without a language it raises an error" do
Enum.each @valid, fn {input, _} ->
assert_raise ArgumentError, fn -> LangString.new(input) end
end
end
end
describe "new!" do
test "with valid values, it behaves the same as new" do
Enum.each @valid, fn {input, _} ->
assert LangString.new!(input, language: "de") ==
LangString.new(input, language: "de")
assert LangString.new!(input, language: "de", datatype: RDF.langString) ==
LangString.new(input, language: "de")
assert LangString.new!(input, language: "de", canonicalize: true) ==
LangString.new(input, language: "de", canonicalize: true)
end
end
test "without a language it raises an error" do
Enum.each @valid, fn {input, _} ->
assert_raise ArgumentError, fn -> LangString.new!(input) end
end
end
end
describe "lexical" do
Enum.each @valid, fn {input, {language, _, lexical, canonicalized}} ->
lexical = lexical || canonicalized
@tag example: %{input: input, language: language, lexical: lexical}
test "of valid LangString.new(#{inspect input}) == #{inspect lexical}",
%{example: example} do
assert (LangString.new(example.input, language: example.language) |> Literal.lexical) == example.lexical
end
end
end
describe "canonicalization" do
Enum.each @valid, fn {input, {language, value, _, _}} ->
expected_literal =
%Literal{value: value, datatype: RDF.langString, language: language}
@tag example: %{input: input, language: language, output: expected_literal}
test "LangString #{inspect input} is canonicalized #{inspect expected_literal}",
%{example: example} do
assert (LangString.new(example.input, language: example.language) |> Literal.canonical) == example.output
end
end
Enum.each @valid, fn {input, {language, _, _, canonicalized}} ->
@tag example: %{input: input, language: language, canonicalized: canonicalized}
test "lexical of canonicalized LangString #{inspect input} is #{inspect canonicalized}",
%{example: example} do
assert (LangString.new(example.input, language: example.language) |> Literal.canonical |> Literal.lexical) ==
example.canonicalized
end
end
end
describe "validation" do
Enum.each Map.keys(@valid), fn value ->
@tag value: value
test "#{inspect value} as a RDF.LangString is valid", %{value: value} do
assert Literal.valid? LangString.new(value, language: "es")
end
end
test "a RDF.LangString without a language is invalid" do
Enum.each @valid, fn {_, {_, value, lexical, _}} ->
refute Literal.valid?(
%Literal{value: value, uncanonical_lexical: lexical, datatype: RDF.langString})
end
end
end
end

View file

@ -0,0 +1,14 @@
defmodule RDF.StringTest do
use RDF.Datatype.Test.Case, datatype: RDF.String, id: RDF.NS.XSD.string,
valid: %{
# input => { value , lexical , canonicalized }
"foo" => { "foo" , nil , "foo" },
0 => { "0" , nil , "0" },
42 => { "42" , nil , "42" },
3.14 => { "3.14" , nil , "3.14" },
true => { "true" , nil , "true" },
false => { "false" , nil , "false" },
},
invalid: []
end

View file

@ -9,29 +9,31 @@ defmodule RDF.LiteralTest do
doctest RDF.Literal
@examples %{
RDF.String => ["foo"],
RDF.Integer => [42],
RDF.Double => [3.14],
RDF.Boolean => [true, false],
}
describe "construction by type inference" do
test "string" do
assert Literal.new("foo") == RDF.String.new("foo")
Enum.each @examples, fn {datatype, example_values} ->
@tag example: %{datatype: datatype, values: example_values}
test (datatype |> Module.split |> List.last |> to_string), %{example: example} do
Enum.each example.values, fn example_value ->
assert Literal.new(example_value) == example.datatype.new(example_value)
end
end
end
test "integer" do
assert Literal.new(42) == RDF.Integer.new(42)
test "when options without datatype given" do
assert Literal.new(true, %{}) == RDF.Boolean.new(true)
assert Literal.new(42, %{}) == RDF.Integer.new(42)
end
test "double" do
assert Literal.new(3.14) == RDF.Double.new(3.14)
end
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)
@ -45,9 +47,11 @@ defmodule RDF.LiteralTest do
assert Literal.new("42", datatype: XSD.integer) == RDF.Integer.new("42")
end
test "string" do
assert Literal.new("foo", datatype: XSD.string) == RDF.String.new("foo")
end
test "unknown datatype" do
test "unmapped/unknown datatype" do
literal = Literal.new("custom typed value", datatype: "http://example/dt")
assert literal.value == "custom typed value"
assert literal.datatype == ~I<http://example/dt>
@ -84,6 +88,7 @@ defmodule RDF.LiteralTest do
end
end
describe "language" do
Enum.each literals(:all_plain_lang), fn literal ->
@tag literal: literal
@ -104,6 +109,7 @@ defmodule RDF.LiteralTest do
end
end
describe "datatype" do
Enum.each literals(:all_simple), fn literal ->
@tag literal: literal
@ -131,6 +137,7 @@ defmodule RDF.LiteralTest do
end)
end
describe "has_datatype?" do
Enum.each literals(~W[all_simple all_plain_lang]a), fn literal ->
@tag literal: literal
@ -147,6 +154,7 @@ defmodule RDF.LiteralTest do
end
end
describe "plain?" do
Enum.each literals(:all_plain), fn literal ->
@tag literal: literal
@ -162,6 +170,7 @@ defmodule RDF.LiteralTest do
end
end
describe "simple?" do
Enum.each literals(:all_simple), fn literal ->
@tag literal: literal
@ -177,6 +186,31 @@ defmodule RDF.LiteralTest do
end
end
describe "canonicalization" do
# for mapped/known datatypes the RDF.Datatype.Test.Case uses the general RDF.Literal.canonical function
test "an unmapped/unknown datatypes is always canonical" do
assert Literal.canonical? Literal.new("custom typed value", datatype: "http://example/dt")
end
test "for unmapped/unknown datatypes, canonicalize is a no-op" do
assert Literal.new("custom typed value", datatype: "http://example/dt") ==
Literal.canonical(Literal.new("custom typed value", datatype: "http://example/dt"))
end
end
describe "validation" do
# for mapped/known datatypes the RDF.Datatype.Test.Case uses the general RDF.Literal.valid? function
test "a literal with an unmapped/unknown datatype is always valid" do
assert Literal.valid? Literal.new("custom typed value", datatype: "http://example/dt")
end
end
describe "String.Chars protocol implementation" do
Enum.each values(:all_plain), fn value ->
@tag value: value