Packages

Reduce boilerplate by generating struct `new` and `put` functions. and validate your structs when they are created and updated.

Current section

Files

Jump to
ex_structable lib ex_structable.ex
Raw

lib/ex_structable.ex

defmodule ExStructable do
@moduledoc """
The `use`-able module.
Example usage:
```elixir
defmodule Point do
@enforce_keys [:x, :y]
defstruct [:x, :y, :z]
use ExStructable # Adds `new` and `put` dynamically
# Optional hook
def validate_struct(struct) do
if struct.x < 0 or struct.y < 0 or struct.z < 0 do
raise ArgumentError
end
struct
end
end
Point.new(x: 1, y: 2)
# => %Point{x: 1, y: 2, z: nil}
Point.new(x: -1, y: 2)
# ArgumentError: Fails validation, as expected
```
See [README](https://github.com/dylan-chong/ex_structable) for more
instructions.
"""
# TODO customisable new/put names
# TODO publish in hex
@doc false
def ex_constructor_new_name, do: :__new__
@doc false
def call_hook(caller_module, method, method_args) do
caller_functions = caller_module.__info__(:functions)
module = if Keyword.has_key?(caller_functions, method) do
caller_module
else
ExStructable.DefaultHooks
end
apply(module, method, method_args)
end
@doc false
def ex_constructor_lib_args(options) do
use_option = Keyword.fetch!(options, :use_ex_constructor_library)
if use_option do
default_options = [name: ex_constructor_new_name()]
if is_list(use_option) do
Keyword.merge(default_options, use_option)
else
default_options
end
else
nil
end
end
defmacro __using__(options) do
options = Keyword.merge([
# call validate_struct callback?
validate_struct: true,
# use library https://github.com/appcues/exconstructor
use_ex_constructor_library: false, # boolean, or kw list
], options)
lib_args = ex_constructor_lib_args(options)
quote do
if unquote(lib_args) do
use ExConstructor, unquote(lib_args)
end
def new(args, override_options \\ []) when is_list(override_options) do
merged_options =
Keyword.merge(unquote(options), override_options)
opt = &Keyword.fetch!(merged_options, &1)
call_hook = &ExStructable.call_hook(__MODULE__, &1, &2)
struct = call_hook.(:create_struct, [
args, __MODULE__, merged_options
])
result = if opt.(:validate_struct) do
validated_struct = call_hook.(:validate_struct, [
struct, merged_options
])
if validated_struct == nil do
# To prevent accidental mistakes
raise ExStructable.InvalidHookError,
"validate_struct cannot return nil. "
<> "Return the struct instead (if validation passed)."
end
validated_struct
else
struct
end
call_hook.(
merged_options[:on_success] || :on_successful_new,
[result, merged_options]
)
result
end
def put(struct = %_{}, args, override_options \\ [])
when is_list(override_options) do
# TODO accept struct being a map or kw list? (create overload)
unless struct.__struct__ == __MODULE__ do
raise ArgumentError,
"#{inspect(struct)} struct is not a %#{__MODULE__}{}"
end
struct
|> Map.from_struct
|> Keyword.new
|> Keyword.merge(args)
|> new(Keyword.put(
override_options,
:on_success,
:on_successful_put
))
end
end
end
end