-
-
Notifications
You must be signed in to change notification settings - Fork 152
/
schema.ex
238 lines (198 loc) · 7.18 KB
/
schema.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
defmodule Pow.Extension.Ecto.Schema do
@moduledoc """
Handles extensions for the user Ecto schema.
The macro will append fields to the `@pow_fields` module attribute using the
attributes from `[Pow Extension].Ecto.Schema.attrs/1`, so they can be used in
the `Pow.Ecto.Schema.pow_user_fields/0` function call.
After module compilation `[Pow Extension].Ecto.Schema.validate!/2` will run.
## Usage
Configure `lib/my_project/users/user.ex` the following way:
defmodule MyApp.Users.User do
use Ecto.Schema
use Pow.Ecto.Schema
use Pow.Extension.Ecto.Schema,
extensions: [PowExtensionOne, PowExtensionTwo]
schema "users" do
pow_user_fields()
timestamps()
end
def changeset(user_or_changeset, attrs) do
user
|> pow_changeset(attrs)
|> pow_extension_changeset(attrs)
end
end
"""
alias Ecto.Changeset
alias Pow.{Config, Extension, Extension.Base}
defmodule SchemaError do
@moduledoc false
defexception [:message]
end
@doc false
defmacro __using__(config) do
quote do
@pow_extension_config Config.merge(@pow_config, unquote(config))
Module.eval_quoted(__MODULE__, unquote(__MODULE__).__use_extensions__(@pow_extension_config))
unquote(__MODULE__).__register_extension_fields__()
unquote(__MODULE__).__register_extension_assocs__()
unquote(__MODULE__).__pow_extension_functions__()
unquote(__MODULE__).__register_after_compile_validation__()
end
end
@doc false
def __use_extensions__(config) do
config
|> schema_modules_with_use()
|> Enum.map(fn module ->
quote do
use unquote(module), unquote(config)
end
end)
end
@doc false
defmacro __register_extension_fields__ do
quote do
for {name, value, options, _migration_options} <- unquote(__MODULE__).attrs(@pow_extension_config) do
Module.put_attribute(__MODULE__, :pow_fields, {name, value, options})
end
end
end
@doc false
defmacro __register_extension_assocs__ do
quote do
@pow_extension_config
|> unquote(__MODULE__).assocs()
|> Enum.map(fn
{type, name, :users, field_options, _migration_options} -> {type, name, __MODULE__, field_options}
{type, name, module, field_options, _migration_options} -> {type, name, module, field_options}
end)
|> Enum.each(&Module.put_attribute(__MODULE__, :pow_assocs, &1))
end
end
@doc false
defmacro __pow_extension_functions__ do
quote do
def pow_extension_changeset(changeset, attrs) do
unquote(__MODULE__).changeset(changeset, attrs, @pow_extension_config)
end
end
end
@doc false
defmacro __register_after_compile_validation__ do
quote do
def pow_extension_validate_after_compilation!(env, _bytecode) do
unquote(__MODULE__).validate!(@pow_extension_config, __MODULE__)
end
@after_compile {__MODULE__, :pow_extension_validate_after_compilation!}
end
end
@doc """
Merge all extension attributes together to one list.
The extension ecto schema modules is discovered through the `:extensions` key
in the configuration, and the attribute list will be in the same order as the
extensions list.
"""
@spec attrs(Config.t()) :: [tuple]
def attrs(config) do
config
|> schema_modules()
|> Enum.reduce([], fn extension, attrs ->
extension_attrs = extension.attrs(config)
Enum.concat(attrs, extension_attrs)
end)
|> Enum.map(&normalize_attr/1)
end
defp normalize_attr({name, value}), do: {name, value, [], []}
defp normalize_attr({name, value, field_options}), do: {name, value, field_options, []}
defp normalize_attr({name, value, field_options, migration_options}), do: {name, value, field_options, migration_options}
@doc """
Merge all extension associations together to one list.
The extension ecto schema modules is discovered through the `:extensions` key
in the configuration, and the attribute list will be in the same order as the
extensions list.
"""
@spec assocs(Config.t()) :: [tuple]
def assocs(config) do
config
|> schema_modules()
|> Enum.reduce([], fn extension, assocs ->
extension_assocs = extension.assocs(config)
Enum.concat(assocs, extension_assocs)
end)
|> Enum.map(&normalize_assoc/1)
end
defp normalize_assoc({type, name, module}), do: {type, name, module, [], []}
defp normalize_assoc({type, name, module, field_options}), do: {type, name, module, field_options, []}
defp normalize_assoc({type, name, module, field_options, migration_options}), do: {type, name, module, field_options, migration_options}
@doc """
Merge all extension indexes together to one list.
The extension ecto schema modules is discovered through the `:extensions` key
in the configuration, and the index list will be in the same order as the
extensions list.
"""
@spec indexes(Config.t()) :: [tuple]
def indexes(config) do
config
|> schema_modules()
|> Enum.reduce([], fn extension, indexes ->
extension_indexes = extension.indexes(config)
Enum.concat(indexes, extension_indexes)
end)
end
@doc """
This will run `changeset/3` on all extension ecto schema modules.
The extension ecto schema modules is discovered through the `:extensions` key
in the configuration, and the changesets will be piped in the same order
as the extensions list.
"""
@spec changeset(Changeset.t(), map(), Config.t()) :: Changeset.t()
def changeset(changeset, attrs, config) do
config
|> schema_modules()
|> Enum.reduce(changeset, fn extension, changeset ->
extension.changeset(changeset, attrs, config)
end)
end
@doc """
This will run `validate!/2` on all extension ecto schema modules.
It's used to ensure certain fields are available, e.g. an `:email` field. The
function should either raise an exception, or return `:ok`. Compilation will
fail when the exception is raised.
"""
@spec validate!(Config.t(), atom()) :: :ok
def validate!(config, module) do
config
|> schema_modules()
|> Enum.each(& &1.validate!(config, module))
:ok
end
defp schema_modules(config) do
config
|> Extension.Config.extensions()
|> Extension.Config.extension_modules(["Ecto", "Schema"])
end
defp schema_modules_with_use(config) do
config
|> Extension.Config.extensions()
|> Enum.filter(&Base.use?(&1, ["Ecto", "Schema"]))
|> Enum.map(&Module.concat([&1] ++ ["Ecto", "Schema"]))
end
@doc """
Validates that the ecto schema has the specified field.
If the field doesn't exist, it'll raise an exception.
"""
@spec require_schema_field!(atom(), atom(), atom()) :: :ok
def require_schema_field!(module, field, extension) do
fields = module.__schema__(:fields)
fields
|> Enum.member?(field)
|> case do
true -> :ok
false -> raise_missing_field_error!(module, field, extension)
end
end
@spec raise_missing_field_error!(module(), atom(), atom()) :: no_return()
defp raise_missing_field_error!(module, field, extension),
do: raise SchemaError, message: "A `#{inspect field}` schema field should be defined in #{inspect module} to use #{inspect extension}"
end