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(), to: BlankNode, as: :new
defdelegate bnode(id), 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), to: Literal, as: :new
defdelegate literal(value, opts), 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(s, p, o), to: Triple, as: :new
defdelegate triple(tuple), to: Triple, as: :new defdelegate triple(tuple), to: Triple, as: :new

View file

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

View file

@ -103,16 +103,16 @@ defmodule RDF.Literal do
def coerce(%NaiveDateTime{} = value), do: RDF.XSD.DateTime.new(value) def coerce(%NaiveDateTime{} = value), do: RDF.XSD.DateTime.new(value)
def coerce(%URI{} = value), do: RDF.XSD.AnyURI.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 # 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 def coerce(%unquote(datatype){} = datatype_literal) do
%__MODULE__{literal: datatype_literal} %__MODULE__{literal: datatype_literal}
end end
end) end)
def coerce(%_datatype{} = datatype_literal) do def coerce(%datatype{} = datatype_literal) do
if Datatype.Registry.literal?(datatype_literal) do if Datatype.Registry.datatype_struct?(datatype) do
%__MODULE__{literal: datatype_literal} %__MODULE__{literal: datatype_literal}
end end
end end
@ -148,6 +148,23 @@ defmodule RDF.Literal do
end end
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 """ @doc """
Returns if a literal is a language-tagged literal. Returns if a literal is a language-tagged literal.
@ -178,7 +195,7 @@ defmodule RDF.Literal do
""" """
@spec simple?(t) :: boolean @spec simple?(t) :: boolean
def simple?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true def simple?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true
def simple?(%__MODULE__{} = _), do: false def simple?(%__MODULE__{}), do: false
@doc """ @doc """
@ -192,11 +209,7 @@ defmodule RDF.Literal do
@spec plain?(t) :: boolean @spec plain?(t) :: boolean
def plain?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true def plain?(%__MODULE__{literal: %RDF.XSD.String{}}), do: true
def plain?(%__MODULE__{literal: %LangString{}}), do: true def plain?(%__MODULE__{literal: %LangString{}}), do: true
def plain?(%__MODULE__{} = _), do: false def plain?(%__MODULE__{}), do: false
@spec typed?(t) :: boolean
def typed?(literal), do: not plain?(literal)
############################################################################ ############################################################################
# functions delegating to the RDF.Literal.Datatype of a RDF.Literal # 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 @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 """ @doc """
The datatype IRI of the given `RDF.Literal`. 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 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 defmacro __using__(opts) do
name = Keyword.fetch!(opts, :name) name = Keyword.fetch!(opts, :name)
id = Keyword.fetch!(opts, :id) id = Keyword.fetch!(opts, :id)
@ -157,6 +177,13 @@ defmodule RDF.Literal.Datatype do
quote do quote do
@behaviour unquote(__MODULE__) @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) @name unquote(name)
@impl unquote(__MODULE__) @impl unquote(__MODULE__)
def name, do: @name def name, do: @name
@ -165,6 +192,16 @@ defmodule RDF.Literal.Datatype do
@impl unquote(__MODULE__) @impl unquote(__MODULE__)
def id, do: @id 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__) @impl unquote(__MODULE__)
def datatype(%Literal{literal: literal}), do: datatype(literal) def datatype(%Literal{literal: literal}), do: datatype(literal)
def datatype(%__MODULE__{}), do: @id def datatype(%__MODULE__{}), do: @id
@ -227,8 +264,8 @@ defmodule RDF.Literal.Datatype do
def equal_value?(_, nil), do: nil def equal_value?(_, nil), do: nil
def equal_value?(left, right) do def equal_value?(left, right) do
cond do cond do
not RDF.literal?(right) and not RDF.term?(right) -> equal_value?(left, Literal.coerce(right)) not Literal.datatype?(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?(left) and not RDF.term?(left) -> equal_value?(Literal.coerce(left), right)
true -> do_equal_value?(left, right) true -> do_equal_value?(left, right)
end end
end end
@ -256,6 +293,9 @@ defmodule RDF.Literal.Datatype do
|> new() |> new()
end 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} defp literal(datatype_literal), do: %Literal{literal: datatype_literal}
defoverridable datatype: 1, defoverridable datatype: 1,

