Current section
12 Versions
Jump to
Current section
12 Versions
Compare versions
3
files changed
+87
additions
-29
deletions
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/acalejos/flint">>}]}. |
| 2 2 | {<<"name">>,<<"flint">>}. |
| 3 | - {<<"version">>,<<"0.2.0">>}. |
| 3 | + {<<"version">>,<<"0.2.1">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<"Declarative Ecto embedded schemas for data validation, coercion, and manipulation.">>}. |
| 6 6 | {<<"elixir">>,<<"~> 1.14">>}. |
| @@ -1,13 +1,20 @@ | |
| 1 1 | defmodule Flint.Partial do |
| 2 2 | @moduledoc """ |
| 3 | - Can be `use`d to create an `Ecto.ParameterizedType` based on another `Ecto.ParameterizedType`, |
| 4 | - applying default parameters. You may supply any number of default parameters. This essentially provides a new |
| 3 | + `Flint.Partial` is meant to make writing new `Ecto` types require much less boilerplate, because you can base your |
| 4 | + type off of an existing type and only modify the callbacks that have different behavior. |
| 5 | + |
| 6 | + Simply `use Flint.Partial` and pass the `:extends` option which says which type module to inherit callbacks |
| 7 | + from. This will delegate all required callbacks and any implemented optional callbacks and make them |
| 8 | + overridable. |
| 9 | + |
| 10 | + It also lets you make a type from an `Ecto.ParameterizedType` with default parameter values. |
| 11 | + You may supply any number of default parameters. This essentially provides a new |
| 5 12 | `init/1` implementation for the type, supplying the default values, while not affecting any of the |
| 6 | - other `Ecto.ParameterizedType` callbacks. |
| 13 | + other `Ecto.ParameterizedType` callbacks. You may still override the newly set defaults at the local level. |
| 7 14 | |
| 8 | - You may still override the newly set defaults at the local level. |
| 15 | + Just supply all options that you wish to be defaults as extra options when using `Flint.Partial`. |
| 9 16 | |
| 10 | - You may override any of the inherited `Ecto.ParameterizedType` callbacks inherity from the extended module |
| 17 | + You may override any of the inherited callbacks inherity from the extended module |
| 11 18 | in the case that you wish to customize the module further. |
| 12 19 | |
| 13 20 | ## Examples |
| @@ -18,10 +25,12 @@ defmodule Flint.Partial do | |
| 18 25 | end |
| 19 26 | ``` |
| 20 27 | |
| 21 | - This will apply default `values` to `Ecto.Enum` when you supply a `Cateogory` type |
| 28 | + This will apply default `values` to `Ecto.Enum` when you supply a `Category` type |
| 22 29 | to an Ecto schema. You may still override the values if you supply the `:values` |
| 23 30 | option for the field. |
| 24 31 | """ |
| 32 | + require Logger |
| 33 | + |
| 25 34 | defmacro __using__(opts) do |
| 26 35 | {extends, opts} = Keyword.pop!(opts, :extends) |
| 27 36 | extends = Macro.expand_literals(extends, __CALLER__) |
| @@ -30,19 +39,42 @@ defmodule Flint.Partial do | |
| 30 39 | raise ArgumentError, "Cannot load module #{inspect(extends)}!" |
| 31 40 | end |
| 32 41 | |
| 42 | + type = |
| 43 | + cond do |
| 44 | + implements_behaviour?(extends, Ecto.Type) -> |
| 45 | + Ecto.Type |
| 46 | + |
| 47 | + implements_behaviour?(extends, Ecto.ParameterizedType) -> |
| 48 | + Ecto.ParameterizedType |
| 49 | + |
| 50 | + true -> |
| 51 | + raise ArgumentError, |
| 52 | + "You must extend from either a `Ecto.Type` or `Ecto.ParameterizedType`!" |
| 53 | + end |
| 54 | + |
| 55 | + if type == Ecto.Type && opts != [] do |
| 56 | + Logger.warning(""" |
| 57 | + You are passing options #{inspect(opts)} to `Flint.Partial` |
| 58 | + when extending a module of type `Ecto.Type`. `Ecto.Type` does not accept paremters, so these options will be ignored. |
| 59 | + """) |
| 60 | + end |
| 61 | + |
| 33 62 | callbacks = |
| 34 | - Enum.map( |
| 35 | - [ |
| 36 | - {:autogenerate, 1}, |
| 37 | - {:embed_as, 2}, |
| 38 | - {:format, 1}, |
| 39 | - {:type, 1}, |
| 40 | - {:cast, 2}, |
| 41 | - {:load, 3}, |
| 42 | - {:dump, 3}, |
| 43 | - {:equal?, 3} |
| 44 | - ], |
| 45 | - fn {name, arity} -> |
| 63 | + type |
| 64 | + |> required_callbacks() |
| 65 | + |> Keyword.drop([:init]) |
| 66 | + |> Enum.map(fn {name, arity} -> |
| 67 | + args = Macro.generate_arguments(arity, nil) |
| 68 | + |
| 69 | + quote do |
| 70 | + defdelegate unquote(name)(unquote_splicing(args)), to: unquote(extends) |
| 71 | + defoverridable [{unquote(name), unquote(arity)}] |
| 72 | + end |
| 73 | + end) |
| 74 | + |
| 75 | + callbacks = |
| 76 | + callbacks ++ |
| 77 | + Enum.map(apply(type, :behaviour_info, [:optional_callbacks]), fn {name, arity} -> |
| 46 78 | if function_exported?(extends, name, arity) do |
| 47 79 | args = Macro.generate_arguments(arity, nil) |
| 48 80 | |
| @@ -51,19 +83,30 @@ defmodule Flint.Partial do | |
| 51 83 | defoverridable [{unquote(name), unquote(arity)}] |
| 52 84 | end |
| 53 85 | end |
| 86 | + end) |
| 87 | + |
| 88 | + using = quote(do: use(unquote(type))) |
| 89 | + |
| 90 | + init = |
| 91 | + if type == Ecto.ParameterizedType do |
| 92 | + quote do |
| 93 | + def init(opts) do |
| 94 | + opts = opts ++ unquote(opts) |
| 95 | + unquote(extends).init(opts) |
| 96 | + end |
| 54 97 | end |
| 55 | - ) |
| 98 | + end |
| 99 | + |
| 100 | + quoted = |
| 101 | + ([ |
| 102 | + using, |
| 103 | + init |
| 104 | + ] ++ |
| 105 | + callbacks) |
| 56 106 | |> Enum.filter(& &1) |
| 57 107 | |
| 58 108 | quote do |
| 59 | - use Ecto.ParameterizedType |
| 60 | - |
| 61 | - def init(opts) do |
| 62 | - opts = opts ++ unquote(opts) |
| 63 | - unquote(extends).init(opts) |
| 64 | - end |
| 65 | - |
| 66 | - unquote_splicing(callbacks) |
| 109 | + (unquote_splicing(quoted)) |
| 67 110 | end |
| 68 111 | end |
| 69 112 | |
| @@ -71,6 +114,9 @@ defmodule Flint.Partial do | |
| 71 114 | A shorthand for creating a new module to represent a partial `Ecto.ParameterizedType`. This is equivalent to |
| 72 115 | creating a new module with name `module` and calling `use Flint.Partial` and passing `opts`. |
| 73 116 | |
| 117 | + Practially speaking, this is only really useful for defining default values for an `Ecto.ParameterizedType` with |
| 118 | + no changes to any of the other callbacks. |
| 119 | + |
| 74 120 | ## Example |
| 75 121 | |
| 76 122 | ```elixir |
| @@ -86,4 +132,16 @@ defmodule Flint.Partial do | |
| 86 132 | |
| 87 133 | Module.create(module, quoted, Macro.Env.location(__ENV__)) |
| 88 134 | end |
| 135 | + |
| 136 | + defp required_callbacks(module) do |
| 137 | + all_callbacks = apply(module, :behaviour_info, [:callbacks]) |
| 138 | + optional_callbacks = apply(module, :behaviour_info, [:optional_callbacks]) |
| 139 | + Keyword.drop(all_callbacks, Keyword.keys(optional_callbacks)) |
| 140 | + end |
| 141 | + |
| 142 | + defp implements_behaviour?(module, behaviour) do |
| 143 | + Enum.all?(required_callbacks(behaviour), fn {k, v} -> |
| 144 | + function_exported?(module, k, v) |
| 145 | + end) |
| 146 | + end |
| 89 147 | end |
| @@ -5,7 +5,7 @@ defmodule Flint.MixProject do | |
| 5 5 | [ |
| 6 6 | app: :flint, |
| 7 7 | name: "Flint", |
| 8 | - version: "0.2.0", |
| 8 | + version: "0.2.1", |
| 9 9 | elixir: "~> 1.14", |
| 10 10 | start_permanent: Mix.env() == :prod, |
| 11 11 | deps: deps(), |