Redesign datatype reflection API

This commit is contained in:
Marcel Otto 2020-05-15 17:13:31 +02:00
parent 042ff1c1b8
commit fa35b65d9f
23 changed files with 431 additions and 233 deletions

View file

@ -207,11 +207,15 @@ defmodule RDF do
defdelegate bnode(), to: BlankNode, as: :new
defdelegate bnode(id), to: BlankNode, as: :new
@doc """
Checks if the given value is a RDF literal.
"""
def literal?(%Literal{}), do: true
def literal?(_), do: false
defdelegate literal(value), to: Literal, as: :new
defdelegate literal(value, opts), to: Literal, as: :new
defdelegate literal?(literal), to: Literal.Datatype.Registry
defdelegate triple(s, p, o), to: Triple, as: :new
defdelegate triple(tuple), to: Triple, as: :new

View file

@ -1,4 +1,5 @@
defmodule RDF.Guards do
defguard maybe_ns_term(term)
when is_atom(term) and term not in [nil, true, false]
import RDF.Utils.Guards
defguard maybe_ns_term(term) when maybe_module(term)
end

View file

@ -103,16 +103,16 @@ defmodule RDF.Literal do
def coerce(%NaiveDateTime{} = value), do: RDF.XSD.DateTime.new(value)
def coerce(%URI{} = value), do: RDF.XSD.AnyURI.new(value)
# Although the following catch-all-clause for all structs could handle the core datatypes
# Although the following catch-all-clause for all structs could handle the builtin datatypes
# we're generating dedicated clauses for them here, as they are approx. 15% faster
Enum.each(Datatype.Registry.core_datatypes(), fn datatype ->
Enum.each(Datatype.Registry.builtin_datatypes(), fn datatype ->
def coerce(%unquote(datatype){} = datatype_literal) do
%__MODULE__{literal: datatype_literal}
end
end)
def coerce(%_datatype{} = datatype_literal) do
if Datatype.Registry.literal?(datatype_literal) do
def coerce(%datatype{} = datatype_literal) do
if Datatype.Registry.datatype_struct?(datatype) do
%__MODULE__{literal: datatype_literal}
end
end
@ -148,6 +148,23 @@ defmodule RDF.Literal do
end
end
@doc """
Returns if the given value is a `RDF.Literal` or `RDF.Literal.Datatype` struct.
If you simply want to check for a `RDF.Literal` use pattern matching or `RDF.literal?/1`.
This function is a bit slower than those and most of the time only needed when
implementing `RDF.Literal.Datatype`s where you have to deal with the raw,
i.e. unwrapped `RDF.Literal.Datatype` structs.
"""
defdelegate datatype?(value), to: RDF.Literal.Datatype.Registry, as: :datatype?
@doc """
Returns if the literal uses the `RDF.Literal.Generic` datatype or on of the dedicated builtin or custom `RDF.Literal.Datatype`s.
"""
@spec generic?(t) :: boolean
def generic?(%__MODULE__{literal: %RDF.Literal.Generic{}}), do: true
def generic?(%__MODULE__{}), do: false
@doc """
Returns if a literal is a language-tagged literal.
@ -178,7 +195,7 @@ defmodule RDF.Literal do
"""
@spec simple?(t) :: boolean
def simple?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true
def simple?(%__MODULE__{} = _), do: false
def simple?(%__MODULE__{}), do: false
@doc """
@ -192,11 +209,7 @@ defmodule RDF.Literal do
@spec plain?(t) :: boolean
def plain?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true
def plain?(%__MODULE__{literal: %LangString{}}), do: true
def plain?(%__MODULE__{} = _), do: false
@spec typed?(t) :: boolean
def typed?(literal), do: not plain?(literal)
def plain?(%__MODULE__{}), do: false
############################################################################
# functions delegating to the RDF.Literal.Datatype of a RDF.Literal

View file

@ -34,6 +34,17 @@ defmodule RDF.Literal.Datatype do
"""
@callback do_cast(literal | any) :: Literal.t() | nil
@doc """
Checks if the given `RDF.Literal` has the datatype for which the `RDF.Literal.Datatype` is implemented or is derived from it.
## Example
iex> RDF.XSD.byte(42) |> RDF.XSD.Integer.datatype?()
true
"""
@callback datatype?(Literal.t | t | literal) :: boolean
@doc """
The datatype IRI of the given `RDF.Literal`.
"""
@ -139,6 +150,15 @@ defmodule RDF.Literal.Datatype do
defdelegate get(id), to: Literal.Datatype.Registry, as: :datatype
@doc !"""
As opposed to RDF.Literal.valid?/1 this function operates on the datatype structs ...
It's meant for internal use only and doesn't perform checks if the struct
passed is actually a `RDF.Literal.Datatype` struct.
"""
def valid?(%datatype{} = datatype_literal), do: datatype.valid?(datatype_literal)
defmacro __using__(opts) do
name = Keyword.fetch!(opts, :name)
id = Keyword.fetch!(opts, :id)
@ -157,6 +177,13 @@ defmodule RDF.Literal.Datatype do
quote do
@behaviour unquote(__MODULE__)
@doc !"""
This function is just used to check if a module is a RDF.Literal.Datatype.
See `RDF.Literal.Datatype.Registry.is_rdf_literal_datatype?/1`.
"""
def __rdf_literal_datatype_indicator__, do: true
@name unquote(name)
@impl unquote(__MODULE__)
def name, do: @name
@ -165,6 +192,16 @@ defmodule RDF.Literal.Datatype do
@impl unquote(__MODULE__)
def id, do: @id
# RDF.XSD.Datatypes offers another default implementation, but since it is
# still in a macro implementation defoverridable doesn't work
unless RDF.XSD.Datatype in @behaviour do
@impl unquote(__MODULE__)
def datatype?(%Literal{literal: literal}), do: datatype?(literal)
def datatype?(%datatype{}), do: datatype?(datatype)
def datatype?(__MODULE__), do: true
def datatype?(_), do: false
end
@impl unquote(__MODULE__)
def datatype(%Literal{literal: literal}), do: datatype(literal)
def datatype(%__MODULE__{}), do: @id
@ -227,8 +264,8 @@ defmodule RDF.Literal.Datatype do
def equal_value?(_, nil), do: nil
def equal_value?(left, right) do
cond do
not RDF.literal?(right) and not RDF.term?(right) -> equal_value?(left, Literal.coerce(right))
not RDF.literal?(left) and not RDF.term?(left) -> equal_value?(Literal.coerce(left), right)
not Literal.datatype?(right) and not RDF.term?(right) -> equal_value?(left, Literal.coerce(right))
not Literal.datatype?(left) and not RDF.term?(left) -> equal_value?(Literal.coerce(left), right)
true -> do_equal_value?(left, right)
end
end
@ -256,6 +293,9 @@ defmodule RDF.Literal.Datatype do
|> new()
end
# This is a private RDF.Literal constructor, which should be used to build
# the RDF.Literals from the datatype literal structs instead of the
# RDF.Literal/new/1, to bypass the unnecessary datatype checks.
defp literal(datatype_literal), do: %Literal{literal: datatype_literal}
defoverridable datatype: 1,

View file

@ -5,34 +5,162 @@ defmodule RDF.Literal.Datatype.Registry do
alias RDF.Literal.Datatype.Registry.Registration
import RDF.Guards
import RDF.Utils.Guards
@core_datatypes [RDF.LangString | Enum.to_list(XSD.datatypes())]
@primitive_numeric_datatypes [
RDF.XSD.Integer,
RDF.XSD.Decimal,
RDF.XSD.Double
]
@builtin_numeric_datatypes @primitive_numeric_datatypes ++ [
RDF.XSD.Long,
RDF.XSD.Int,
RDF.XSD.Short,
RDF.XSD.Byte,
RDF.XSD.NonNegativeInteger,
RDF.XSD.PositiveInteger,
RDF.XSD.UnsignedLong,
RDF.XSD.UnsignedInt,
RDF.XSD.UnsignedShort,
RDF.XSD.UnsignedByte,
RDF.XSD.NonPositiveInteger,
RDF.XSD.NegativeInteger,
RDF.XSD.Float
]
@builtin_xsd_datatypes [
XSD.Boolean,
XSD.String,
XSD.Date,
XSD.Time,
XSD.DateTime,
XSD.AnyURI
] ++ @builtin_numeric_datatypes
@builtin_datatypes [RDF.LangString | @builtin_xsd_datatypes]
@doc """
All core `RDF.Literal.Datatype` modules.
Returns a list of all builtin `RDF.Literal.Datatype` modules.
"""
@spec core_datatypes :: Enum.t
def core_datatypes, do: @core_datatypes
@spec builtin_datatypes :: [RDF.Literal.Datatype.t]
def builtin_datatypes, do: @builtin_datatypes
@doc """
Checks if the given module is core datatype.
"""
@spec core_datatype?(module) :: boolean
def core_datatype?(module), do: module in @core_datatypes
Checks if the given module is a builtin datatype.
@doc """
Checks if the given module is a core datatype or a registered custom datatype implementing the `RDF.Literal.Datatype` behaviour.
Note: This doesn't include `RDF.Literal.Generic`.
"""
@spec datatype?(module) :: boolean
def datatype?(module) do
core_datatype?(module) or implements_behaviour?(module, Literal.Datatype)
@spec builtin_datatype?(module) :: boolean
def builtin_datatype?(module)
for datatype <- @builtin_datatypes do
def builtin_datatype?(unquote(datatype)), do: true
end
@spec literal?(module) :: boolean
def literal?(%Literal{}), do: true
def literal?(%Literal.Generic{}), do: true
def literal?(%datatype{}), do: datatype?(datatype)
def literal?(_), do: false
def builtin_datatype?(_), do: false
@doc """
Checks if the given module is a builtin datatype or a registered custom datatype implementing the `RDF.Literal.Datatype` behaviour.
"""
@spec datatype?(Literal.t | Literal.Datatype.literal | module) :: boolean
def datatype?(value)
# We assume literals were created properly which means they have a proper RDF.Literal.Datatype
def datatype?(%Literal{}), do: true
def datatype?(value), do: datatype_struct?(value)
@doc false
@spec datatype_struct?(Literal.Datatype.literal | module) :: boolean
def datatype_struct?(value)
def datatype_struct?(%datatype{}), do: datatype_struct?(datatype)
def datatype_struct?(Literal.Generic), do: true
def datatype_struct?(module) when maybe_module(module) do
builtin_datatype?(module) or is_rdf_literal_datatype?(module)
end
def datatype_struct?(_), do: false
@doc """
Returns a list of all builtin `RDF.XSD.Datatype` modules.
"""
@spec builtin_xsd_datatypes :: [RDF.Literal.Datatype.t]
def builtin_xsd_datatypes, do: @builtin_xsd_datatypes
@doc false
@spec builtin_xsd_datatype?(module) :: boolean
def builtin_xsd_datatype?(module)
for datatype <- @builtin_xsd_datatypes do
def builtin_xsd_datatype?(unquote(datatype)), do: true
end
def builtin_xsd_datatype?(_), do: false
@doc """
Checks if the given module is a builtin XSD datatype or a registered custom datatype implementing the `RDF.XSD.Datatype` behaviour.
"""
@spec xsd_datatype?(Literal.t | XSD.Datatype.literal | module) :: boolean
def xsd_datatype?(value)
def xsd_datatype?(%Literal{literal: datatype_struct}), do: xsd_datatype?(datatype_struct)
def xsd_datatype?(value), do: xsd_datatype_struct?(value)
@doc false
@spec xsd_datatype_struct?(RDF.Literal.t() | XSD.Datatype.literal | module) :: boolean
def xsd_datatype_struct?(value)
def xsd_datatype_struct?(%datatype{}), do: xsd_datatype_struct?(datatype)
def xsd_datatype_struct?(module) when maybe_module(module) do
builtin_xsd_datatype?(module) or is_xsd_datatype?(module)
end
def xsd_datatype_struct?(_), do: false
@doc """
Returns a list of all numeric datatype modules.
"""
@spec builtin_numeric_datatypes() :: [RDF.Literal.Datatype.t]
def builtin_numeric_datatypes(), do: @builtin_numeric_datatypes
@doc """
The set of all primitive numeric datatypes.
"""
@spec primitive_numeric_datatypes() :: [RDF.Literal.Datatype.t]
def primitive_numeric_datatypes(), do: @primitive_numeric_datatypes
@doc false
@spec builtin_numeric_datatype?(module) :: boolean
def builtin_numeric_datatype?(module)
for datatype <- @builtin_numeric_datatypes do
def builtin_numeric_datatype?(unquote(datatype)), do: true
end
def builtin_numeric_datatype?(_), do: false
@doc """
Returns if a given literal or datatype has or is a numeric datatype.
"""
@spec numeric_datatype?(RDF.Literal.t() | RDF.XSD.Datatype.t() | any) :: boolean
def numeric_datatype?(literal)
def numeric_datatype?(%RDF.Literal{literal: literal}), do: numeric_datatype?(literal)
def numeric_datatype?(%datatype{}), do: numeric_datatype?(datatype)
def numeric_datatype?(datatype) when maybe_module(datatype) do
builtin_numeric_datatype?(datatype) or (
xsd_datatype?(datatype) and
Enum.any?(@primitive_numeric_datatypes, fn numeric_primitive ->
datatype.derived_from?(numeric_primitive)
end)
)
end
def numeric_datatype?(_), do: false
@doc """
Returns the `RDF.Literal.Datatype` for a datatype IRI.
@ -50,15 +178,31 @@ defmodule RDF.Literal.Datatype.Registry do
def xsd_datatype(id) do
datatype = datatype(id)
if datatype && implements_behaviour?(datatype, XSD.Datatype) do
if datatype && is_xsd_datatype?(datatype) do
datatype
end
end
defp implements_behaviour?(module, behaviour) do
module.module_info[:attributes]
|> Keyword.get_values(:behaviour)
|> List.flatten()
|> Enum.member?(behaviour)
# TODO: Find a better/faster solution for checking datatype modules which includes unknown custom datatypes.
# Although checking for the presence of a function via __info__(:functions) is
# the fastest way to reflect a module type on average over the positive and negative
# case (being roughly comparable to a map access), we would still have to rescue
# from an UndefinedFunctionError since its raised by trying to access __info__
# on plain (non-module) atoms, so we can do the check by rescueing in the first place.
# Although the positive is actually faster than the __info__(:functions) check,
# the negative is more than 7 times slower.
# (Properly checking for the behaviour attribute with module_info[:attributes]
# is more than 200 times slower.)
defp is_rdf_literal_datatype?(module) do
module.__rdf_literal_datatype_indicator__()
rescue
UndefinedFunctionError -> false
end
defp is_xsd_datatype?(module) do
module.__xsd_datatype_indicator__()
rescue
UndefinedFunctionError -> false
end
end

View file

@ -9,6 +9,8 @@ defmodule RDF.LangString do
name: "langString",
id: RDF.Utils.Bootstrapping.rdf_iri("langString")
import RDF.Utils.Guards
alias RDF.Literal.Datatype
alias RDF.Literal
@ -21,7 +23,7 @@ defmodule RDF.LangString do
@spec new(any, String.t | atom | keyword) :: Literal.t
def new(value, language_or_opts \\ [])
def new(value, language) when is_binary(language), do: new(value, language: language)
def new(value, language) when is_atom(language), do: new(value, language: language)
def new(value, language) when is_ordinary_atom(language), do: new(value, language: language)
def new(value, opts) do
%Literal{
literal: %__MODULE__{
@ -33,7 +35,7 @@ defmodule RDF.LangString do
defp normalize_language(nil), do: nil
defp normalize_language(""), do: nil
defp normalize_language(language) when is_atom(language), do: language |> to_string() |> normalize_language()
defp normalize_language(language) when is_ordinary_atom(language), do: language |> to_string() |> normalize_language()
defp normalize_language(language), do: String.downcase(language)
@impl RDF.Literal.Datatype

6
lib/rdf/utils/guards.ex Normal file
View file

@ -0,0 +1,6 @@
defmodule RDF.Utils.Guards do
defguard is_ordinary_atom(term)
when is_atom(term) and term not in [nil, true, false]
defguard maybe_module(term) when is_ordinary_atom(term)
end

View file

@ -5,19 +5,9 @@ defmodule RDF.XSD do
see <https://www.w3.org/TR/xmlschema-2/>
"""
alias __MODULE__
alias RDF.{IRI, Literal}
import RDF.Utils.Guards
@datatypes [
XSD.Boolean,
XSD.String,
XSD.Date,
XSD.Time,
XSD.DateTime,
XSD.AnyURI
]
|> MapSet.new()
|> MapSet.union(XSD.Numeric.datatypes())
alias __MODULE__
@facets [
XSD.Facets.MinInclusive,
@ -32,39 +22,15 @@ defmodule RDF.XSD do
@facets_by_name Map.new(@facets, fn facet -> {facet.name(), facet} end)
def facet(name) when is_atom(name), do: @facets_by_name[to_string(name)]
def facet(name) when is_ordinary_atom(name), do: @facets_by_name[to_string(name)]
def facet(name), do: @facets_by_name[name]
@doc """
The list of all XSD datatypes.
Returns if the given value is a `RDF.XSD.Datatype` struct or `RDF.Literal` with a `RDF.XSD.Datatype`.
"""
@spec datatypes() :: Enum.t()
def datatypes(), do: @datatypes
defdelegate datatype?(value), to: RDF.Literal.Datatype.Registry, as: :xsd_datatype?
@datatypes_by_name Map.new(@datatypes, fn datatype -> {datatype.name(), datatype} end)
@datatypes_by_iri Map.new(@datatypes, fn datatype -> {datatype.id(), datatype} end)
def datatype_by_name(name) when is_atom(name), do: @datatypes_by_name[to_string(name)]
def datatype_by_name(name), do: @datatypes_by_name[name]
def datatype_by_iri(iri) when is_binary(iri), do: @datatypes_by_iri[IRI.new(iri)]
def datatype_by_iri(%IRI{} = iri), do: @datatypes_by_iri[iri]
@doc """
Returns if a given datatype is a XSD datatype.
"""
def datatype?(datatype), do: datatype in @datatypes
@doc """
Returns if a given argument is a `RDF.XSD.datatype` literal.
"""
def literal?(%Literal{literal: %datatype{}}), do: datatype?(datatype)
def literal?(%datatype{}), do: datatype?(datatype)
def literal?(_), do: false
@doc false
def valid?(%datatype{} = datatype_literal), do: datatype.valid?(datatype_literal)
for datatype <- @datatypes do
for datatype <- RDF.Literal.Datatype.Registry.builtin_xsd_datatypes() do
defdelegate unquote(String.to_atom(datatype.name))(value), to: datatype, as: :new
defdelegate unquote(String.to_atom(datatype.name))(value, opts), to: datatype, as: :new

View file

@ -15,6 +15,7 @@ defmodule RDF.XSD.Datatype do
@type comparison_result :: :lt | :gt | :eq
import RDF.Utils.Guards
@doc """
Returns if the `RDF.XSD.Datatype` is a primitive datatype.
@ -34,15 +35,13 @@ defmodule RDF.XSD.Datatype do
@callback base_primitive :: t()
@doc """
Checks if a `RDF.XSD.Datatype` is directly or indirectly derived from another `RDF.XSD.Datatype`.
Checks if the `RDF.XSD.Datatype` is directly or indirectly derived from the given `RDF.XSD.Datatype`.
Note that this is just a basic datatype reflection function on the module level
and does not work with `RDF.Literal`s. See `c:RDF.Literal.Datatype.datatype?/1` instead.
"""
@callback derived_from?(t()) :: boolean
@doc """
Checks if the datatype of a given literal is derived from a `RDF.XSD.Datatype`.
"""
@callback derived?(RDF.XSD.Literal.t()) :: boolean
@doc """
The set of applicable facets of a `RDF.XSD.Datatype`.
"""
@ -95,18 +94,21 @@ defmodule RDF.XSD.Datatype do
uncanonical_lexical: RDF.XSD.Datatype.uncanonical_lexical()
}
@impl unquote(__MODULE__)
def derived_from?(datatype)
@doc !"""
This function is just used to check if a module is a RDF.XSD.Datatype.
def derived_from?(__MODULE__), do: true
See `RDF.Literal.Datatype.Registry.is_xsd_datatype?/1`.
"""
def __xsd_datatype_indicator__, do: true
def derived_from?(datatype) do
base = base()
not is_nil(base) and base.derived_from?(datatype)
@impl RDF.Literal.Datatype
def datatype?(%RDF.Literal{literal: literal}), do: datatype?(literal)
def datatype?(%datatype{}), do: datatype?(datatype)
def datatype?(__MODULE__), do: true
def datatype?(datatype) when maybe_module(datatype) do
RDF.XSD.datatype?(datatype) and datatype.derived_from?(__MODULE__)
end
@impl unquote(__MODULE__)
def derived?(literal), do: RDF.XSD.Datatype.derived_from?(literal, __MODULE__)
def datatype?(_), do: false
# Dialyzer causes a warning on all primitives since the facet_conform?/2 call
# always returns true there, so the other branch is unnecessary. This could
@ -252,14 +254,4 @@ defmodule RDF.XSD.Datatype do
end
end
end
@spec base_primitive(t()) :: t()
def base_primitive(%RDF.Literal{literal: literal}), do: base_primitive(literal)
def base_primitive(%datatype{}), do: base_primitive(datatype)
def base_primitive(datatype), do: datatype.base_primitive()
@spec derived_from?(t() | literal() | RDF.Literal.t(), t()) :: boolean
def derived_from?(%RDF.Literal{literal: literal}, super_datatype), do: derived_from?(literal, super_datatype)
def derived_from?(%datatype{}, super_datatype), do: derived_from?(datatype, super_datatype)
def derived_from?(datatype, super_datatype) when is_atom(datatype), do: datatype.derived_from?(super_datatype)
end

View file

@ -23,6 +23,9 @@ defmodule RDF.XSD.Datatype.Primitive do
@impl RDF.XSD.Datatype
def base_primitive, do: __MODULE__
@impl RDF.XSD.Datatype
def derived_from?(_), do: false
@impl RDF.XSD.Datatype
def init_valid_lexical(value, lexical, opts)
def init_valid_lexical(_value, nil, _opts), do: nil
@ -40,8 +43,8 @@ defmodule RDF.XSD.Datatype.Primitive do
@impl RDF.Literal.Datatype
def do_cast(value) do
if RDF.XSD.literal?(value) do
if derived?(value) do
if RDF.Literal.datatype?(value) do
if datatype?(value) do
build_valid(value.value, value.uncanonical_lexical, [])
end
else

View file

@ -20,6 +20,10 @@ defmodule RDF.XSD.Datatype.Restriction do
@impl RDF.XSD.Datatype
def base_primitive, do: @base.base_primitive()
@impl RDF.XSD.Datatype
def derived_from?(@base), do: true
def derived_from?(datatype), do: @base.derived_from?(datatype)
@impl RDF.XSD.Datatype
def applicable_facets, do: @base.applicable_facets()

View file

@ -41,7 +41,7 @@ defmodule RDF.XSD.Boolean do
end
def do_cast(literal_or_value) do
if RDF.XSD.Numeric.literal?(literal_or_value) do
if RDF.XSD.Numeric.datatype?(literal_or_value) do
new(literal_or_value.value not in [0, 0.0, :nan])
else
super(literal_or_value)
@ -82,7 +82,7 @@ defmodule RDF.XSD.Boolean do
def ebv(%datatype{} = literal) do
if RDF.XSD.Numeric.datatype?(datatype) do
if datatype.valid?(literal) and
not (literal.value == 0 or literal.value == :nan),
not (datatype.value(literal) == 0 or datatype.value(literal) == :nan),
do: RDF.XSD.Boolean.Value.true(),
else: RDF.XSD.Boolean.Value.false()
end

View file

@ -107,8 +107,8 @@ defmodule RDF.XSD.Decimal do
def digit_count(literal) do
cond do
RDF.XSD.Integer.derived?(literal) -> RDF.XSD.Integer.digit_count(literal)
derived?(literal) -> do_digit_count(literal)
RDF.XSD.Integer.datatype?(literal) -> RDF.XSD.Integer.digit_count(literal)
datatype?(literal) -> do_digit_count(literal)
true -> nil
end
end
@ -132,8 +132,8 @@ defmodule RDF.XSD.Decimal do
def fraction_digit_count(literal) do
cond do
RDF.XSD.Integer.derived?(literal) -> 0
derived?(literal) -> do_fraction_digit_count(literal)
RDF.XSD.Integer.datatype?(literal) -> 0
datatype?(literal) -> do_fraction_digit_count(literal)
true -> nil
end
end

View file

@ -74,7 +74,7 @@ defmodule RDF.XSD.Integer do
"""
@spec digit_count(RDF.XSD.Literal.t()) :: non_neg_integer | nil
def digit_count(%datatype{} = literal) do
if derived?(literal) and datatype.valid?(literal) do
if datatype?(literal) and datatype.valid?(literal) do
literal
|> datatype.canonical()
|> datatype.lexical()

View file

@ -3,67 +3,13 @@ defmodule RDF.XSD.Numeric do
Collection of functions for numeric literals.
"""
@type t :: module
alias Elixir.Decimal, as: D
import Kernel, except: [abs: 1, floor: 1, ceil: 1]
@datatypes MapSet.new([
RDF.XSD.Decimal,
RDF.XSD.Integer,
RDF.XSD.Long,
RDF.XSD.Int,
RDF.XSD.Short,
RDF.XSD.Byte,
RDF.XSD.NonNegativeInteger,
RDF.XSD.PositiveInteger,
RDF.XSD.UnsignedLong,
RDF.XSD.UnsignedInt,
RDF.XSD.UnsignedShort,
RDF.XSD.UnsignedByte,
RDF.XSD.NonPositiveInteger,
RDF.XSD.NegativeInteger,
RDF.XSD.Double,
RDF.XSD.Float
])
@type t ::
RDF.XSD.Decimal.t()
| RDF.XSD.Integer.t()
| RDF.XSD.Long.t()
| RDF.XSD.Int.t()
| RDF.XSD.Short.t()
| RDF.XSD.Byte.t()
| RDF.XSD.NonNegativeInteger.t()
| RDF.XSD.PositiveInteger.t()
| RDF.XSD.UnsignedLong.t()
| RDF.XSD.UnsignedInt.t()
| RDF.XSD.UnsignedShort.t()
| RDF.XSD.UnsignedByte.t()
| RDF.XSD.NonPositiveInteger.t()
| RDF.XSD.NegativeInteger.t()
| RDF.XSD.Double.t()
| RDF.XSD.Float.t()
@doc """
The set of all numeric datatypes.
"""
@spec datatypes() :: Enum.t
def datatypes(), do: @datatypes
@doc """
Returns if a given datatype is a numeric datatype.
"""
@spec datatype?(RDF.XSD.Datatype.t() | any) :: boolean
def datatype?(datatype), do: datatype in @datatypes
@doc """
Returns if a given XSD literal has a numeric datatype.
"""
@spec literal?(RDF.Literal.t() | any) :: boolean
def literal?(literal)
def literal?(%RDF.Literal{literal: literal}), do: literal?(literal)
def literal?(%datatype{}), do: datatype?(datatype)
def literal?(_), do: false
defdelegate datatype?(value), to: RDF.Literal.Datatype.Registry, as: :numeric_datatype?
@doc """
Tests for numeric value equality of two numeric XSD datatyped literals.
@ -346,8 +292,8 @@ defmodule RDF.XSD.Numeric do
def abs(value) do
cond do
literal?(value) ->
if RDF.XSD.valid?(value) do
datatype?(value) ->
if RDF.Literal.Datatype.valid?(value) do
%datatype{} = value
case value.value do
@ -367,7 +313,7 @@ defmodule RDF.XSD.Numeric do
end
end
RDF.XSD.literal?(value) ->
RDF.Literal.datatype?(value) ->
nil
true ->
@ -424,8 +370,8 @@ defmodule RDF.XSD.Numeric do
def round(value, precision) do
cond do
literal?(value) ->
if RDF.XSD.valid?(value) do
datatype?(value) ->
if RDF.Literal.Datatype.valid?(value) do
if precision < 0 do
value.value
|> new_decimal()
@ -437,7 +383,7 @@ defmodule RDF.XSD.Numeric do
end
end
RDF.XSD.literal?(value) ->
RDF.Literal.datatype?(value) ->
nil
true ->
@ -494,12 +440,12 @@ defmodule RDF.XSD.Numeric do
def ceil(value) do
cond do
literal?(value) ->
if RDF.XSD.valid?(value) do
datatype?(value) ->
if RDF.Literal.Datatype.valid?(value) do
literal(value)
end
RDF.XSD.literal?(value) ->
RDF.Literal.datatype?(value) ->
nil
true ->
@ -550,10 +496,10 @@ defmodule RDF.XSD.Numeric do
def floor(value) do
cond do
literal?(value) ->
if RDF.XSD.valid?(value), do: literal(value)
datatype?(value) ->
if RDF.Literal.Datatype.valid?(value), do: literal(value)
RDF.XSD.literal?(value) ->
RDF.Literal.datatype?(value) ->
nil
true ->
@ -578,8 +524,8 @@ defmodule RDF.XSD.Numeric do
cond do
is_nil(left) -> nil
is_nil(right) -> nil
not RDF.XSD.literal?(left) -> arithmetic_operation(op, RDF.Literal.coerce(left), right, fun)
not RDF.XSD.literal?(right) -> arithmetic_operation(op, left, RDF.Literal.coerce(right), fun)
not RDF.Literal.datatype?(left) -> arithmetic_operation(op, RDF.Literal.coerce(left), right, fun)
not RDF.Literal.datatype?(right) -> arithmetic_operation(op, left, RDF.Literal.coerce(right), fun)
true -> false
end
end

View file

@ -65,7 +65,7 @@ defmodule RDF.XSD.String do
end
def do_cast(%datatype{} = literal) do
if RDF.XSD.datatype?(datatype) do
if RDF.Literal.Datatype.Registry.xsd_datatype_struct?(datatype) do
default_canonical_cast(literal, datatype)
end
end

View file

@ -33,9 +33,14 @@ defmodule RDF.XSD.Datatype.Test.Case do
@invalid unquote(invalid)
test "registration" do
assert unquote(datatype) in XSD.datatypes()
assert XSD.datatype_by_name(unquote(datatype_name)) == unquote(datatype)
assert XSD.datatype_by_iri(unquote(datatype_iri)) == unquote(datatype)
assert unquote(datatype) in RDF.Literal.Datatype.Registry.builtin_datatypes()
assert unquote(datatype) in RDF.Literal.Datatype.Registry.builtin_xsd_datatypes()
assert unquote(datatype) |> RDF.Literal.Datatype.Registry.builtin_datatype?()
assert unquote(datatype) |> RDF.Literal.Datatype.Registry.builtin_xsd_datatype?()
assert RDF.Literal.Datatype.get(unquote(datatype_iri)) == unquote(datatype)
assert XSD.Datatype.get(unquote(datatype_iri)) == unquote(datatype)
end
test "primitive/0" do
@ -59,7 +64,7 @@ defmodule RDF.XSD.Datatype.Test.Case do
end
test "derived_from?/1" do
assert unquote(datatype).derived_from?(unquote(datatype)) == true
assert unquote(datatype).derived_from?(unquote(datatype)) == false
unless unquote(primitive) do
assert unquote(datatype).derived_from?(unquote(base)) == true
@ -67,6 +72,26 @@ defmodule RDF.XSD.Datatype.Test.Case do
end
end
describe "datatype?/1" do
test "with itself" do
assert unquote(datatype).datatype?(unquote(datatype)) == true
end
test "with non-RDF values" do
assert unquote(datatype).datatype?(self()) == false
assert unquote(datatype).datatype?(Elixir.Enum) == false
assert unquote(datatype).datatype?(:foo) == false
end
unless unquote(primitive) do
test "on a base datatype" do
# We're using apply here to suppress "nil.datatype?/1 is undefined" warnings caused by the primitives
assert apply(unquote(base), :datatype?, [unquote(datatype)]) == true
assert apply(unquote(base_primitive), :datatype?, [unquote(datatype)]) == true
end
end
end
test "applicable_facets/0" do
assert MapSet.new(unquote(datatype).applicable_facets()) ==
MapSet.new(unquote(applicable_facets))
@ -88,10 +113,36 @@ defmodule RDF.XSD.Datatype.Test.Case do
assert unquote(datatype).id() == RDF.iri(unquote(datatype_iri))
end
test "language/1" do
Enum.each(@valid, fn {input, _} ->
assert (unquote(datatype).new(input) |> unquote(datatype).language()) == nil
end)
describe "general datatype?/1" do
test "on the exact same datatype" do
assert (unquote(datatype).datatype?(unquote(datatype))) == true
Enum.each(@valid, fn {input, _} ->
literal = unquote(datatype).new(input)
assert (unquote(datatype).datatype?(literal)) == true
assert (unquote(datatype).datatype?(literal.literal)) == true
end)
end
unless unquote(primitive) do
test "on the base datatype" do
assert (unquote(base).datatype?(unquote(datatype))) == true
Enum.each(@valid, fn {input, _} ->
literal = unquote(datatype).new(input)
assert (unquote(base).datatype?(literal)) == true
assert (unquote(base).datatype?(literal.literal)) == true
end)
end
test "on the base primitive datatype" do
assert (unquote(base_primitive).datatype?(unquote(datatype))) == true
Enum.each(@valid, fn {input, _} ->
literal = unquote(datatype).new(input)
assert (unquote(base_primitive).datatype?(literal)) == true
assert (unquote(base_primitive).datatype?(literal.literal)) == true
end)
end
end
end
test "datatype/1" do
@ -100,6 +151,12 @@ defmodule RDF.XSD.Datatype.Test.Case do
end)
end
test "language/1" do
Enum.each(@valid, fn {input, _} ->
assert (unquote(datatype).new(input) |> unquote(datatype).language()) == nil
end)
end
describe "general new" do
Enum.each(@valid, fn {input, {value, lexical, _}} ->
expected = %RDF.Literal{

View file

@ -90,6 +90,15 @@ defmodule RDF.Literal.GenericTest do
end
end
test "datatype?/1" do
assert Generic.datatype?(Generic) == true
Enum.each @valid, fn {input, {_, datatype}} ->
literal = Generic.new(input, datatype: datatype)
assert Generic.datatype?(literal) == true
assert Generic.datatype?(literal.literal) == true
end
end
test "datatype/1" do
Enum.each @valid, fn {input, {_, datatype}} ->
assert (Generic.new(input, datatype: datatype) |> Generic.datatype()) == RDF.iri(datatype)

View file

@ -100,6 +100,15 @@ defmodule RDF.LangStringTest do
end
end
test "datatype?/1" do
assert LangString.datatype?(LangString) == true
Enum.each @valid, fn {input, {_, language}} ->
literal = LangString.new(input, language: language)
assert LangString.datatype?(literal) == true
assert LangString.datatype?(literal.literal) == true
end
end
test "datatype/1" do
Enum.each @valid, fn {input, {_, language}} ->
assert (LangString.new(input, language: language) |> LangString.datatype()) == RDF.iri(LangString.id())

View file

@ -4,6 +4,7 @@ defmodule RDF.Literal.Datatype.RegistryTest do
alias RDF.TestDatatypes.Age
alias RDF.Literal.Datatype
alias RDF.NS
alias RDF.TestDatatypes
@unsupported_xsd_datatypes ~w[
ENTITIES
@ -37,8 +38,8 @@ defmodule RDF.Literal.Datatype.RegistryTest do
describe "datatype/1" do
test "core datatypes" do
Enum.each(Datatype.Registry.core_datatypes(), fn datatype ->
test "builtin datatypes" do
Enum.each(Datatype.Registry.builtin_datatypes(), fn datatype ->
assert datatype == Datatype.Registry.datatype(datatype.id)
assert datatype == Datatype.Registry.datatype(to_string(datatype.id))
end)
@ -67,21 +68,40 @@ defmodule RDF.Literal.Datatype.RegistryTest do
end
end
describe "xsd_datatype/1" do
test "when a core XSD datatype with the given IRI exists" do
assert XSD.String = Datatype.Registry.xsd_datatype(NS.XSD.string)
end
test "datatype?/1" do
assert Datatype.Registry.datatype?(XSD.string("foo"))
assert Datatype.Registry.datatype?(~L"foo"en)
assert Datatype.Registry.datatype?(XSD.integer(42))
assert Datatype.Registry.datatype?(XSD.byte(42))
assert Datatype.Registry.datatype?(TestDatatypes.Age.new(42))
assert Datatype.Registry.datatype?(RDF.literal("foo", datatype: "http://example.com"))
refute Datatype.Registry.datatype?(~r/foo/)
refute Datatype.Registry.datatype?(:foo)
refute Datatype.Registry.datatype?(42)
end
test "when a custom XSD datatype with the given IRI exists" do
assert Age = Datatype.Registry.xsd_datatype(EX.Age)
end
test "xsd_datatype?/1" do
assert Datatype.Registry.xsd_datatype?(XSD.string("foo"))
assert Datatype.Registry.xsd_datatype?(XSD.integer(42))
assert Datatype.Registry.xsd_datatype?(XSD.byte(42))
assert Datatype.Registry.xsd_datatype?(TestDatatypes.Age.new(42))
refute Datatype.Registry.xsd_datatype?(~L"foo"en)
refute Datatype.Registry.xsd_datatype?(RDF.literal("foo", datatype: "http://example.com"))
refute Datatype.Registry.xsd_datatype?(~r/foo/)
refute Datatype.Registry.xsd_datatype?(:foo)
refute Datatype.Registry.xsd_datatype?(42)
end
test "when datatype with the given IRI exists, but it is not an XSD datatype" do
refute Datatype.Registry.xsd_datatype(RDF.langString)
end
test "when no datatype with the given IRI exists" do
refute Datatype.Registry.xsd_datatype(EX.foo)
end
test "numeric_datatype?/1" do
assert Datatype.Registry.numeric_datatype?(XSD.integer(42))
assert Datatype.Registry.numeric_datatype?(XSD.byte(42))
assert Datatype.Registry.numeric_datatype?(TestDatatypes.Age.new(42))
refute Datatype.Registry.numeric_datatype?(XSD.string("foo"))
refute Datatype.Registry.numeric_datatype?(~L"foo"en)
refute Datatype.Registry.numeric_datatype?(RDF.literal("foo", datatype: "http://example.com"))
refute Datatype.Registry.numeric_datatype?(~r/foo/)
refute Datatype.Registry.numeric_datatype?(:foo)
refute Datatype.Registry.numeric_datatype?(42)
end
end

View file

@ -30,8 +30,8 @@ defmodule RDF.LiteralTest do
end
end
test "with core datatype literals" do
Enum.each Datatype.Registry.core_datatypes(), fn datatype ->
test "with builtin datatype literals" do
Enum.each Datatype.Registry.builtin_datatypes(), fn datatype ->
datatype_literal = datatype.new("foo").literal
assert %Literal{literal: ^datatype_literal} = Literal.new(datatype_literal)
end

View file

@ -1,18 +0,0 @@
defmodule RDF.XSD.DatatypeTest do
use ExUnit.Case
alias RDF.XSD
test "base_primitive/1" do
assert XSD.integer(42) |> XSD.Datatype.base_primitive() == XSD.Integer
assert XSD.non_negative_integer(42) |> XSD.Datatype.base_primitive() == XSD.Integer
assert XSD.positive_integer(42) |> XSD.Datatype.base_primitive() == XSD.Integer
end
test "derived_from?/2" do
assert XSD.integer(42) |> XSD.Datatype.derived_from?(XSD.Integer)
assert XSD.non_negative_integer(42) |> XSD.Datatype.derived_from?(XSD.Integer)
assert XSD.positive_integer(42) |> XSD.Datatype.derived_from?(XSD.Integer)
assert XSD.positive_integer(42) |> XSD.Datatype.derived_from?(XSD.NonNegativeInteger)
end
end

View file

@ -3,8 +3,8 @@ defmodule RDF.XSDTest do
doctest RDF.XSD
test "Datatype constructor alias functions" do
Enum.each(XSD.datatypes(), fn datatype ->
test "builtin datatype constructor alias functions" do
Enum.each(RDF.Literal.Datatype.Registry.builtin_xsd_datatypes(), fn datatype ->
assert apply(XSD, String.to_atom(datatype.name), [1]) == datatype.new(1)
assert apply(XSD, String.to_atom(Macro.underscore(datatype.name)), [1]) == datatype.new(1)
end)