rdf-ex/lib/rdf/xsd/facet.ex
2020-06-01 15:43:38 +02:00

155 lines
6.5 KiB
Elixir

defmodule RDF.XSD.Facet do
@moduledoc """
A behaviour for XSD restriction facets.
Here's a list of all the `RDF.XSD.Facet`s RDF.ex implements out-of-the-box:
| XSD facet | `RDF.XSD.Facet` |
| :-------------- | :------------- |
| length | `RDF.XSD.Facets.Length` |
| minLength | `RDF.XSD.Facets.MinLength` |
| maxLength | `RDF.XSD.Facets.MaxLength` |
| maxInclusive | `RDF.XSD.Facets.MaxInclusive` |
| maxExclusive | `RDF.XSD.Facets.MaxExclusive` |
| minInclusive | `RDF.XSD.Facets.MinInclusive` |
| minExclusive | `RDF.XSD.Facets.MinExclusive` |
| totalDigits | `RDF.XSD.Facets.TotalDigits` |
| fractionDigits | `RDF.XSD.Facets.FractionDigits` |
| explicitTimezone | `RDF.XSD.Facets.ExplicitTimezone` |
| pattern | `RDF.XSD.Facets.Pattern` |
| whiteSpace | ❌ |
| enumeration | ❌ |
| assertions | ❌ |
Every `RDF.XSD.Datatype.Primitive` defines a set of applicable constraining facets which are can
be used on derivations of this primitive or any of its existing derivations:
| Primitive datatype | Applicable facets |
| :----------------- | :---------------- |
|string | `RDF.XSD.Facets.Length`, `RDF.XSD.Facets.MaxLength`, `RDF.XSD.Facets.MinLength`, `RDF.XSD.Facets.Pattern` |
|boolean | `RDF.XSD.Facets.Pattern` |
|float | `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern` |
|double | `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern` |
|decimal | `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern`, `RDF.XSD.Facets.TotalDigits`, `RDF.XSD.Facets.FractionDigits` |
|decimal | `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern`, `RDF.XSD.Facets.TotalDigits` |
|duration | `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern` |
|dateTime | `RDF.XSD.Facets.ExplicitTimezone`, `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern` |
|time | `RDF.XSD.Facets.ExplicitTimezone`, `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern` |
|date | `RDF.XSD.Facets.ExplicitTimezone`, `RDF.XSD.Facets.MaxExclusive`, `RDF.XSD.Facets.MaxInclusive`, `RDF.XSD.Facets.MinExclusive`, `RDF.XSD.Facets.MinInclusive`, `RDF.XSD.Facets.Pattern` |
|anyURI | `RDF.XSD.Facets.Length`, `RDF.XSD.Facets.MaxLength`, `RDF.XSD.Facets.MinLength`, `RDF.XSD.Facets.Pattern` |
<https://www.w3.org/TR/xmlschema11-2/datatypes.html#rf-facets>
"""
@type t :: module
@doc """
The name of a `RDF.XSD.Facet`.
"""
@callback name :: String.t()
defmacro __using__(opts) do
name = Keyword.fetch!(opts, :name)
type_ast = Keyword.fetch!(opts, :type)
quote bind_quoted: [], unquote: true do
@behaviour RDF.XSD.Facet
@doc """
Returns the value of this `RDF.XSD.Facet` on specific `RDF.XSD.Datatype`.
"""
@callback unquote(name)() :: unquote(type_ast) | nil
@doc """
Validates if a `value` and `lexical` conforms with a concrete `facet_constaint_value` for this `RDF.XSD.Facet`.
This function must be implemented on a `RDF.XSD.Datatype` using this `RDF.XSD.Facet`.
"""
@callback unquote(conform_fun_name(name))(
facet_constaint_value :: any,
value :: any,
RDF.XSD.Datatype.uncanonical_lexical()
) :: boolean
@name unquote(Atom.to_string(name))
@impl RDF.XSD.Facet
def name, do: @name
@doc """
Checks if a `value` and `lexical` conforms with the `c:#{unquote(conform_fun_name(name))}/3` implementation on the `datatype` `RDF.XSD.Datatype`.
"""
@spec conform?(RDF.XSD.Datatype.t(), any, RDF.XSD.Datatype.uncanonical_lexical()) :: boolean
def conform?(datatype, value, lexical) do
constrain_value = apply(datatype, unquote(name), [])
is_nil(constrain_value) or
apply(datatype, unquote(conform_fun_name(name)), [constrain_value, value, lexical])
end
defmacro __using__(_opts) do
import unquote(__MODULE__)
default_facet_impl(__MODULE__, unquote(name))
end
end
end
defp conform_fun_name(facet_name), do: :"#{facet_name}_conform?"
@doc """
Macro for the definition of concrete constraining `value` for a `RDF.XSD.Facet` on a `RDF.XSD.Datatype`.
"""
defmacro def_facet_constraint(facet, value) do
facet_mod = Macro.expand_once(facet, __CALLER__)
facet_name = String.to_atom(facet_mod.name)
quote do
unless unquote(facet) in @base.applicable_facets,
do: raise("#{unquote(facet_name)} is not an applicable facet of #{@base}")
@facets unquote(facet_name)
@impl unquote(facet)
def unquote(facet_name)(), do: unquote(value)
end
end
@doc false
def default_facet_impl(facet_mod, facet_name) do
quote do
@behaviour unquote(facet_mod)
Module.put_attribute(__MODULE__, unquote(facet_mod), nil)
@impl unquote(facet_mod)
def unquote(facet_name)(), do: nil
defoverridable [{unquote(facet_name), 0}]
end
end
@doc false
def restriction_impl(facets, applicable_facets) do
Enum.map(applicable_facets, fn applicable_facet ->
applicable_facet_name = String.to_atom(applicable_facet.name)
quote do
@behaviour unquote(applicable_facet)
unless unquote(applicable_facet_name in facets) do
@impl unquote(applicable_facet)
def unquote(applicable_facet_name)(),
do: apply(@base, unquote(applicable_facet_name), [])
end
@impl unquote(applicable_facet)
def unquote(conform_fun_name(applicable_facet_name))(constrain_value, value, lexical) do
apply(@base, unquote(conform_fun_name(applicable_facet_name)), [
constrain_value,
value,
lexical
])
end
end
end)
end
end