defmodule Flint.Type do @moduledoc """ `Flint.Type` is meant to make writing new `Ecto` types require much less boilerplate, because you can base your type off of an existing type and only modify the callbacks that have different behavior. Simply `use Flint.Type` and pass the `:extends` option which says which type module to inherit callbacks from. This will delegate all required callbacks and any implemented optional callbacks and make them overridable. It also lets you make a type from an `Ecto.ParameterizedType` with default parameter values. You may supply any number of default parameters. This essentially provides a new `init/1` implementation for the type, supplying the default values, while not affecting any of the other `Ecto.ParameterizedType` callbacks. You may still override the newly set defaults at the local level. Just supply all options that you wish to be defaults as extra options when using `Flint.Type`. You may override any of the inherited callbacks inherity from the extended module in the case that you wish to customize the module further. ## Examples ```elixir defmodule Category do use Flint.Type, extends: Ecto.Enum, values: [:folder, :file] end ``` This will apply default `values` to `Ecto.Enum` when you supply a `Category` type to an Ecto schema. You may still override the values if you supply the `:values` option for the field. ```elixir import Flint.Type deftype NewUID, extends: Ecto.UUID, dump: &String.length/1 ``` This will create a new `NewUID` type that behaves exactly like an `Ecto.UUID` except it dumps its string length. """ require Logger defmacro __using__(opts) do {extends, opts} = Keyword.pop!(opts, :extends) extends = Macro.expand_literals(extends, __CALLER__) unless Ecto.Type.base?(extends) || Ecto.Type.composite?(extends) || Code.ensure_loaded?(extends) do raise ArgumentError, "Cannot load module #{inspect(extends)}!" end type = cond do Ecto.Type.base?(extends) -> extends Ecto.Type.composite?(extends) -> extends implements_behaviour?(extends, Ecto.Type) -> Ecto.Type implements_behaviour?(extends, Ecto.ParameterizedType) -> Ecto.ParameterizedType true -> raise ArgumentError, "You must extend from either a base type, a composite type, an `Ecto.Type`, or an `Ecto.ParameterizedType`!" end {impls, opts} = Keyword.split( opts, Keyword.keys(module_for_type(type).behaviour_info(:callbacks)) ) if type != Ecto.ParameterizedType && opts != [] do Logger.warning(""" You are passing options #{inspect(opts)} to type `#{inspect(extends)}` `#{inspect(extends)}` does not accept paremters, so these options will be ignored. Only `Ecto.ParameterizedType` accepts parameters. """) end required = required_callbacks(module_for_type(type)) callbacks = module_for_type(type).behaviour_info(:callbacks) |> Keyword.drop([:init]) |> Enum.map(fn {name, arity} -> cond do Keyword.has_key?(impls, name) -> args = Macro.generate_arguments(arity, nil) option = Keyword.fetch!(impls, name) quote do if is_function(unquote(option), unquote(arity)) do def unquote(name)(unquote_splicing(args)), do: unquote(option).(unquote_splicing(args)) else raise ArgumentError, "Function and arity mismatch for callback #{Exception.format_mfa(unquote(type), unquote(name), unquote(arity))} in module #{inspect(__MODULE__)}." end end Keyword.has_key?(required, name) || function_exported?(extends, name, arity) -> args = Macro.generate_arguments(arity, nil) if type in [Ecto.Type, Ecto.ParameterizedType] do quote do def unquote(name)(unquote_splicing(args)) do unquote(extends).unquote(name)(unquote_splicing(args)) end defoverridable [{unquote(name), unquote(arity)}] end else quote do def unquote(name)(unquote_splicing(args)) do Ecto.Type.unquote(name)(unquote(extends), unquote_splicing(args)) end defoverridable [{unquote(name), unquote(arity)}] end end true -> nil end end) using = quote(do: use(unquote(module_for_type(type)))) reflection = quote do def base_type, do: unquote(extends) end init = if type == Ecto.ParameterizedType do quote do def init(opts) do opts = opts ++ unquote(opts) unquote(extends).init(opts) |> Map.put_new(:extends, unquote(extends)) end end end quoted = ([ using, reflection, init ] ++ callbacks) |> Enum.filter(& &1) quote do (unquote_splicing(quoted)) end end @doc """ A shorthand for creating a new `Flint.Type`. This is equivalent to creating a new module with name `module` and calling `use Flint.Type` and passing `opts`. ## Example ```elixir deftype Vehicle, extends: Ecto.Enum, values: [:car, :motorcycle, :truck] ``` """ defmacro deftype(module, opts) do module = Macro.expand_literals(module, __CALLER__) quote do defmodule unquote(module) do use Flint.Type, unquote(opts) end end end defp required_callbacks(module) do all_callbacks = module.behaviour_info(:callbacks) optional_callbacks = module.behaviour_info(:optional_callbacks) Keyword.drop(all_callbacks, Keyword.keys(optional_callbacks)) end defp implements_behaviour?(module, behaviour) do Enum.all?(required_callbacks(behaviour), fn {k, v} -> function_exported?(module, k, v) end) end defp module_for_type(Ecto.ParameterizedType), do: Ecto.ParameterizedType defp module_for_type(_other), do: Ecto.Type end