Packages
spark
2.6.0
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
usage-rules.md
<!--
SPDX-FileCopyrightText: 2020 Zach Daniel
SPDX-FileCopyrightText: 2022 spark contributors <https://github.com/ash-project/spark/graphs.contributors>
SPDX-License-Identifier: MIT
-->
# Rules for working with Spark
## Overview
Spark is a framework for building Domain-Specific Languages (DSLs) in Elixir. It enables developers to create declarative DSLs with minimal boilerplate.
## Core Architecture
### 1. Entity-Section-Extension Model
Spark DSLs are built using three core components:
```elixir
# 1. Entities - Individual DSL constructors
@field %Spark.Dsl.Entity{
name: :field, # DSL function name
target: MyApp.Field, # Struct to build
args: [:name, :type], # Positional arguments
schema: [ # Validation schema
name: [type: :atom, required: true],
type: [type: :atom, required: true]
]
}
# 2. Sections - Organize related entities
@fields %Spark.Dsl.Section{
name: :fields,
entities: [@field], # Contains entities
sections: [], # Can nest sections
schema: [ # Section-level options
required: [type: {:list, :atom}, default: []]
]
}
# 3. Extensions - Package DSL functionality
defmodule MyExtension do
use Spark.Dsl.Extension,
sections: [@fields],
transformers: [MyTransformer],
verifiers: [MyVerifier]
end
```
### 2. Compile-Time Processing Pipeline
```
DSL Definition → Parsing → Validation → Transformation → Verification → Code Generation
```
1. **Parsing**: DSL syntax converted to internal representation
2. **Validation**: Schema validation via `Spark.Options`
3. **Transformation**: Transformers modify DSL state
4. **Verification**: Verifiers validate final state
5. **Code Generation**: Generate runtime modules/functions
## Creating a DSL with Spark
### Basic DSL Structure
```elixir
# Step 1: Define your extension
defmodule MyLibrary.Dsl do
@my_nested_entity %Spark.Dsl.Entity{
name: :my_nested_entity,
target: MyLibrary.MyNestedEntity,
describe: """
Describe the entity.
""",
examples: [
"my_nested_entity :name, option: \"something\"",
"""
my_nested_entity :name do
option "something"
end
"""
],
args: [:name],
schema: [
name: [type: :atom, required: true, doc: "The name of the entity"],
option: [type: :string, default: "default", doc: "An option that does X"]
]
}
@my_entity %Spark.Dsl.Entity{
name: :my_entity,
target: MyLibrary.MyEntity,
args: [:name],
entities: [
entities: [@my_nested_entity]
],
describe: ...,
examples: [...],
schema: [
name: [type: :atom, required: true, doc: "The name of the entity"],
option: [type: :string, default: "default", doc: "An option that does X"]
]
}
@my_section %Spark.Dsl.Section{
name: :my_section,
describe: ...,
examples: [...],
schema: [
section_option: [type: :string]
],
entities: [@my_entity]
}
use Spark.Dsl.Extension, sections: [@my_section]
end
# Entity Targets
defmodule MyLibrary.MyEntity do
defstruct [:name, :option, :entities, :__spark_metadata__]
end
defmodule MyLibrary.MyNestedEntity do
defstruct [:name, :option, :__spark_metadata__]
end
# Step 2: Create the DSL module
defmodule MyLibrary do
use Spark.Dsl,
default_extensions: [
extensions: [MyLibrary.Dsl]
]
end
# Step 3: Use the DSL
defmodule MyApp.Example do
use MyLibrary
my_section do
my_entity :example_name do
option "custom value"
my_nested_entity :nested_name do
option "nested custom value"
end
end
end
end
```
### Working with Transformers
Transformers modify the DSL at compile time:
```elixir
defmodule MyLibrary.Transformers.AddDefaults do
use Spark.Dsl.Transformer
def transform(dsl_state) do
# Add a default entity if none exist
entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section])
if Enum.empty?(entities) do
{:ok,
Spark.Dsl.Transformer.add_entity(
dsl_state,
[:my_section],
%MyLibrary.MyEntity{name: :default}
)}
else
{:ok, dsl_state}
end
end
# Control execution order
def after?(_), do: false
def before?(OtherTransformer), do: true
def before?(_), do: false
end
```
### Creating Verifiers
Verifiers validate the final DSL state:
```elixir
defmodule MyLibrary.Verifiers.UniqueNames do
use Spark.Dsl.Verifier
def verify(dsl_state) do
entities = Spark.Dsl.Extension.get_entities(dsl_state, [:my_section])
names = Enum.map(entities, & &1.name)
if length(names) == length(Enum.uniq(names)) do
:ok
else
# Find the duplicate entity to get its location
duplicate_name =
names
|> Enum.frequencies()
|> Enum.find_value(fn {name, count} -> if count > 1, do: name end)
duplicate_entity = Enum.find(entities, &(&1.name == duplicate_name))
location = Spark.Dsl.Entity.anno(duplicate_entity)
{:error,
Spark.Error.DslError.exception(
message: "Entity names must be unique, found duplicate: #{duplicate_name}",
path: [:my_section, duplicate_name],
module: Spark.Dsl.Verifier.get_persisted(dsl_state, :module),
location: location
)}
end
end
end
```
## Info Modules
Spark provides an Info Generator system for introspection of DSL-defined modules. All introspection should go through info modules rather than accessing DSL state directly.
### 1. Info Module Generation
```elixir
# Define an info module for your extension
defmodule MyLibrary.Info do
use Spark.InfoGenerator,
extension: MyLibrary.Dsl,
sections: [:my_section]
end
# The info generator creates accessor functions for DSL data:
# - MyLibrary.Info.my_section_entities(module)
# - MyLibrary.Info.my_section_option!(module, :option_name)
# - MyLibrary.Info.my_section_option(module, :option_name, default)
# Example usage:
entities = MyLibrary.Info.my_section_entities(MyApp.Example)
required_option = MyLibrary.Info.my_section_option!(MyApp.Example, :required)
{:ok,optional_value} = MyLibrary.Info.my_section_option(MyApp.Example, :optional, "default")
```
Benefits of using info modules:
- **Type Safety**: Generated functions provide compile-time guarantees
- **Performance**: Info data is cached and optimized for runtime access
- **Consistency**: Standardized API across all Spark-based libraries
- **Documentation**: Auto-generated docs for introspection functions
## Key APIs for Development
### Essential Functions
```elixir
# Get entities from a section
entities = Spark.Dsl.Extension.get_entities(dsl_state, [:section_path])
# Get section options
option = Spark.Dsl.Extension.get_opt(dsl_state, [:section_path], :option_name, default_value)
# Add entities during transformation
dsl_state = Spark.Dsl.Transformer.add_entity(dsl_state, [:section_path], entity)
# Persist data across transformers
dsl_state = Spark.Dsl.Transformer.persist(dsl_state, :key, value)
value = Spark.Dsl.Transformer.get_persisted(dsl_state, :key)
# Get current module being compiled
module = Spark.Dsl.Verifier.get_persisted(dsl_state, :module)
# Get annotation information for error reporting
section_anno = Spark.Dsl.Transformer.get_section_anno(dsl_state, [:section_path])
option_anno = Spark.Dsl.Transformer.get_opt_anno(dsl_state, [:section_path], :option_name)
entity_anno = Spark.Dsl.Entity.anno(entity)
```
### Schema Definition with Spark.Options
```elixir
schema = [
required_field: [
type: :atom,
required: true,
doc: "Documentation for the field"
],
optional_field: [
type: {:list, :string},
default: [],
doc: "Optional field with default"
],
complex_field: [
type: {:or, [:atom, :string, {:struct, MyStruct}]},
required: false
]
]
```
## Important Implementation Details
### 1. Compile-Time vs Runtime
- **DSL processing happens at compile time**
- Transformers and verifiers run during compilation
- Generated code is optimized for runtime performance
- Use `persist/3` to cache expensive computations
### 2. Error Handling
Always provide context in errors:
```elixir
{:error,
Spark.Error.DslError.exception(
message: "Clear error message",
path: [:section, :subsection], # DSL path
module: module, # Module being compiled
location: annotation # Source location (when available)
)}
```
### Error Location Information
When creating DslErrors, include location information whenever possible to help developers quickly identify the source of issues:
```elixir
# For entity-related errors, get entity annotations
entity_location = Spark.Dsl.Entity.anno(entity)
# For section-related errors, get section annotations
section_location = Spark.Dsl.Transformer.get_section_anno(dsl_state, [:section_path])
# For option-related errors, get option annotations
option_location = Spark.Dsl.Transformer.get_opt_anno(dsl_state, [:section_path], :option_name)
# Include in DslError
{:error,
Spark.Error.DslError.exception(
message: "Detailed error message",
path: [:section_path],
module: module,
location: entity_location # or section_location, option_location
)}
```
## Common Gotchas
### 1. Compilation Deadlocks
```elixir
# WRONG - Causes deadlock
def transform(dsl_state) do
module = get_persisted(dsl_state, :module)
module.some_function() # Module isn't compiled yet!
end
# RIGHT - Use DSL state
def transform(dsl_state) do
entities = get_entities(dsl_state, [:section])
# Work with DSL state, not module functions
end
```
### 2. Extension Order
- Extensions are processed in order
- Later extensions can modify earlier ones
- Use transformer ordering for dependencies
## Performance Considerations
1. **Compilation Performance**
- Heavy transformers slow compilation
- Cache expensive computations with `persist/3`
- Use verifiers instead of transformers when possible
2. **Runtime Performance**
- DSL processing has zero runtime overhead
- Generated code is optimized
- Info modules cache DSL data efficiently
3. **Memory Usage**
- DSL state is cleaned up after compilation
- Runtime footprint is minimal
- Use structs efficiently in entities
## Summary
Spark enables:
- Clean, declarative DSL syntax
- Compile-time validation and transformation
- Extensible architecture
- Excellent developer experience
When coding with Spark:
1. Think in terms of entities, sections, and extensions
2. Leverage compile-time processing for validation
3. Use transformers for complex logic
4. Test DSLs thoroughly
5. Provide clear error messages with location information
6. Use annotation introspection for better debugging
7. Document your DSLs well