defmodule Dry do @moduledoc """ Allow for creating structs, with fields of certain type, default value, custom function and more. ## Example: ```elixir defmodule Sibling do use Dry alias Dry.Types schema do attribute(:name, Types.String) end end defmodule User do use Dry alias Dry.Types schema do attribute(:name, Types.String) attribute(:age, Types.Integer.options(optional: true)) attribute(:height) attribute(:country, Types.String.options(default: "UK")) attribute(:siblings, Types.Array.options(type: Sibling)) attribute(:favourite_colours, Types.Array.options(type: Types.String, default: ["blue", "green"])) attribute :is_adult do Map.get(entity, :age, 0) >= 18 end attribute :tall do Map.get(entity, :height, 0) >= 180 end end end user = User.new!(%{name: "Rob", age: 18, height: 169, country: "BG", siblings: [%{name: "John"}]}) user == %User{ age: 18, country: "BG", height: 169, is_adult: true, name: "Rob", tall: false, siblings: [%Sibling{name: "John"}], favourite_colours: ["blue", "green"] } {:ok, _user} = User.new(%{name: "Rob", age: 18, height: 169, country: "BG", siblings: [%{name: "John"}]}) ``` """ defmacro __using__(_) do quote do import Dry, only: [schema: 1] Module.register_attribute(__MODULE__, :attributes, accumulate: true) end end defmacro schema(do: block) do prelude = quote do try do import Dry unquote(block) after :ok end end postlude = quote unquote: false do @attributes |> Enum.map(fn attr -> Enum.at(attr, 0) end) |> Kernel.defstruct() def __attributes__(), do: @attributes def new!(attr) do attr = __attributes__() |> Enum.reverse() |> Enum.reject(&Dry.Processor.function?/1) |> Enum.map(fn a -> Dry.Processor.process(a, attr, __MODULE__) end) |> Enum.into(%{}) processed = __attributes__() |> Enum.reverse() |> Enum.filter(&Dry.Processor.function?/1) |> Enum.map(fn a -> Dry.Processor.process(a, attr, __MODULE__) end) |> Enum.into(attr) struct(__MODULE__, processed) end def new(attr) do try do {:ok, new!(attr)} rescue e in Dry.RuntimeError -> {:error, e.message} end end def valid?(%{__struct__: __MODULE__} = value) when is_struct(value), do: true def valid?(_value), do: false @doc "Used to recognise Dry modules" def __dry__(), do: true end quote do unquote(prelude) unquote(postlude) end end defmacro attribute(name) do quote do attribute(unquote(name), nil) end end defmacro attribute(name, do: block) do quote do attribute = [unquote(name), :__func__] def unquote(name)(e) do Kernel.var!(entity) = e unquote(block) end Module.put_attribute(__MODULE__, :attributes, attribute) end end defmacro attribute(name, type) do quote do Module.put_attribute(__MODULE__, :attributes, [unquote(name), unquote(type)]) end end @doc """ Macro to define attribute withing a Dry schema Examples: ``` attribute(:age, :integer, optional: true) ``` Valid types can be - :atom, :string, :integer, :float, :bool, :map, :atom or a struct """ defmacro attribute(name, type, opts) do quote do attribute = [unquote(name), unquote(type), unquote(opts)] Module.put_attribute(__MODULE__, :attributes, attribute) end end end