Diesel

Hex.pmDocumentation

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

Installation

Add diesel to your list of dependencies in mix.exs:

def deps do
  [
    {:diesel, "~> 0.8"}
  ]
end

Quick 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
end

2. 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
    ]
end

3. 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
end

4. 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
end

Core Concepts

Tags

Tags are the building blocks of your DSL. Each tag can have:

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
end

Parsers

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
end

Generators

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
end

Attribute 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

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
    ]
end

Documentation

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.