Packages
spark
2.7.2
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.0
2.4.1
2.4.0
2.3.14
2.3.13
2.3.12
2.3.11
2.3.10
2.3.9
2.3.8
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.69
2.2.68
2.2.67
2.2.66
2.2.65
2.2.64
2.2.63
2.2.62
2.2.61
2.2.60
2.2.59
2.2.58
2.2.57
2.2.56
2.2.55
2.2.54
2.2.53
2.2.52
2.2.51
2.2.50
2.2.49
2.2.48
2.2.47
2.2.46
2.2.45
2.2.44
2.2.43
2.2.42
2.2.41
2.2.40
2.2.39
2.2.38
2.2.37
2.2.36
2.2.35
2.2.34
2.2.33
2.2.32
2.2.31
2.2.30
2.2.29
2.2.28
2.2.27
2.2.26
2.2.25
2.2.24
2.2.23
2.2.22
2.2.21
2.2.20
2.2.19
2.2.18
2.2.17
2.2.16
2.2.15
2.2.14
2.2.13
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.24
2.1.23
2.1.22
2.1.21
2.1.20
2.1.19
2.1.18
2.1.17
2.1.16
2.1.15
2.1.14
2.1.13
2.1.12
2.1.11
2.1.10
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.1
2.0.0
1.1.55
1.1.54
1.1.53
1.1.52
1.1.51
1.1.50
1.1.49
1.1.48
1.1.47
1.1.46
1.1.45
1.1.44
1.1.43
1.1.42
1.1.41
1.1.40
1.1.39
1.1.38
1.1.37
1.1.36
1.1.35
1.1.34
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
retired
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.22
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Generic tooling for building DSLs
Current section
Files
Jump to
Current section
Files
documentation/how_to/writing-extensions.md
<!--
SPDX-FileCopyrightText: 2020 Zach Daniel
SPDX-FileCopyrightText: 2022 spark contributors <https://github.com/ash-project/spark/graphs.contributors>
SPDX-License-Identifier: MIT
-->
# Writing Extensions
Writing extensions generally involves three main components.
## The DSL declaration
The DSL is declared as a series of `Spark.Dsl.Section`, which can contain `Spark.Dsl.Entity` and further `Spark.Dsl.Section` structs. See `Spark.Dsl.Section` and `Spark.Dsl.Entity` for more information.
If you want to build those structs programmatically, see [Building Extensions with the Builder API](build-extensions-with-builders.md).
## Compile-time processing: Transformers, Persisters, and Verifiers
Extensions can hook into compilation at two stages: during compilation (Transformers and Persisters) and after compilation (Verifiers). Within the compilation stage, all Transformers run before any Persisters.
The overall execution order is:
1. **Transformers** — run during compilation, in dependency order. Can read and modify any part of the DSL state.
2. **Persisters** — run during compilation, after all Transformers have completed. Should only write to the persisted data map.
3. **Verifiers** — run after the module is compiled. Read-only. Does not create compile-time dependencies between modules.
All three are declared as options to `use Spark.Dsl.Extension`:
```elixir
use Spark.Dsl.Extension,
sections: [@my_section],
transformers: [MyApp.Transformers.SetDefaults],
persisters: [MyApp.Persisters.CacheComputedValues],
verifiers: [MyApp.Verifiers.ValidateConfig]
```
## Transformers
Each transformer can declare other transformers it must go before or after using `before?/1` and `after?/1` callbacks, and is then given the opportunity to modify the entirety of the DSL state. This allows extensions to make rich modifications to the structure in question.
```elixir
defmodule MyApp.Transformers.SetDefaults do
use Spark.Dsl.Transformer
def transform(dsl_state) do
name = Spark.Dsl.Transformer.get_option(dsl_state, [:my_section], :name)
dsl_state = Spark.Dsl.Transformer.persist(dsl_state, :name, name || :default)
{:ok, dsl_state}
end
def after?(MyApp.Transformers.EarlierTransformer), do: true
def after?(_), do: false
end
```
See `Spark.Dsl.Transformer` for the full list of helper functions and return values.
## Persisters
Persisters implement the same `Spark.Dsl.Transformer` behaviour as transformers — they use `use Spark.Dsl.Transformer` and define a `transform/1` callback. The differences are:
- They are listed under `persisters:` instead of `transformers:`
- They **always** run after all transformers have finished, regardless of any `before?`/`after?` declarations targeting transformers (those are silently ignored)
- By convention, they should **only** write to the persisted data map via `Spark.Dsl.Transformer.persist/3`, and should not mutate sections or entities
Persisters do support `before?`/`after?` ordering relative to **other persisters**.
Use persisters to precompute and cache derived values that need a complete, fully-transformed view of the DSL:
```elixir
defmodule MyApp.Persisters.CacheActionNames do
use Spark.Dsl.Transformer
def transform(dsl_state) do
action_names =
dsl_state
|> Spark.Dsl.Transformer.get_entities([:actions])
|> Enum.map(& &1.name)
{:ok, Spark.Dsl.Transformer.persist(dsl_state, :action_names, action_names)}
end
end
```
Persisted values can be retrieved at runtime via `Spark.Dsl.Extension.get_persisted/3`.
## Verifiers
Verifiers validate DSL state after the module has been compiled. They are read-only — they cannot modify the DSL state, only return `:ok`, `{:error, term}`, or `{:warn, warning}`. Because verifiers run post-compilation, they can safely reference other modules without creating compile-time dependencies between them.
```elixir
defmodule MyApp.Verifiers.ValidateConfig do
use Spark.Dsl.Verifier
def verify(dsl_state) do
name = Spark.Dsl.Verifier.get_option(dsl_state, [:my_section], :name)
if name do
:ok
else
{:error,
Spark.Error.DslError.exception(
message: "name is required",
path: [:my_section, :name],
module: Spark.Dsl.Verifier.get_persisted(dsl_state, :module)
)}
end
end
end
```
Prefer verifiers over transformers when you only need to validate — they run later, see the final state, and their post-compilation timing avoids circular compile-time dependencies when referencing other Spark-based modules.
See `Spark.Dsl.Verifier` for the full list of helper functions and return values.
## Introspection
Use functions in `Spark.Dsl.Extension` to retrieve the stored values from the DSL and expose them in a module. The convention is to place functions for something like `MyApp.MyExtension` in `MyApp.MyExtension.Info`. Using introspection functions like this allows for a richer introspection API (i.e not just getting and retrieving raw values), and it also allows us to add type specs and documentation, which is helpful when working generically. I.e `module_as_variable.table()` can't be known by dialyzer, whereas `Extension.table(module)` can be.
## Source Annotations
Spark automatically tracks source location information for all DSL elements. This enables better error messages, IDE integration, and debugging capabilities. See [Using Source Annotations](use-source-annotations.md) for details on accessing and using this information in your extensions.