defmodule TypedStruct do @external_resource "README.md" @moduledoc "README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1) @accumulating_attrs [ :ts_plugins, :ts_plugin_fields, :ts_fields, :ts_types, :ts_enforce_keys ] @attrs_to_delete [:ts_enforce? | @accumulating_attrs] @doc false defmacro __using__(_) do quote do import TypedStruct, only: [typedstruct: 1, typedstruct: 2] end end @doc """ Defines a typed struct. Inside a `typedstruct` block, each field is defined through the `field/2` macro. ## Options * `enforce` - if set to true, sets `enforce: true` to all fields by default. This can be overridden by setting `enforce: false` or a default value on individual fields. * `opaque` - if set to true, creates an opaque type for the struct. * `module` - if set, creates the struct in a submodule named `module`. ## Examples defmodule MyStruct do use TypedStruct typedstruct do field :field_one, String.t() field :field_two, integer(), enforce: true field :field_three, boolean(), enforce: true field :field_four, atom(), default: :hey end end The following is an equivalent using the *enforce by default* behaviour: defmodule MyStruct do use TypedStruct typedstruct enforce: true do field :field_one, String.t(), enforce: false field :field_two, integer() field :field_three, boolean() field :field_four, atom(), default: :hey end end You can create the struct in a submodule instead: defmodule MyModule do use TypedStruct typedstruct module: Struct do field :field_one, String.t() field :field_two, integer(), enforce: true field :field_three, boolean(), enforce: true field :field_four, atom(), default: :hey end end """ defmacro typedstruct(opts \\ [], do: block) do ast = TypedStruct.__typedstruct__(block, opts) case opts[:module] do nil -> quote do # Create a lexical scope. (fn -> unquote(ast) end).() end module -> quote do defmodule unquote(module) do unquote(ast) end end end end @doc false def __typedstruct__(block, opts) do quote do Enum.each(unquote(@accumulating_attrs), fn attr -> Module.register_attribute(__MODULE__, attr, accumulate: true) end) Module.put_attribute(__MODULE__, :ts_enforce?, unquote(!!opts[:enforce])) @before_compile {unquote(__MODULE__), :__plugin_callbacks__} import TypedStruct unquote(block) @enforce_keys @ts_enforce_keys defstruct @ts_fields TypedStruct.__type__(@ts_types, unquote(opts)) end end @doc false defmacro __type__(types, opts) do if Keyword.get(opts, :opaque, false) do quote bind_quoted: [types: types] do @opaque t() :: %__MODULE__{unquote_splicing(types)} end else quote bind_quoted: [types: types] do @type t() :: %__MODULE__{unquote_splicing(types)} end end end @doc """ Registers a plugin for the currently defined struct. ## Example typedstruct do plugin MyPlugin field :a_field, String.t() end For more information on how to define your own plugins, please see `TypedStruct.Plugin`. To use a third-party plugin, please refer directly to its documentation. """ defmacro plugin(plugin, opts \\ []) do quote do Module.put_attribute( __MODULE__, :ts_plugins, {unquote(plugin), unquote(opts)} ) require unquote(plugin) unquote(plugin).init(unquote(opts)) end end @doc """ Defines a field in a typed struct. ## Example # A field named :example of type String.t() field :example, String.t() ## Options * `default` - sets the default value for the field * `enforce` - if set to true, enforces the field and makes its type non-nullable """ defmacro field(name, type, opts \\ []) do quote bind_quoted: [name: name, type: Macro.escape(type), opts: opts] do TypedStruct.__field__(name, type, opts, __ENV__) end end @doc false def __field__(name, type, opts, %Macro.Env{module: mod} = env) when is_atom(name) do if mod |> Module.get_attribute(:ts_fields) |> Keyword.has_key?(name) do raise ArgumentError, "the field #{inspect(name)} is already set" end has_default? = Keyword.has_key?(opts, :default) enforce_by_default? = Module.get_attribute(mod, :ts_enforce?) enforce? = if is_nil(opts[:enforce]), do: enforce_by_default? && !has_default?, else: !!opts[:enforce] nullable? = !has_default? && !enforce? Module.put_attribute(mod, :ts_fields, {name, opts[:default]}) Module.put_attribute(mod, :ts_plugin_fields, {name, type, opts, env}) Module.put_attribute(mod, :ts_types, {name, type_for(type, nullable?)}) if enforce?, do: Module.put_attribute(mod, :ts_enforce_keys, name) end def __field__(name, _type, _opts, _env) do raise ArgumentError, "a field name must be an atom, got #{inspect(name)}" end # Makes the type nullable if the key is not enforced. defp type_for(type, false), do: type defp type_for(type, _), do: quote(do: unquote(type) | nil) @doc false defmacro __plugin_callbacks__(%Macro.Env{module: module}) do plugins = Module.get_attribute(module, :ts_plugins) fields = Module.get_attribute(module, :ts_plugin_fields) |> Enum.reverse() Enum.each(unquote(@attrs_to_delete), &Module.delete_attribute(module, &1)) fields_block = for {plugin, plugin_opts} <- plugins, {name, type, field_opts, env} <- fields do plugin.field(name, type, field_opts ++ plugin_opts, env) end after_definition_block = for {plugin, plugin_opts} <- plugins do plugin.after_definition(plugin_opts) end quote do unquote_splicing(fields_block) unquote_splicing(after_definition_block) end end end