Current section

Files

Jump to
croma lib croma subtype.ex
Raw

lib/croma/subtype.ex

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
@default 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 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
@default 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 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
@default 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 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
@default default
if @default do
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 :: [@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
@default default
if @default do
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 :: %{@key_module.t => @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
@default default
if @default do
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