Packages
croma
0.4.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 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