Skip to content

Latest commit

 

History

History
123 lines (85 loc) · 5.42 KB

File metadata and controls

123 lines (85 loc) · 5.42 KB

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.

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:

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.

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:

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.

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 for details on accessing and using this information in your extensions.