rdf-ex/lib/rdf/datatype.ex

303 lines
8.8 KiB
Elixir

defmodule RDF.Datatype do
@moduledoc """
A behaviour for natively supported literal datatypes.
A `RDF.Datatype` implements the foundational functions for the lexical form,
the validation, conversion and canonicalization of typed `RDF.Literal`s.
"""
alias RDF.Literal
alias RDF.Datatype.NS.XSD
@doc """
The IRI of the datatype.
"""
@callback id :: RDF.IRI.t
@doc """
Produces the lexical form of a `RDF.Literal`.
"""
@callback lexical(literal :: RDF.Literal.t) :: any
@doc """
Produces the lexical form of a value.
"""
@callback canonical_lexical(any) :: binary
@doc """
Produces the lexical form of an invalid value of a typed Literal.
The default implementation of the `_using__` macro just returns `to_string`
representation of the value.
"""
@callback invalid_lexical(any) :: binary
@doc """
Produces the canonical form of a `RDF.Literal`.
"""
@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
@doc """
Casts a literal of another datatype into a literal of the datatype the function is implemented on.
If the given literal is invalid or can not be converted into this datatype
`nil` is returned.
"""
@callback cast(RDF.Literal.t) :: RDF.Literal.t
@doc """
Determines if the value of a `RDF.Literal` is a member of lexical value space of its datatype.
"""
@callback valid?(literal :: RDF.Literal.t) :: boolean
@doc """
Checks if the value of two `RDF.Literal`s of this datatype are equal.
Non-RDF terms are tried to be coerced via `RDF.Term.coerce/1` before comparison.
Returns `nil` when the given arguments are not comparable as literals of this datatype.
The default implementation of the `_using__` macro compares the values of the
`canonical/1` forms of the given literals of this datatype.
"""
@callback equal_value?(literal1 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: boolean | nil
@doc """
Compares two `RDF.Literal`s.
Returns `:gt` if first literal is greater than the second in terms of their datatype
and `:lt` for vice versa. If the two literals are equal `:eq` is returned.
For datatypes with only partial ordering `:indeterminate` is returned when the
order of the given literals is not defined.
Returns `nil` when the given arguments are not comparable datatypes or if one
them is invalid.
The default implementation of the `_using__` macro compares the values of the
`canonical/1` forms of the given literals of this datatype.
"""
@callback compare(literal1 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: :lt | :gt | :eq | :indeterminate | nil
@lang_string RDF.iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")
# TODO: This mapping should be created dynamically and be extendable, to allow user-defined datatypes ...
@mapping %{
@lang_string => RDF.LangString,
XSD.string => RDF.String,
XSD.integer => RDF.Integer,
XSD.double => RDF.Double,
XSD.decimal => RDF.Decimal,
XSD.boolean => RDF.Boolean,
XSD.date => RDF.Date,
XSD.time => RDF.Time,
XSD.dateTime => RDF.DateTime,
}
@doc """
The mapping of IRIs of datatypes to their `RDF.Datatype`.
"""
def mapping, do: @mapping
@doc """
The IRIs of all datatypes with a `RDF.Datatype` defined.
"""
def ids, do: Map.keys(@mapping)
@doc """
All defined `RDF.Datatype` modules.
"""
def modules, do: Map.values(@mapping)
@doc """
Returns the `RDF.Datatype` for a directly datatype IRI or the datatype IRI of a `RDF.Literal`.
"""
def get(%Literal{datatype: id}), do: get(id)
def get(id), do: @mapping[id]
defmacro __using__(opts) do
id = Keyword.fetch!(opts, :id)
quote bind_quoted: [], unquote: true do
@behaviour unquote(__MODULE__)
alias RDF.Literal
alias RDF.Datatype.NS.XSD
@id unquote(id)
@impl unquote(__MODULE__)
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, opts) when is_binary(value),
do: build_literal_by_lexical(value, opts)
def new(value, opts),
do: build_literal_by_value(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
case convert(value, opts) do
nil ->
build_literal(nil, invalid_lexical(value), opts)
converted_value ->
build_literal(converted_value, nil, opts)
end
end
def build_literal_by_lexical(lexical, opts) do
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(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 convert(value, _), do: nil
@impl unquote(__MODULE__)
def lexical(literal)
def lexical(%RDF.Literal{value: value, uncanonical_lexical: nil}) do
canonical_lexical(value)
end
def lexical(%RDF.Literal{uncanonical_lexical: lexical}) do
lexical
end
@impl unquote(__MODULE__)
def canonical_lexical(value), do: to_string(value)
@impl unquote(__MODULE__)
def invalid_lexical(value), do: to_string(value)
@impl unquote(__MODULE__)
def canonical(literal)
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
@impl unquote(__MODULE__)
def valid?(literal)
def valid?(%Literal{value: nil}), do: false
def valid?(%Literal{datatype: @id}), do: true
def valid?(_), do: false
@impl unquote(__MODULE__)
def equal_value?(literal1, literal2)
def equal_value?(%Literal{uncanonical_lexical: lexical1, datatype: @id, value: nil},
%Literal{uncanonical_lexical: lexical2, datatype: @id}) do
lexical1 == lexical2
end
def equal_value?(%Literal{datatype: @id} = literal1, %Literal{datatype: @id} = literal2) do
canonical(literal1).value == canonical(literal2).value
end
def equal_value?(%RDF.Literal{} = left, right) when not is_nil(right) do
unless RDF.Term.term?(right) do
equal_value?(left, RDF.Term.coerce(right))
end
end
def equal_value?(_, _), do: nil
def less_than?(literal1, literal2), do: RDF.Literal.less_than?(literal1, literal2)
def greater_than?(literal1, literal2), do: RDF.Literal.greater_than?(literal1, literal2)
@impl unquote(__MODULE__)
def compare(left, right)
def compare(%Literal{datatype: @id, value: value1} = literal1,
%Literal{datatype: @id, value: value2} = literal2)
when not (is_nil(value1) or is_nil(value2))
do
case {canonical(literal1).value, canonical(literal2).value} do
{value1, value2} when value1 < value2 -> :lt
{value1, value2} when value1 > value2 -> :gt
_ ->
if equal_value?(literal1, literal2), do: :eq
end
end
def compare(_, _), do: nil
def validate_cast(%Literal{} = literal) do
if valid?(literal), do: literal
end
def validate_cast(_), do: nil
defoverridable [
build_literal_by_value: 2,
build_literal_by_lexical: 2,
build_literal: 3,
lexical: 1,
canonical_lexical: 1,
invalid_lexical: 1,
convert: 2,
valid?: 1,
equal_value?: 2,
compare: 2,
new: 2,
new!: 2,
]
end
end
end