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:
parent
d812998fb0
commit
2b9aa62d69
16 changed files with 645 additions and 84 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
180
test/support/rdf_datatype_case.ex
Normal file
180
test/support/rdf_datatype_case.ex
Normal 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
|
45
test/unit/datatypes/boolean_test.exs
Normal file
45
test/unit/datatypes/boolean_test.exs
Normal 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
|
41
test/unit/datatypes/integer_test.exs
Normal file
41
test/unit/datatypes/integer_test.exs
Normal 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
|
123
test/unit/datatypes/lang_string_test.exs
Normal file
123
test/unit/datatypes/lang_string_test.exs
Normal 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
|
14
test/unit/datatypes/string_test.exs
Normal file
14
test/unit/datatypes/string_test.exs
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue