-
Notifications
You must be signed in to change notification settings - Fork 23
/
transformer.ex
341 lines (284 loc) · 9.46 KB
/
transformer.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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
defmodule Spark.Dsl.Transformer do
@moduledoc """
A transformer manipulates and/or validates the entire DSL state of a resource.
It's `transform/1` takes a `map`, which is just the values/configurations at each point
of the DSL. Don't manipulate it directly, if possible, instead use functions like
`get_entities/3` and `replace_entity/4` to manipulate it.
Use the `after?/1` and `before?/1` callbacks to ensure that your transformer
runs either before or after some other transformer.
Return `true` in `after_compile/0` to have the transformer run in an `after_compile` hook,
but keep in mind that no modifications to the dsl structure will be retained, so there is no
real point in modifying the dsl that you return.
"""
@callback transform(map) ::
:ok
| {:ok, map}
| {:error, term}
| {:warn, map, String.t() | list(String.t())}
| :halt
@callback before?(module) :: boolean
@callback after?(module) :: boolean
@callback after_compile?() :: boolean
defmacro __using__(_) do
quote generated: true do
@behaviour Spark.Dsl.Transformer
def before?(_), do: false
def after?(_), do: false
def after_compile?, do: false
defoverridable before?: 1, after?: 1, after_compile?: 0
end
end
@doc """
Saves a value into the dsl config with the given key.
This can be used to precompute some information and cache it onto the resource,
or simply store a computed value. It can later be retrieved with `Spark.Dsl.Extension.get_persisted/3`.
"""
def persist(dsl, key, value) do
Map.update(dsl, :persist, %{key => value}, &Map.put(&1, key, value))
end
@doc """
Runs the function in an async compiler.
Use this for compiling new modules and having them compiled
efficiently asynchronously.
"""
def async_compile(dsl, fun) do
task = Spark.Dsl.Extension.do_async_compile(fun)
tasks = get_persisted(dsl, :spark_compile_tasks, [])
persist(dsl, :spark_compile_tasks, [task | tasks])
end
@doc """
Add a quoted expression to be evaluated in the DSL module's context.
Use this *extremely sparingly*. It should almost never be necessary, unless building certain
extensions that *require* the module in question to define a given function.
What you likely want is either one of the DSL introspection functions, like `Spark.Dsl.Extension.get_entities/2`
or `Spark.Dsl.Extension.get_opt/5)`. If you simply want to store a custom value that can be retrieved easily, or
cache some precomputed information onto the resource, use `persist/3`.
Provide the dsl state, bindings that should be unquote-able, and the quoted block
to evaluate in the module. For example, if we wanted to support a `resource.primary_key()` function
that would return the primary key (this is unnecessary, just an example), we might do this:
```elixir
fields = the_primary_key_fields
dsl_state =
Transformer.eval(
dsl_state,
[fields: fields],
quote do
def primary_key() do
unquote(fields)
end
end
)
```
"""
def eval(dsl, bindings, block) do
to_eval = {block, bindings}
Map.update(
dsl,
:eval,
[to_eval],
&[to_eval | &1]
)
end
def get_persisted(dsl, key, default \\ nil) do
dsl
|> Map.get(:persist, %{})
|> Map.get(key, default)
end
def build_entity!(extension, path, name, opts) do
case build_entity(extension, path, name, opts) do
{:ok, entity} ->
entity
{:error, error} ->
if is_exception(error) do
raise error
else
raise "Error building entity #{inspect(error)}"
end
end
end
def build_entity(extension, path, name, opts) do
do_build_entity(extension.sections(), path, name, opts)
end
defp do_build_entity(sections, [section_name], name, opts) do
section = Enum.find(sections, &(&1.name == section_name))
entity = Enum.find(section.entities, &(&1.name == name))
do_build(entity, opts)
end
defp do_build_entity(
sections,
[section_name, maybe_entity_name],
maybe_nested_entity_name,
opts
) do
section = Enum.find(sections, &(&1.name == section_name))
entity =
if section do
Enum.find(section.entities, &(&1.name == maybe_entity_name))
end
sub_entity =
if entity do
entity.entities
|> Keyword.values()
|> List.flatten()
|> Enum.find(&(&1.name == maybe_nested_entity_name))
end
if sub_entity do
do_build(sub_entity, opts)
else
do_build_entity(section.sections, [maybe_entity_name], maybe_nested_entity_name, opts)
end
end
defp do_build_entity(sections, [section_name | rest], name, opts) do
section = Enum.find(sections, &(&1.name == section_name))
do_build_entity(section.sections, rest, name, opts)
end
defp do_build(entity, opts) do
entity_names =
entity.entities
|> Kernel.||([])
|> Keyword.keys()
{entities, opts} = Keyword.split(opts, entity_names)
{before_validate_auto, after_validate_auto} =
Keyword.split(entity.auto_set_fields || [], Keyword.keys(entity.schema))
with {:ok, opts} <-
Spark.Options.validate(
Keyword.merge(opts, before_validate_auto),
entity.schema
),
opts <- Keyword.merge(opts, after_validate_auto) do
result = struct(struct(entity.target, opts), entities)
case Spark.Dsl.Entity.transform(entity.transform, result) do
{:ok, built} ->
Spark.Dsl.Entity.maybe_apply_identifier(built, entity.identifier)
other ->
other
end
else
{:error, error} ->
{:error, error}
end
end
def add_entity(dsl_state, path, entity, opts \\ []) do
Map.update(dsl_state, path, %{entities: [entity], opts: []}, fn config ->
Map.update(config, :entities, [entity], fn entities ->
if (opts[:type] || :prepend) == :prepend do
[entity | entities]
else
entities ++ [entity]
end
end)
end)
end
def remove_entity(dsl_state, path, func) do
Map.update(dsl_state, path, %{entities: [], opts: []}, fn config ->
Map.update(config, :entities, [], fn entities ->
Enum.reject(entities, func)
end)
end)
end
def get_entities(dsl_state, path) do
dsl_state
|> Map.get(path, %{entities: []})
|> Map.get(:entities, [])
end
def fetch_option(dsl_state, path, option) do
dsl_state
|> Map.get(path, %{opts: []})
|> Map.get(:opts)
|> Kernel.||([])
|> Keyword.fetch(option)
end
def get_option(dsl_state, path, option, default \\ nil) do
dsl_state
|> Map.get(path, %{opts: []})
|> Map.get(:opts)
|> Kernel.||([])
|> Keyword.get(option, default)
end
def set_option(dsl_state, path, option, value) do
dsl_state
|> Map.put_new(path, %{opts: []})
|> Map.update!(path, fn existing_opts ->
existing_opts
|> Map.put_new(:opts, [])
|> Map.update!(:opts, fn opts ->
Keyword.put(opts, option, value)
end)
end)
end
def replace_entity(dsl_state, path, replacement, matcher \\ nil) do
matcher =
matcher ||
fn record ->
record.__struct__ == replacement.__struct__ and
record.__identifier__ == replacement.__identifier__
end
Map.replace_lazy(dsl_state, path, fn config ->
Map.replace_lazy(config, :entities, fn entities ->
replace_match(entities, replacement, matcher)
end)
end)
end
defp replace_match(entities, replacement, matcher) do
Enum.map(entities, fn entity ->
if matcher.(entity) do
replacement
else
entity
end
end)
end
def sort(transformers) do
digraph = :digraph.new()
transformers
|> Enum.each(fn transformer ->
:digraph.add_vertex(digraph, transformer)
end)
transformers
|> Enum.each(fn left ->
transformers
|> Enum.each(fn right ->
if left != right do
left_before_right? = left.before?(right) || right.after?(left)
left_after_right? = left.after?(right) || right.before?(left)
cond do
# This is annoying, but some modules have `def after?(_), do: true`
# The idea being that they'd like to go after everything that isn't
# explicitly after it. Same with `def before?(_), do: true`
left_before_right? && left_after_right? ->
:ok
left_before_right? ->
:digraph.add_edge(digraph, left, right)
left_after_right? ->
:digraph.add_edge(digraph, right, left)
true ->
:ok
end
end
end)
end)
transformers = walk_rest(digraph)
:digraph.delete(digraph)
transformers
end
defp walk_rest(digraph, acc \\ []) do
case :digraph.vertices(digraph) do
[] ->
Enum.reverse(acc)
vertices ->
case Enum.find(vertices, &(:digraph.in_neighbours(digraph, &1) == [])) do
nil ->
case Enum.find(vertices, &(:digraph.out_neighbours(digraph, &1) == [])) do
nil ->
raise "Cycle detected in transformer order"
vertex ->
:digraph.del_vertex(digraph, vertex)
walk_rest(digraph, acc ++ [vertex])
end
vertex ->
:digraph.del_vertex(digraph, vertex)
walk_rest(digraph, [vertex | acc])
end
end
end
end