Packages
croma
0.6.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 module definitions.
`Croma` leverages Elixir's lightweight module syntax and advocate coding styles to define many modules.
Macros in this module helps defining modules in an ad-hoc way (in other words "in-line") based on existing modules.
"""
@doc """
Creates a new module that represents a nilable type, based on the given module `module`.
The module passed to `nilable/1` must define the following members:
- `@type t`
- `@spec validate(term) :: Croma.Result.t(t)`
Using the above members `nilable/1` generates a new module that also defines the same members:
- `@type t :: nil | module.t`
- `@spec validate(term) :: Croma.Result.t(t)`
## Examples
iex> use Croma
...> defmodule I do
...> use Croma.SubtypeOfInt, min: 0
...> end
This is useful in defining a struct with nilable fields using `Croma.Struct`.
...> 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
q = quote do
@moduledoc false
@type t :: nil | unquote(mod).t
defun validate(value :: term) :: R.t(t) do
nil -> {:ok, nil}
v -> case unquote(mod).validate(v) do
{:ok , _ } = r -> r
{:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)}
end
end
defun default() :: t, do: nil
end
name = Module.concat(Croma.TypeGen.Nilable, mod)
ensure_module_defined(name, q, location)
name
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
q = quote do
@moduledoc false
@type t :: [unquote(mod).t]
defun validate(list :: term) :: R.t(t) do
l when is_list(l) ->
Enum.map(l, &unquote(mod).validate/1) |> R.sequence
_ -> {:error, {:invalid_value, [__MODULE__]}}
end
defun default() :: t, do: []
end
name = Module.concat(Croma.TypeGen.ListOf, mod)
ensure_module_defined(name, q, location)
name
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
type = Enum.map(modules, fn m -> quote do: unquote(m).t end) |> Croma.TypeUtil.list_to_type_union
q = quote do
@moduledoc false
@modules unquote(modules)
@type t :: unquote(type)
defun validate(value :: term) :: R.t(t) do
error_result = {:error, {:invalid_value, [__MODULE__]}}
Enum.find_value(@modules, error_result, fn mod ->
case mod.validate(value) do
{:ok , _} = r -> r
{:error, _} -> nil
end
end)
end
end
hash = Enum.map(modules, &Atom.to_string/1) |> :erlang.md5 |> Base.encode16
name = Module.concat(Croma.TypeGen.Union, hash)
ensure_module_defined(name, q, location)
name
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
q = quote do
@moduledoc false
@type t :: unquote(value)
defun validate(v :: term) :: R.t(t) do
if v == unquote(value) do
{:ok, unquote(value)}
else
{:error, {:invalid_value, [__MODULE__]}}
end
end
defun default() :: t, do: unquote(value)
end
hash = :erlang.term_to_binary(value) |> :erlang.md5 |> Base.encode16
name = Module.concat(Croma.TypeGen.Fixed, hash)
ensure_module_defined(name, q, location)
name
end
defp 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__)
nilable_impl(mod, location)
list_of_impl(mod, location)
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