import Croma.Defun alias Croma.Result, as: R defmodule Croma.SubtypeOfInt do @moduledoc """ Helper module to define an integer-based type. The following members are generated by `use Croma.SubtypeOfInt`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:min` - Minimum value of this type (inclusive). - `:max` - Maximum value of this type (inclusive). - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyInt do use Croma.SubtypeOfInt, min: 0, max: 10, default: 0 end """ defmacro __using__(opts) do quote do @min unquote(opts[:min]) @max unquote(opts[:max]) if !is_nil(@min) && !is_integer(@min), do: raise ":min must be either nil or integer" if !is_nil(@max) && !is_integer(@max), do: raise ":max must be either nil or integer" if is_nil(@min) && is_nil(@max) , do: raise ":min and/or :max must be given" if @min && @max && @max < @min , do: raise ":min must be smaller than :max" cond do is_nil(@min) -> cond do @max <= -1 -> @type t :: neg_integer true -> @type t :: integer end defun validate(term: any) :: R.t(t) do i when is_integer(i) and i <= @max -> {:ok, i} x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end is_nil(@max) -> cond do 1 <= @min -> @type t :: pos_integer 0 == @min -> @type t :: non_neg_integer true -> @type t :: integer end defun validate(term: any) :: R.t(t) do i when is_integer(i) and @min <= i -> {:ok, i} x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end true -> @type t :: unquote(opts[:min]) .. unquote(opts[:max]) defun validate(term: any) :: R.t(t) do i when is_integer(i) and @min <= i and i <= @max -> {:ok, i} x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end end @default unquote(opts[:default]) if @default do if !is_integer(@default) , do: raise ":default must be an integer" if !is_nil(@min) && @default < @min, do: raise ":default must be a valid value" if !is_nil(@max) && @max < @default, do: raise ":default must be a valid value" defun default :: t, do: @default end end end end defmodule Croma.SubtypeOfFloat do @moduledoc """ Helper module to define a float-based type. The following members are generated by `use Croma.SubtypeOfFloat`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:min` - Minimum value of this type (inclusive). - `:max` - Maximum value of this type (inclusive). - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyFloat do use Croma.SubtypeOfFloat, min: 0.0, max: 5.0, default: 0.0 end """ defmacro __using__(opts) do quote do @min unquote(opts[:min]) @max unquote(opts[:max]) if !is_nil(@min) && !is_float(@min), do: raise ":min must be either nil or float" if !is_nil(@max) && !is_float(@max), do: raise ":max must be either nil or float" if is_nil(@min) && is_nil(@max) , do: raise ":min and/or :max must be given" if @min && @max && @max < @min , do: raise ":min must be smaller than :max" @type t :: float cond do is_nil(@min) -> defun validate(term: any) :: R.t(t) do f when is_float(f) and f <= @max -> {:ok, f} x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end is_nil(@max) -> defun validate(term: any) :: R.t(t) do f when is_float(f) and @min <= f -> {:ok, f} x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end true -> defun validate(term: any) :: R.t(t) do f when is_float(f) and @min <= f and f <= @max -> {:ok, f} x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end end @default unquote(opts[:default]) if @default do if !is_float(@default) , do: raise ":default must be a float" if !is_nil(@min) && @default < @min, do: raise ":default must be a valid value" if !is_nil(@max) && @max < @default, do: raise ":default must be a valid value" defun default :: t, do: @default end end end end defmodule Croma.SubtypeOfString do @moduledoc """ Helper module to define a string-based type. The following members are generated by `use Croma.SubtypeOfString`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:pattern` - A regex pattern to check whether a string is classified into this type or not. - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyString do use Croma.SubtypeOfString, pattern: ~r/^foo|bar$/, default: "foo" end """ defmacro __using__(opts) do quote do @pattern unquote(opts[:pattern]) if !Regex.regex?(@pattern), do: raise ":pattern must be a regex" @type t :: String.t defun validate(s: term) :: R.t(t) do s when is_binary(s) -> if Regex.match?(@pattern, s) do {:ok, s} else {:error, "validation error for #{__MODULE__}: #{inspect s}"} end x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end @default unquote(opts[:default]) if @default do if !Regex.match?(@pattern, @default), do: raise ":default must be a valid string" defun default :: t, do: @default end end end end defmodule Croma.SubtypeOfAtom do @moduledoc """ Helper module to define a type whose members are a fixed set of atoms. The following members are generated by `use Croma.SubtypeOfAtom`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:values` - List of atoms of possible values. - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyAtom do use Croma.SubtypeOfAtom, values: [:foo, :bar, :baz], default: :foo end """ defp values_as_typespec([v ]), do: v defp values_as_typespec([h|t]), do: {:|, [], [h, values_as_typespec(t)]} defmacro __using__(opts) do value_atoms = opts[:values] || raise ":values must be present" if Enum.empty?(value_atoms), do: raise ":values must be present" value_strings = Enum.map(value_atoms, &Atom.to_string/1) values_as_typespec = values_as_typespec(value_atoms) quote do @type t :: unquote(values_as_typespec) defun validate(term: any) :: R.t(t) do a when is_atom(a) -> if a in unquote(value_atoms) do {:ok, a} else {:error, "validation error for #{__MODULE__}: #{inspect a}"} end s when is_binary(s) -> if s in unquote(value_strings) do {:ok, String.to_existing_atom(s)} else {:error, "validation error for #{__MODULE__}: #{inspect s}"} end x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end @default unquote(opts[:default]) if @default do unless @default in unquote(value_atoms), do: raise ":default must be a valid atom" defun default :: t, do: @default end end end end defmodule Croma.SubtypeOfList do @moduledoc """ Helper module to define a list-based type. The following members are generated by `use Croma.SubtypeOfList`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:elem_module` - A module that defines the type of the element. The module must provide `@type t` and `@spec validate(term) :: Croma.Result.t(t)`. - `:min_length` - Minimum length of valid values of this type (inclusive). - `:max_length` - Maximum length of valid values of this type (inclusive). - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyList do use Croma.SubtypeOfList, elem_module: MyInt, default: [] end """ defmacro __using__(opts) do mod = opts[:elem_module] || raise ":elem_module must be given" quote do @type t :: [unquote(mod).t] defun validate(term: any) :: R.t(t) do l when is_list(l) -> require R R.m do elems <- Enum.map(l, &unquote(mod).validate/1) |> R.sequence if valid_length?(length(elems)) do {:ok, elems} else {:error, "validation error for #{__MODULE__}: #{inspect l}"} end end x -> {:error, "validation error for #{__MODULE__}: #{inspect x}"} end @min unquote(opts[:min_length]) @max unquote(opts[:max_length]) cond do is_nil(@min) && is_nil(@max) -> defp valid_length?(_), do: true is_nil(@min) -> defp valid_length?(len), do: len <= @max is_nil(@max) -> defp valid_length?(len), do: @min <= len true -> defp valid_length?(len), do: @min <= len && len <= @max end @default unquote(opts[:default]) if @default do if Enum.any?(@default, fn e -> unquote(mod).validate(e) |> R.error? end), do: raise ":default must be a valid list" len = length(@default) if !is_nil(@min) && len < @min, do: raise ":default must be a valid list" if !is_nil(@max) && @max < len, do: raise ":default must be a valid list" defun default :: t, do: @default end end end end