Diesel
Diesel is a powerful toolkit for building Domain Specific Languages (DSLs) in Elixir. Create declarative, structured, and reusable DSLs with compile-time validation and flexible code generation.
Features
- Declarative: Clean, HTML-like syntax that’s easy to read and write
- Structured: Compile-time schema validation ensures correctness
- Reusable: Same DSL can power multiple code generators
- Flexible: Support for custom parsers, generators, and tag definitions
- Type-safe: Rich attribute validation with kinds, constraints, and defaults
Installation
Add diesel to your list of dependencies in mix.exs:
def deps do
[
{:diesel, "~> 0.8"}
]
endQuick Start
Here’s an example of building a database schema DSL:
1. Define your DSL library
defmodule MyApp.Schema do
use Diesel, otp_app: :my_app
end2. Define the DSL structure
defmodule MyApp.Schema.Dsl do
use Diesel.Dsl,
otp_app: :my_app,
tags: [
MyApp.Schema.Dsl.Table,
MyApp.Schema.Dsl.Column,
MyApp.Schema.Dsl.Index,
MyApp.Schema.Dsl.Relationship,
MyApp.Schema.Dsl.Validation
]
end3. Define your tags
defmodule MyApp.Schema.Dsl.Table do
use Diesel.Tag
tag do
attribute :name, kind: :atom
attribute :primary_key, kind: :atom, default: :id
child :column, min: 1
child :index, min: 0
child :relationship, min: 0
end
end
defmodule MyApp.Schema.Dsl.Column do
use Diesel.Tag
tag do
attribute :name, kind: :atom
attribute :type, kind: :atom,
one_of: [:string, :integer, :boolean, :datetime, :decimal, :text]
attribute :null, kind: :boolean, default: true
attribute :unique, kind: :boolean, default: false
attribute :size, kind: :integer, required: false
child :validation, min: 0
end
end
defmodule MyApp.Schema.Dsl.Relationship do
use Diesel.Tag
tag do
attribute :type, kind: :atom, one_of: [:belongs_to, :has_many, :has_one]
attribute :name, kind: :atom
attribute :target, kind: :atom
attribute :foreign_key, kind: :atom, required: false
end
end4. Use your DSL
defmodule MyApp.UserSchema do
use MyApp.Schema
database do
table :users do
column :email, type: :string, null: false, unique: true do
validation :format, pattern: ~r/@/
validation :length, min: 5, max: 100
end
column :name, type: :string, null: false, size: 255 do
validation :length, min: 2, max: 50
end
column :age, type: :integer do
validation :range, min: 13, max: 120
end
column :created_at, type: :datetime, null: false
column :updated_at, type: :datetime, null: false
index [:email], unique: true
index [:created_at]
relationship :has_many, :posts, target: :posts, foreign_key: :user_id
relationship :has_one, :profile, target: :profiles
end
table :posts do
column :title, type: :string, null: false, size: 200 do
validation :length, min: 5, max: 200
end
column :content, type: :text, null: false
column :published, type: :boolean, default: false
column :user_id, type: :integer, null: false
index [:user_id]
index [:published, :created_at]
relationship :belongs_to, :user, target: :users
end
end
endCore Concepts
Tags
Tags are the building blocks of your DSL. Each tag can have:
- Attributes: Named parameters with type validation
- Children: Nested tags with cardinality constraints
- Content: Raw values or modules
defmodule MyDsl.Tag do
use Diesel.Tag
tag do
attribute :name, kind: :atom, required: true
attribute :size, kind: :integer, min: 1, max: 1000
child :nested_tag, min: 0, max: 5
child kind: :module, min: 1, max: 1 # unnamed children
end
endParsers
Transform your DSL definition into custom data structures:
defmodule MyApp.Schema.Parser do
@behaviour Diesel.Parser
def parse(definition, _opts) do
# Transform table definitions into Schema structs
tables = extract_tables(definition)
relationships = extract_relationships(definition)
# Return enhanced definition with parsed data
definition
|> put_in([:parsed_tables], tables)
|> put_in([:relationships], relationships)
end
defp extract_tables({:database, _attrs, children}) do
children
|> Enum.filter(&match?({:table, _attrs, _children}, &1))
|> Enum.map(&parse_table/1)
end
endGenerators
Generate code from your parsed DSL:
defmodule MyApp.Schema.EctoGenerator do
@behaviour Diesel.Generator
def generate(definition, _opts) do
tables = get_in(definition, [:parsed_tables])
quote do
# Generate Ecto schema modules
unquote_splicing(generate_schemas(tables))
# Generate migration functions
def migration_up do
unquote_splicing(generate_create_tables(tables))
end
end
end
defp generate_schemas(tables) do
Enum.map(tables, fn table ->
quote do
defmodule unquote(Module.concat(__MODULE__, table.name)) do
use Ecto.Schema
schema unquote(Atom.to_string(table.name)) do
unquote_splicing(generate_fields(table.columns))
end
end
end
end)
end
endAttribute Types
Diesel supports rich attribute validation:
attribute :name, kind: :atom # Basic types
attribute :size, kind: :integer, min: 1, max: 255 # Numeric constraints
attribute :type, kind: :atom, one_of: [:string, :integer] # Enums
attribute :options, kind: :keyword_list # Complex types
attribute :*, kind: :* # Generic attributes (0.8.0+)Advanced Features
- Kernel Conflicts: Handle conflicts with Kernel functions using
:overrides - Debug Mode: Inspect generated code during compilation with
debug: true - Custom Naming: Override default module naming conventions
- Multiple Parsers: Chain multiple parsers for complex transformations
- Conditional Generation: Generate different code based on parsed data
Example with advanced options:
defmodule MyApp.Schema do
use Diesel,
otp_app: :my_app,
overrides: [import: 2], # Handle kernel conflicts
parsers: [
MyApp.Schema.Parser,
MyApp.Schema.ValidationParser,
MyApp.Schema.RelationshipParser
],
generators: [
MyApp.Schema.EctoGenerator,
MyApp.Schema.MigrationGenerator,
MyApp.Schema.GraphQLGenerator
]
endDocumentation
For detailed guides and examples:
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE.md file for details.