lib/rdf/xsd/facet.ex

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