import Croma.Defun alias Croma.Result, as: R defmodule Croma.SubtypeOfInt do @moduledoc """ Helper module to define integer-based types. 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 bind_quoted: [min: opts[:min], max: opts[:max], default: opts[:default]] do @min min @max 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} _ -> {:error, {:invalid_value, [__MODULE__]}} 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} _ -> {:error, {:invalid_value, [__MODULE__]}} end true -> @type t :: unquote(min) .. unquote(max) defun validate(term :: any) :: R.t(t) do i when is_integer(i) and @min <= i and i <= @max -> {:ok, i} _ -> {:error, {:invalid_value, [__MODULE__]}} end end if !is_nil(@min) do def min, do: @min end if !is_nil(@max) do def max, do: @max end if default do @default default 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 float-based types. 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 bind_quoted: [min: opts[:min], max: opts[:max], default: opts[:default]] do @min min @max 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} _ -> {:error, {:invalid_value, [__MODULE__]}} end is_nil(@max) -> defun validate(term :: any) :: R.t(t) do f when is_float(f) and @min <= f -> {:ok, f} _ -> {:error, {:invalid_value, [__MODULE__]}} end true -> defun validate(term :: any) :: R.t(t) do f when is_float(f) and @min <= f and f <= @max -> {:ok, f} _ -> {:error, {:invalid_value, [__MODULE__]}} end end if !is_nil(@min) do def min, do: @min end if !is_nil(@max) do def max, do: @max end if default do @default default 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 string-based types. 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 bind_quoted: [pattern: opts[:pattern], default: opts[:default]] do @pattern pattern if !Regex.regex?(@pattern), do: raise ":pattern must be a regex" def pattern, do: @pattern @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, {:invalid_value, [__MODULE__]}} end _ -> {:error, {:invalid_value, [__MODULE__]}} end if default do @default default 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 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 """ defmacro __using__(opts) do quote bind_quoted: [values: opts[:values], default: opts[:default]] do @values values if is_nil(@values) or Enum.empty?(@values), do: raise ":values must be present" def values, do: @values @value_strings Enum.map(@values, &Atom.to_string/1) @type t :: unquote(Croma.TypeUtil.list_to_type_union(@values)) defun validate(term :: any) :: R.t(t) do a when is_atom(a) -> if a in @values do {:ok, a} else {:error, {:invalid_value, [__MODULE__]}} end s when is_binary(s) -> if s in @value_strings do {:ok, String.to_existing_atom(s)} else {:error, {:invalid_value, [__MODULE__]}} end _ -> {:error, {:invalid_value, [__MODULE__]}} end if default do @default default if !Enum.member?(@values, @default), 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 list-based types. 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 quote bind_quoted: [mod: opts[:elem_module], min: opts[:min_length], max: opts[:max_length], default: opts[:default]] do @mod mod @type t :: [unquote(@mod).t] @min min @max max cond do is_nil(@min) && is_nil(@max) -> defmacrop valid_length?(_), do: true is_nil(@min) -> defmacrop valid_length?(len) do quote do: unquote(len) <= @max end is_nil(@max) -> defmacrop valid_length?(len) do quote do: @min <= unquote(len) end true -> defmacrop valid_length?(len) do quote do: @min <= unquote(len) && unquote(len) <= @max end end defun validate(term :: any) :: R.t(t) do l when is_list(l) -> valid_length? = valid_length?(length(l)) result = Enum.map(l, &@mod.validate/1) |> R.sequence case result do {:ok, _} when valid_length? -> result _ -> # suppress warning on unmatched case clause when both @min and @max are nil (`valid_length?` is always true) # by separating case expressions case result do {:ok , _} -> {:error, {:invalid_value, [__MODULE__]}} {:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)} end end _ -> {:error, {:invalid_value, [__MODULE__]}} end if !is_nil(@min) do def min_length, do: @min end if !is_nil(@max) do def max_length, do: @max end if default do @default default if Enum.any?(@default, fn e -> @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 is shorter than the given :min_length #{Integer.to_string(@min)}" if !is_nil(@max) && @max < len, do: raise ":default is longer than the given :max_length #{Integer.to_string(@max)}" defun default :: t, do: @default end end end end defmodule Croma.SubtypeOfMap do @moduledoc """ Helper module to define map-based types. The following members are generated by `use Croma.SubtypeOfMap`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:key_module` - A module that defines the type of keys. The module must provide `@type t` and `@spec validate(term) :: Croma.Result.t(t)`. - `:value_module` - A module that defines the type of values. The module must provide `@type t` and `@spec validate(term) :: Croma.Result.t(t)`. - `:min_size` - Minimum size of valid values of this type (inclusive). - `:max_size` - Maximum size of valid values of this type (inclusive). - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyMap do use Croma.SubtypeOfMap, key_module: MyString, value_module: MyInt, default: %{} end """ defmacro __using__(opts) do quote bind_quoted: [key_module: opts[:key_module], value_module: opts[:value_module], min_size: opts[:min_size], max_size: opts[:max_size], default: opts[:default]] do @key_module key_module @value_module value_module if is_nil(@key_module ), do: raise ":key_module must be given" if is_nil(@value_module), do: raise ":value_module must be given" @type t :: %{unquote(@key_module).t => unquote(@value_module).t} @min min_size @max max_size cond do is_nil(@min) && is_nil(@max) -> defmacrop valid_size?(_), do: true is_nil(@min) -> defmacrop valid_size?(size) do quote do: unquote(size) <= @max end is_nil(@max) -> defmacrop valid_size?(size) do quote do: @min <= unquote(size) end true -> defmacrop valid_size?(size) do quote do: @min <= unquote(size) and unquote(size) <= @max end end defun validate(term :: term) :: R.t(t) do m when is_map(m) and valid_size?(map_size(m)) -> results = Enum.map(m, fn {k0, v0} -> @key_module.validate(k0) |> R.bind(fn k -> @value_module.validate(v0) |> R.map(fn v -> {k, v} end) end) end) case R.sequence(results) do {:ok , kvs } -> {:ok, Enum.into(kvs, %{})} {:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)} end _ -> {:error, {:invalid_value, [__MODULE__]}} end if !is_nil(@min) do def min_size, do: @min end if !is_nil(@max) do def max_size, do: @max end if default do @default default if !is_map(@default), do: raise ":default must be a map" size = map_size(@default) if !is_nil(@min) && size < @min, do: raise "items in :default is less than the given :min_size #{Integer.to_string(@min)}" if !is_nil(@max) && @max < size, do: raise "items in :default is more than the given :max_size #{Integer.to_string(@max)}" any_kv_invalid? = Enum.any?(@default, fn {k, v} -> R.error?(@key_module.validate(k)) or R.error?(@value_module.validate(v)) end) if any_kv_invalid?, do: raise ":default must be a valid value of #{Atom.to_string(__MODULE__)}" defun default :: t, do: @default end end end end defmodule Croma.SubtypeOfTuple do @moduledoc """ Helper module to define tuple-based types. The following members are generated by `use Croma.SubtypeOfTuple`: - `@type t` - `@spec validate(term) :: Croma.Result.t(t)` Options: - `:elem_modules` - A list of modules each of which represents type of each element. - `:default` - Default value for this type. Passing this option generates `default/0`. ## Examples defmodule MyTuple do use Croma.SubtypeOfTuple, elem_modules: [MyInt, MyString] end """ defmacro __using__(opts) do quote bind_quoted: [elem_modules: opts[:elem_modules], default: opts[:default]] do @elem_modules elem_modules if !is_list(@elem_modules), do: raise ":elem_modules must be a list" @size length(@elem_modules) @type t :: {unquote_splicing(Enum.map(@elem_modules, fn m -> (quote do: unquote(m).t) end))} defun validate(term :: any) :: R.t(t) do t when is_tuple(t) and tuple_size(t) == @size -> result = Enum.zip(Tuple.to_list(t), @elem_modules) |> Enum.map(fn {elem, mod} -> mod.validate(elem) end) |> R.sequence case result do {:ok , l } -> {:ok, List.to_tuple(l)} {:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)} end _ -> {:error, {:invalid_value, [__MODULE__]}} end if default do @default default if !is_tuple(@default), do: raise ":default must be a tuple" if tuple_size(@default) != @size, do: raise "tuple size of :default is different from the length of :elem_modules" any_elem_invalid? = Enum.zip(Tuple.to_list(@default), @elem_modules) |> Enum.any?(fn {elem, mod} -> R.error?(mod.validate(elem)) end) if any_elem_invalid?, do: raise ":default must be a valid value of #{Atom.to_string(__MODULE__)}" defun default :: t, do: @default end end end end