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 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}
_ -> {: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(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}
_ -> {:error, {:invalid_value, [__MODULE__]}}
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}
_ -> {: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
@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, {:invalid_value, [__MODULE__]}}
end
_ -> {:error, {:invalid_value, [__MODULE__]}}
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, {:invalid_value, [__MODULE__]}}
end
s when is_binary(s) ->
if s in unquote(value_strings) do
{:ok, String.to_existing_atom(s)}
else
{:error, {:invalid_value, [__MODULE__]}}
end
_ -> {:error, {:invalid_value, [__MODULE__]}}
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) ->
case Enum.map(l, &unquote(mod).validate/1) |> R.sequence do
{:ok , elems } = r -> if valid_length?(length(elems)), do: r, else: {:error, {:invalid_value, [__MODULE__]}}
{:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)}
end
_ -> {:error, {:invalid_value, [__MODULE__]}}
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