View file

@ -5,34 +5,162 @@ defmodule RDF.Literal.Datatype.Registry do
alias RDF.Literal.Datatype.Registry.Registration alias RDF.Literal.Datatype.Registry.Registration
import RDF.Guards 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 """ @doc """
All core `RDF.Literal.Datatype` modules. Returns a list of all builtin `RDF.Literal.Datatype` modules.
""" """
@spec core_datatypes :: Enum.t @spec builtin_datatypes :: [RDF.Literal.Datatype.t]
def core_datatypes, do: @core_datatypes def builtin_datatypes, do: @builtin_datatypes
@doc """ @doc """
Checks if the given module is core datatype. Checks if the given module is a builtin datatype.
"""
@spec core_datatype?(module) :: boolean
def core_datatype?(module), do: module in @core_datatypes
@doc """ Note: This doesn't include `RDF.Literal.Generic`.
Checks if the given module is a core datatype or a registered custom datatype implementing the `RDF.Literal.Datatype` behaviour.
""" """
@spec datatype?(module) :: boolean @spec builtin_datatype?(module) :: boolean
def datatype?(module) do def builtin_datatype?(module)
core_datatype?(module) or implements_behaviour?(module, Literal.Datatype)
for datatype <- @builtin_datatypes do
def builtin_datatype?(unquote(datatype)), do: true
end end
@spec literal?(module) :: boolean def builtin_datatype?(_), do: false
def literal?(%Literal{}), do: true
def literal?(%Literal.Generic{}), do: true @doc """
def literal?(%datatype{}), do: datatype?(datatype) Checks if the given module is a builtin datatype or a registered custom datatype implementing the `RDF.Literal.Datatype` behaviour.
def literal?(_), do: false """
@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 """ @doc """
Returns the `RDF.Literal.Datatype` for a datatype IRI. Returns the `RDF.Literal.Datatype` for a datatype IRI.
@ -50,15 +178,31 @@ defmodule RDF.Literal.Datatype.Registry do
def xsd_datatype(id) do def xsd_datatype(id) do
datatype = datatype(id) datatype = datatype(id)
if datatype && implements_behaviour?(datatype, XSD.Datatype) do if datatype && is_xsd_datatype?(datatype) do
datatype datatype
end end
end end
defp implements_behaviour?(module, behaviour) do # TODO: Find a better/faster solution for checking datatype modules which includes unknown custom datatypes.
module.module_info[:attributes] # Although checking for the presence of a function via __info__(:functions) is
|> Keyword.get_values(:behaviour) # the fastest way to reflect a module type on average over the positive and negative
|> List.flatten() # case (being roughly comparable to a map access), we would still have to rescue
|> Enum.member?(behaviour) # 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
end end

View file

