Packages

Declarative Ecto embedded schemas for data validation, coercion, and manipulation.

Current section

12 Versions

Jump to

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(),