Packages
croma
0.7.3
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/type_gen.ex
import Croma.Defun
alias Croma.Result, as: R
defmodule Croma.TypeGen do
@moduledoc """
Module that defines macros for ad-hoc (in other words "in-line") module definitions.
"""
@doc """
Creates a new module that represents a nilable type, based on the given type module `module`.
Using the given type module `nilable/1` generates a new module that defines:
- `@type t :: nil | module.t`
- `@spec valid?(term) :: boolean`
- (If the given module exports `new/1`) `@spec new(term) :: Croma.Result.t(t)`
This is useful in defining a struct with nilable fields using `Croma.Struct`.
## Examples
iex> use Croma
...> defmodule I do
...> use Croma.SubtypeOfInt, min: 0
...> end
...> defmodule S do
...> use Croma.Struct, fields: [not_nilable_int: I, nilable_int: Croma.TypeGen.nilable(I)]
...> end
...> S.new(%{not_nilable_int: 0, nilable_int: nil})
%S{nilable_int: nil, not_nilable_int: 0}
"""
defmacro nilable(mod) do
nilable_impl(Macro.expand(mod, __CALLER__), Macro.Env.location(__CALLER__))
end
defp nilable_impl(mod, location) do
module_body = Macro.escape(nilable_module_body(mod))
quote bind_quoted: [mod: mod, module_body: module_body, location: location] do
name = Module.concat(Croma.TypeGen.Nilable, mod)
Croma.TypeGen.ensure_module_defined(name, module_body, location)
name
end
end
defp nilable_module_body(mod) do
quote bind_quoted: [mod: mod] do
@moduledoc false
@mod mod
@type t :: nil | unquote(@mod).t
defun valid?(value :: term) :: boolean do
nil -> true
v -> Croma.Validation.call_valid1(@mod, v)
end
defun validate(value :: term) :: R.t(t) do
nil -> {:ok, nil}
v -> Croma.Validation.call_validate1(@mod, v) |> R.map_error(&R.ErrorReason.add_context(&1, __MODULE__))
end
# Invoking `module_info/0` on `mod` automatically compiles and loads the module if necessary.
if {:new, 1} in @mod.module_info[:exports] do
defun new(value :: term) :: R.t(t) do
nil -> {:ok, nil}
v ->
case @mod.new(v) do
{:ok , _ } = r -> r
{:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)}
end
end
end
defun default() :: t, do: nil
end
end
@doc """
An ad-hoc version of `Croma.SubtypeOfList`.
Options for `Croma.SubtypeOfList` are not available in `list_of/1`.
Usage of `list_of/1` macro is the same as `nilable/1`.
"""
defmacro list_of(mod) do
list_of_impl(Macro.expand(mod, __CALLER__), Macro.Env.location(__CALLER__))
end
defp list_of_impl(mod, location) do
module_body = Macro.escape(list_of_module_body(mod))
quote bind_quoted: [mod: mod, module_body: module_body, location: location] do
name = Module.concat(Croma.TypeGen.ListOf, mod)
Croma.TypeGen.ensure_module_defined(name, module_body, location)
name
end
end
defp list_of_module_body(mod) do
quote bind_quoted: [mod: mod] do
@moduledoc false
@mod mod
@type t :: [unquote(@mod).t]
defun valid?(list :: term) :: boolean do
l when is_list(l) -> Enum.all?(l, &Croma.Validation.call_valid1(@mod, &1))
_ -> false
end
defun validate(list :: term) :: R.t(t) do
l when is_list(l) -> Enum.map(l, &Croma.Validation.call_validate1(@mod, &1)) |> R.sequence()
_ -> {:error, {:invalid_value, [__MODULE__]}}
end
# Invoking `module_info/0` on `mod` automatically compiles and loads the module if necessary.
if {:new, 1} in @mod.module_info[:exports] do
defun new(list :: term) :: R.t(t) do
l when is_list(l) -> Enum.map(l, &@mod.new/1) |> R.sequence()
_ -> {:error, {:invalid_value, [__MODULE__]}}
end
end
defun default() :: t, do: []
end
end
@doc """
Creates a new module that represents a sum type of the given types.
The argument must be a list of modules each of which defines `@type t` and `@spec validate(term) :: Croma.Result.t(t)`.
"""
defmacro union(modules) do
ms = Enum.map(modules, fn m -> Macro.expand(m, __CALLER__) end)
if Enum.empty?(ms), do: raise "Empty union is not allowed"
union_impl(ms, Macro.Env.location(__CALLER__))
end
defp union_impl(modules, location) do
module_body = Macro.escape(union_module_body(modules))
quote bind_quoted: [modules: modules, module_body: module_body, location: location] do
hash = Enum.map(modules, &Atom.to_string/1) |> :erlang.md5() |> Base.encode16()
name = Module.concat(Croma.TypeGen.Union, hash)
Croma.TypeGen.ensure_module_defined(name, module_body, location)
name
end
end
defp union_module_body(modules) do
quote bind_quoted: [modules: modules] do
@moduledoc false
@modules modules
@type t :: unquote(Enum.map(@modules, fn m -> quote do: unquote(m).t end) |> Croma.TypeUtil.list_to_type_union())
defun valid?(value :: term) :: boolean do
Enum.any?(@modules, fn mod -> Croma.Validation.call_valid1(mod, value) end)
end
defun validate(value :: term) :: R.t(t) do
error_result = {:error, {:invalid_value, [__MODULE__]}}
Enum.find_value(@modules, error_result, fn mod ->
case Croma.Validation.call_validate1(mod, value) do
{:ok , _} = r -> r
{:error, _} -> nil
end
end)
end
end
end
@doc """
Creates a new module that simply represents a type whose sole member is the given value.
Only atoms and integers are supported.
"""
defmacro fixed(value) do
fixed_impl(value, Macro.Env.location(__CALLER__))
end
defp fixed_impl(value, location) when is_atom(value) or is_integer(value) do
module_body = Macro.escape(fixed_module_body(value))
quote bind_quoted: [value: value, module_body: module_body, location: location] do
hash = :erlang.term_to_binary(value) |> :erlang.md5() |> Base.encode16()
name = Module.concat(Croma.TypeGen.Fixed, hash)
Croma.TypeGen.ensure_module_defined(name, module_body, location)
name
end
end
defp fixed_module_body(value) do
quote bind_quoted: [value: value] do
@moduledoc false
@value value
@type t :: unquote(@value)
defun valid?(v :: term) :: boolean do
v == @value
end
defun validate(v :: term) :: R.t(t) do
if v == @value do
{:ok, @value}
else
{:error, {:invalid_value, [__MODULE__]}}
end
end
defun default() :: t, do: @value
end
end
@doc false
def ensure_module_defined(name, quoted_expr, location) do
# Skip creating module if its beam file is already generated by previous compilation
if :code.which(name) == :non_existing do
# Use processes' registered names (just because it's easy) to remember whether already defined or not
# (Using `module_info/0` leads to try-rescue, which results in compilation error:
# see https://github.com/elixir-lang/elixir/issues/4055)
case Agent.start(fn -> nil end, [name: name]) do
{:ok , _pid } -> Module.create(name, quoted_expr, location)
{:error, _already_defined} -> nil
end
end
end
@doc false
def define_nilable_and_list_of(mod) do
location = Macro.Env.location(__ENV__)
q1 = nilable_impl(mod, location)
q2 = list_of_impl(mod, location)
Code.eval_quoted(q1, [], __ENV__)
Code.eval_quoted(q2, [], __ENV__)
end
end
# Predefine some type modules to avoid warnings when generated by multiple mix projects
defmodule Croma.PredefineVariantsOfBuiltinTypes do
@moduledoc false
Croma.BuiltinType.all() |> Enum.each(&Croma.TypeGen.define_nilable_and_list_of/1)
end