View Source TypedStructor.Plugin behaviour (TypedStructor v0.5.0)
This module defines the plugin behaviour for TypedStructor.
Plugin Behaviour
A plugin is a module that implements the TypedStructor.Plugin behaviour.
Three macro callbacks are available for injecting code at different stages:
init/1: This macro callback is called when the plugin is used.before_definition/2: This macro callback is called right before defining the struct. Note hat plugins will run in the order they are registered.after_definition/2: This macro callback is called right after defining the struct. Note that plugins will run in the reverse order they are registered.
Plugins guides
Here are some Plugin Guides for creating your own plugins. Please check them out and feel free to copy-paste the code.
Example
Let's define a plugin that defines Ecto.Schema while defining a typed struct.
This plugin takes a :source option which passing to Ecto.Schema.schema/2,
you can use belongs_to and has_many directly in the module.
It would be used like this:
defmodule MyApp do
  use TypedStructor
  # fix aliases
  alias __MODULE__.User
  alias __MODULE__.Post
  typed_structor module: User do
    # import the plugin with source option
    plugin EctoSchemaPlugin, source: "users"
    field :name, :string, enforce: true
    field :age, :integer, default: 0
    # pass redact option to Ecto.Schema.field/3
    field :password, :string, redact: true
    has_many :posts, Post
  end
  typed_structor module: Post do
    # import the plugin with source option
    plugin EctoSchemaPlugin, source: "posts"
    field :title, :string, enforce: true
    field :content, :string, enforce: true
    belongs_to :user, User, enforce: true
  end
endAfter compiled, you got:
iex> t MyApp.User
@type t() :: %MyApp.User{
        __meta__: Ecto.Schema.Metadata.t(),
        age: integer() | nil,
        id: integer(),
        name: String.t(),
        password: String.t() | nil,
        posts: [MyApp.Post.t()] | nil
      }
iex> t MyApp.Post
@type t() :: %MyApp.Post{
        __meta__: Ecto.Schema.Metadata.t(),
        content: String.t(),
        id: integer(),
        title: String.t(),
        user: MyApp.User.t(),
        user_id: integer()
      }
iex> MyApp.User.__schema__(:redact_fields)
[:password]
iex> MyApp.User.__schema__(:association, :posts)
%Ecto.Association.Has{
  cardinality: :many,
  field: :posts,
  owner: MyApp.User,
  related: MyApp.Post,
  owner_key: :id,
  related_key: :user_id,
  on_cast: nil,
  queryable: MyApp.Post,
  on_delete: :nothing,
  on_replace: :raise,
  where: [],
  unique: true,
  defaults: [],
  relationship: :child,
  ordered: false,
  preload_order: []
}
iex> MyApp.Post.__schema__(:association, :user)
%Ecto.Association.BelongsTo{
  field: :user,
  owner: MyApp.Post,
  related: MyApp.User,
  owner_key: :user_id,
  related_key: :id,
  queryable: MyApp.User,
  on_cast: nil,
  on_replace: :raise,
  where: [],
  defaults: [],
  cardinality: :one,
  relationship: :parent,
  unique: true,
  ordered: false
}Following is the implementation of the plugin:
defmodule EctoSchemaPlugin do
  use TypedStructor.Plugin
  @impl TypedStructor.Plugin
  defmacro init(opts) do
    quote do
      unless Keyword.has_key?(unquote(opts), :source) do
        raise "The `:source` option is not provided."
      end
      # import association functions to the module,
      # so that we can use `has_many` and `belongs_to` directly
      import unquote(__MODULE__), only: [has_many: 2, belongs_to: 3]
    end
  end
  @impl TypedStructor.Plugin
  defmacro before_definition(definition, _opts) do
    # manipulate the definition before defining the struct
    quote do
      unquote(definition)
      # disable defining struct, for Ecto.Schema will define it
      |> Map.update!(:options, &Keyword.put(&1, :define_struct, false))
      |> Map.update!(:fields, fn fields ->
        Enum.flat_map(fields, fn field ->
          {ecto_type, options} = Keyword.pop!(field, :type)
          type = unquote(__MODULE__).__ecto_type_to_type__(ecto_type)
          field = Keyword.merge(options, type: type, ecto_type: ecto_type)
          case ecto_type do
            {:belongs_to, name} ->
              foreign_key_name =
                name
                |> Macro.expand(__ENV__)
                |> Module.split()
                |> List.last()
                |> Macro.underscore()
                |> Kernel.<>("_id")
                |> String.to_atom()
              foreign_key =
                Keyword.merge(options, name: foreign_key_name, type: quote(do: integer()))
              [foreign_key, field]
            _other ->
              [field]
          end
        end)
      end)
      |> Map.update!(
        :fields,
        &[
          [name: :__meta__, type: quote(do: Ecto.Schema.Metadata.t()), enforce: true],
          [name: :id, type: quote(do: integer()), enforce: true]
          | &1
        ]
      )
    end
  end
  @impl TypedStructor.Plugin
  defmacro after_definition(definition, opts) do
    # here we define the Ecto.Schema
    quote bind_quoted: [definition: definition, opts: opts] do
      use Ecto.Schema
      source = Keyword.fetch!(opts, :source)
      schema source do
        for options <- definition.fields do
          {name, options} = Keyword.pop!(options, :name)
          {ecto_type, options} = Keyword.pop(options, :ecto_type)
          options = Keyword.take(options, [:primary_key, :default, :redact])
          case ecto_type do
            nil ->
              # skip some fields
              nil
            {:has_many, module} ->
              module = Macro.expand(module, __ENV__)
              has_many name, module, options
            {:belongs_to, module} ->
              module = Macro.expand(module, __ENV__)
              belongs_to name, module, options
            _ ->
              field name, ecto_type, options
          end
        end
      end
    end
  end
  defmacro has_many(name, queryable) do
    quote do
      field unquote(name), {:has_many, unquote(queryable)}
    end
  end
  defmacro belongs_to(name, queryable, opts) do
    quote do
      field unquote(name), {:belongs_to, unquote(queryable)}, unquote(opts)
    end
  end
  def __ecto_type_to_type__(:string), do: quote(do: String.t())
  def __ecto_type_to_type__(:integer), do: quote(do: integer())
  def __ecto_type_to_type__({:has_many, module}), do: quote(do: [unquote(module).t()])
  def __ecto_type_to_type__({:belongs_to, module}), do: quote(do: unquote(module).t())
end
    Summary
Callbacks
This macro callback is called right after defining the struct.
This macro callback is called right before defining the struct.
This macro callback is called when the plugin is used.
Callbacks
@macrocallback after_definition( definition :: TypedStructor.Definition.t(), plugin_opts :: Keyword.t() ) :: Macro.t()
This macro callback is called right after defining the struct.
It receives the definition of the struct and the plugin options, and its return value is ignored.
@macrocallback before_definition( definition :: TypedStructor.Definition.t(), plugin_opts :: Keyword.t() ) :: Macro.t()
This macro callback is called right before defining the struct.
It receives the definition of the struct and the plugin options,
and it should return the TypedStructor.Definition struct or
a list which contains exactly one TypedStructor.Definition struct.
This macro callback is called when the plugin is used.
Here you can define module attributes, import modules, etc.