@ -9,6 +9,8 @@ defmodule RDF.LangString do
name: "langString", name: "langString",
id: RDF.Utils.Bootstrapping.rdf_iri("langString") id: RDF.Utils.Bootstrapping.rdf_iri("langString")
import RDF.Utils.Guards
alias RDF.Literal.Datatype alias RDF.Literal.Datatype
alias RDF.Literal alias RDF.Literal
@ -21,7 +23,7 @@ defmodule RDF.LangString do
@spec new(any, String.t | atom | keyword) :: Literal.t @spec new(any, String.t | atom | keyword) :: Literal.t
def new(value, language_or_opts \\ []) 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_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 def new(value, opts) do
%Literal{ %Literal{
literal: %__MODULE__{ literal: %__MODULE__{
@ -33,7 +35,7 @@ defmodule RDF.LangString do
defp normalize_language(nil), do: nil defp normalize_language(nil), do: nil
defp normalize_language(""), 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) defp normalize_language(language), do: String.downcase(language)
@impl RDF.Literal.Datatype @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/> see <https://www.w3.org/TR/xmlschema-2/>
""" """
alias __MODULE__ import RDF.Utils.Guards
alias RDF.{IRI, Literal}
@datatypes [ alias __MODULE__
XSD.Boolean,
XSD.String,
XSD.Date,
XSD.Time,
XSD.DateTime,
XSD.AnyURI
]
|> MapSet.new()
|> MapSet.union(XSD.Numeric.datatypes())
@facets [ @facets [
XSD.Facets.MinInclusive, XSD.Facets.MinInclusive,
@ -32,39 +22,15 @@ defmodule RDF.XSD do
@facets_by_name Map.new(@facets, fn facet -> {facet.name(), facet} end) @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] def facet(name), do: @facets_by_name[name]
@doc """ @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() defdelegate datatype?(value), to: RDF.Literal.Datatype.Registry, as: :xsd_datatype?
def datatypes(), do: @datatypes
@datatypes_by_name Map.new(@datatypes, fn datatype -> {datatype.name(), datatype} end) for datatype <- RDF.Literal.Datatype.Registry.builtin_xsd_datatypes() do
@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
defdelegate unquote(String.to_atom(datatype.name))(value), to: datatype, as: :new 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 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 @type comparison_result :: :lt | :gt | :eq
import RDF.Utils.Guards
@doc """ @doc """
Returns if the `RDF.XSD.Datatype` is a primitive datatype. Returns if the `RDF.XSD.Datatype` is a primitive datatype.
@ -34,15 +35,13 @@ defmodule RDF.XSD.Datatype do
@callback base_primitive :: t() @callback base_primitive :: t()
@doc """ @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 @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 """ @doc """
The set of applicable facets of a `RDF.XSD.Datatype`. 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() uncanonical_lexical: RDF.XSD.Datatype.uncanonical_lexical()
} }
@impl unquote(__MODULE__) @doc !"""
def derived_from?(datatype) 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 @impl RDF.Literal.Datatype
base = base() def datatype?(%RDF.Literal{literal: literal}), do: datatype?(literal)
not is_nil(base) and base.derived_from?(datatype) 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 end
def datatype?(_), do: false
@impl unquote(__MODULE__)
def derived?(literal), do: RDF.XSD.Datatype.derived_from?(literal, __MODULE__)
# Dialyzer causes a warning on all primitives since the facet_conform?/2 call # 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 # always returns true there, so the other branch is unnecessary. This could
@ -252,14 +254,4 @@ defmodule RDF.XSD.Datatype do
end end
end 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 end

View file

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

View file

@ -20,6 +20,10 @@ defmodule RDF.XSD.Datatype.Restriction do
@impl RDF.XSD.Datatype @impl RDF.XSD.Datatype
def base_primitive, do: @base.base_primitive() 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 @impl RDF.XSD.Datatype
def applicable_facets, do: @base.applicable_facets() def applicable_facets, do: @base.applicable_facets()

View file

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

View file

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

View file

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

View file

@ -3,67 +3,13 @@ defmodule RDF.XSD.Numeric do
Collection of functions for numeric literals. Collection of functions for numeric literals.
""" """
@type t :: module
alias Elixir.Decimal, as: D alias Elixir.Decimal, as: D
import Kernel, except: [abs: 1, floor: 1, ceil: 1] import Kernel, except: [abs: 1, floor: 1, ceil: 1]
@datatypes MapSet.new([ defdelegate datatype?(value), to: RDF.Literal.Datatype.Registry, as: :numeric_datatype?
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
@doc """ @doc """
Tests for numeric value equality of two numeric XSD datatyped literals. Tests for numeric value equality of two numeric XSD datatyped literals.
@ -346,8 +292,8 @@ defmodule RDF.XSD.Numeric do
def abs(value) do def abs(value) do
cond do cond do
literal?(value) -> datatype?(value) ->
if RDF.XSD.valid?(value) do if RDF.Literal.Datatype.valid?(value) do
%datatype{} = value %datatype{} = value
case value.value do case value.value do
@ -367,7 +313,7 @@ defmodule RDF.XSD.Numeric do
end end
end end
RDF.XSD.literal?(value) -> RDF.Literal.datatype?(value) ->
nil nil
true -> true ->
@ -424,8 +370,8 @@ defmodule RDF.XSD.Numeric do
def round(value, precision) do def round(value, precision) do
cond do cond do
literal?(value) -> datatype?(value) ->
if RDF.XSD.valid?(value) do if RDF.Literal.Datatype.valid?(value) do
if precision < 0 do if precision < 0 do
value.value value.value
|> new_decimal() |> new_decimal()
@ -437,7 +383,7 @@ defmodule RDF.XSD.Numeric do
end end
end end
RDF.XSD.literal?(value) -> RDF.Literal.datatype?(value) ->
nil nil
true -> true ->
@ -494,12 +440,12 @@ defmodule RDF.XSD.Numeric do
def ceil(value) do def ceil(value) do
cond do cond do
literal?(value) -> datatype?(value) ->
if RDF.XSD.valid?(value) do if RDF.Literal.Datatype.valid?(value) do
literal(value) literal(value)
end end
RDF.XSD.literal?(value) -> RDF.Literal.datatype?(value) ->
nil nil
true -> true ->
@ -550,10 +496,10 @@ defmodule RDF.XSD.Numeric do
def floor(value) do def floor(value) do
cond do cond do
literal?(value) -> datatype?(value) ->
if RDF.XSD.valid?(value), do: literal(value) if RDF.Literal.Datatype.valid?(value), do: literal(value)
RDF.XSD.literal?(value) -> RDF.Literal.datatype?(value) ->
nil nil
true -> true ->
@ -578,8 +524,8 @@ defmodule RDF.XSD.Numeric do
cond do cond do
is_nil(left) -> nil is_nil(left) -> nil
is_nil(right) -> nil is_nil(right) -> nil
not RDF.XSD.literal?(left) -> arithmetic_operation(op, RDF.Literal.coerce(left), right, fun) not RDF.Literal.datatype?(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?(right) -> arithmetic_operation(op, left, RDF.Literal.coerce(right), fun)
true -> false true -> false
end end
end end

View file

@ -65,7 +65,7 @@ defmodule RDF.XSD.String do
end end
def do_cast(%datatype{} = literal) do 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) default_canonical_cast(literal, datatype)
end end
end end

View file

@ -33,9 +33,14 @@ defmodule RDF.XSD.Datatype.Test.Case do
@invalid unquote(invalid) @invalid unquote(invalid)
test "registration" do test "registration" do
assert unquote(datatype) in XSD.datatypes() assert unquote(datatype) in RDF.Literal.Datatype.Registry.builtin_datatypes()
assert XSD.datatype_by_name(unquote(datatype_name)) == unquote(datatype) assert unquote(datatype) in RDF.Literal.Datatype.Registry.builtin_xsd_datatypes()
assert XSD.datatype_by_iri(unquote(datatype_iri)) == unquote(datatype)
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 end
test "primitive/0" do test "primitive/0" do
@ -59,7 +64,7 @@ defmodule RDF.XSD.Datatype.Test.Case do
end end
test "derived_from?/1" do 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 unless unquote(primitive) do
assert unquote(datatype).derived_from?(unquote(base)) == true assert unquote(datatype).derived_from?(unquote(base)) == true
@ -67,6 +72,26 @@ defmodule RDF.XSD.Datatype.Test.Case do
end end
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 test "applicable_facets/0" do
assert MapSet.new(unquote(datatype).applicable_facets()) == assert MapSet.new(unquote(datatype).applicable_facets()) ==
MapSet.new(unquote(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)) assert unquote(datatype).id() == RDF.iri(unquote(datatype_iri))
end end
test "language/1" do describe "general datatype?/1" do
Enum.each(@valid, fn {input, _} -> test "on the exact same datatype" do
assert (unquote(datatype).new(input) |> unquote(datatype).language()) == nil assert (unquote(datatype).datatype?(unquote(datatype))) == true
end) 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 end
test "datatype/1" do test "datatype/1" do
@ -100,6 +151,12 @@ defmodule RDF.XSD.Datatype.Test.Case do
end) end)
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 describe "general new" do
Enum.each(@valid, fn {input, {value, lexical, _}} -> Enum.each(@valid, fn {input, {value, lexical, _}} ->
expected = %RDF.Literal{ expected = %RDF.Literal{

View file

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

View file

@ -100,6 +100,15 @@ defmodule RDF.LangStringTest do
end end
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 test "datatype/1" do
Enum.each @valid, fn {input, {_, language}} -> Enum.each @valid, fn {input, {_, language}} ->
assert (LangString.new(input, language: language) |> LangString.datatype()) == RDF.iri(LangString.id()) 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.TestDatatypes.Age
alias RDF.Literal.Datatype alias RDF.Literal.Datatype
alias RDF.NS alias RDF.NS
alias RDF.TestDatatypes
@unsupported_xsd_datatypes ~w[ @unsupported_xsd_datatypes ~w[
ENTITIES ENTITIES
@ -37,8 +38,8 @@ defmodule RDF.Literal.Datatype.RegistryTest do
describe "datatype/1" do describe "datatype/1" do
test "core datatypes" do test "builtin datatypes" do
Enum.each(Datatype.Registry.core_datatypes(), fn datatype -> Enum.each(Datatype.Registry.builtin_datatypes(), fn datatype ->
assert datatype == Datatype.Registry.datatype(datatype.id) assert datatype == Datatype.Registry.datatype(datatype.id)
assert datatype == Datatype.Registry.datatype(to_string(datatype.id)) assert datatype == Datatype.Registry.datatype(to_string(datatype.id))
end) end)
@ -67,21 +68,40 @@ defmodule RDF.Literal.Datatype.RegistryTest do
end end
end end
describe "xsd_datatype/1" do test "datatype?/1" do
test "when a core XSD datatype with the given IRI exists" do assert Datatype.Registry.datatype?(XSD.string("foo"))
assert XSD.String = Datatype.Registry.xsd_datatype(NS.XSD.string) assert Datatype.Registry.datatype?(~L"foo"en)
end 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 test "xsd_datatype?/1" do
assert Age = Datatype.Registry.xsd_datatype(EX.Age) assert Datatype.Registry.xsd_datatype?(XSD.string("foo"))
end 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 test "numeric_datatype?/1" do
refute Datatype.Registry.xsd_datatype(EX.foo) assert Datatype.Registry.numeric_datatype?(XSD.integer(42))
end 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
end end

View file

@ -30,8 +30,8 @@ defmodule RDF.LiteralTest do
end end
end end
test "with core datatype literals" do test "with builtin datatype literals" do
Enum.each Datatype.Registry.core_datatypes(), fn datatype -> Enum.each Datatype.Registry.builtin_datatypes(), fn datatype ->
datatype_literal = datatype.new("foo").literal datatype_literal = datatype.new("foo").literal
assert %Literal{literal: ^datatype_literal} = Literal.new(datatype_literal) assert %Literal{literal: ^datatype_literal} = Literal.new(datatype_literal)
end 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 doctest RDF.XSD
test "Datatype constructor alias functions" do test "builtin datatype constructor alias functions" do
Enum.each(XSD.datatypes(), fn datatype -> 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(datatype.name), [1]) == datatype.new(1)
assert apply(XSD, String.to_atom(Macro.underscore(datatype.name)), [1]) == datatype.new(1) assert apply(XSD, String.to_atom(Macro.underscore(datatype.name)), [1]) == datatype.new(1)
end) end)