Packages
croma
0.2.2
0.13.0
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir macro utilities to make type-based programming easier
Current section
Files
Jump to
Current section
Files
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