Home/Macro Mastery/DSL Creation

DSL Creation

Domain-specific languages in Rust

expert
dslmacrosyntax
🎮 Interactive Playground

What is DSL Creation?

Domain-Specific Languages (DSLs) are specialized mini-languages designed to solve problems within a specific domain in a more expressive and natural way than general-purpose languages. In Rust, DSLs leverage the type system, macros, and method chaining to create intuitive, type-safe APIs that look and feel like custom languages while maintaining all the safety guarantees of Rust.

There are two types of DSLs: External DSLs require custom parsers (like SQL or regex), while Internal DSLs (also called embedded DSLs) are built within Rust itself using macros, builder patterns, and clever API design. Internal DSLs are Rust code that feels like a different language through syntactic sugar and domain-specific abstractions.

// HTML DSL - looks like HTML, but it's Rust macros
html! {
    <div class="container">
        <h1>"Hello, World!"</h1>
        <p>{ format!("Count: {}", count) }</p>
        <ul>
            { for item in items {
                <li>{ item }</li>
            }}
        </ul>
    </div>
}

// Query DSL - SQL-like but type-safe at compile time
let active_users = users::table
    .select((users::id, users::name, users::email))
    .filter(users::active.eq(true))
    .order(users::created_at.desc())
    .limit(10)
    .load::<User>(&mut conn)?;

// State machine DSL - declarative state transitions
state_machine! {
    transitions: {
        Idle + Start => Running,
        Running + Pause => Paused,
        Paused + Resume => Running,
        Running + Stop => Idle,
        * + Reset => Idle,
    }
}

// Testing DSL - BDD-style readable tests
describe! {
    before_each {
        let mut db = setup_test_db();
    }
    
    it "creates new users" {
        let user = db.create_user("alice@example.com");
        expect(user.id).to_be_greater_than(0);
    }
    
    it "prevents duplicate emails" {
        db.create_user("bob@example.com");
        expect(|| db.create_user("bob@example.com"))
            .to_throw::<DuplicateError>();
    }
}

// Router DSL - expressive route definition
routes! {
    GET "/users" => list_users,
    GET "/users/:id" => get_user,
    POST "/users" => create_user,
    PUT "/users/:id" => update_user,
    DELETE "/users/:id" => delete_user,
    
    scope "/api/v1" {
        GET "/health" => health_check,
        POST "/auth/login" => login,
    }
}
Key characteristics:
  • Domain-focused syntax: Natural expression of domain concepts
  • Type safety: Compile-time validation of DSL correctness
  • Ergonomic: Reduces boilerplate and improves readability
  • Composable: DSL elements combine naturally
  • Zero-cost: Often compiles to efficient code with no runtime overhead
  • Error messages: Can provide domain-specific error diagnostics

Real-World Examples

Example 1: HTML Builder DSL (Web/Frontend)

Web frameworks like Yew and Dioxus use HTML DSLs for declarative UI construction. This pattern provides type-safe HTML generation with compile-time validation:

// HTML DSL implementation using macro_rules!
use std::fmt::{self, Display};

#[derive(Debug, Clone)]
pub struct HtmlNode {
    tag: String,
    attributes: Vec<(String, String)>,
    children: Vec<HtmlContent>,
}

#[derive(Debug, Clone)]
pub enum HtmlContent {
    Node(HtmlNode),
    Text(String),
    Expression(String),
}

impl Display for HtmlNode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "<{}", self.tag)?;
        for (key, value) in &self.attributes {
            write!(f, " {}=\"{}\"", key, value)?;
        }
        write!(f, ">")?;
        
        for child in &self.children {
            match child {
                HtmlContent::Node(node) => write!(f, "{}", node)?,
                HtmlContent::Text(text) => write!(f, "{}", text)?,
                HtmlContent::Expression(expr) => write!(f, "{}", expr)?,
            }
        }
        
        write!(f, "</{}>", self.tag)
    }
}

// HTML DSL macro
macro_rules! html {
    // Self-closing tags
    (<$tag:ident $($attr:ident = $val:expr)* />) => {
        HtmlContent::Node(HtmlNode {
            tag: stringify!($tag).to_string(),
            attributes: vec![$(
                (stringify!($attr).to_string(), $val.to_string())
            ),*],
            children: vec![],
        })
    };
    
    // Tags with children
    (<$tag:ident $($attr:ident = $val:expr)* > $($children:tt)*) => {{
        let mut children = Vec::new();
        html!(@children children $($children)*);
        HtmlContent::Node(HtmlNode {
            tag: stringify!($tag).to_string(),
            attributes: vec![$(
                (stringify!($attr).to_string(), $val.to_string())
            ),*],
            children,
        })
    }};
    
    // Text content
    ($text:expr) => {
        HtmlContent::Text($text.to_string())
    };
    
    // Expression interpolation
    ({ $expr:expr }) => {
        HtmlContent::Expression($expr.to_string())
    };
    
    // Helper to collect children
    (@children $children:ident) => {};
    (@children $children:ident $child:tt $($rest:tt)*) => {
        $children.push(html!($child));
        html!(@children $children $($rest)*);
    };
}

// Real-world usage: Building a user profile page
pub fn render_user_profile(user: &User, posts: &[Post]) -> String {
    let html = html! {
        <div class = "profile-container">
            <div class = "header">
                <img src = {user.avatar_url} alt = "avatar" />
                <h1> { user.name } </h1>
                <p class = "email"> { user.email } </p>
            </div>
            <div class = "posts">
                <h2> "Recent Posts" </h2>
                { posts.iter().map(|post| html! {
                    <article>
                        <h3> { post.title } </h3>
                        <p> { post.excerpt } </p>
                    </article>
                }).collect::<Vec<_>>().join("") }
            </div>
        </div>
    };
    
    html.to_string()
}

// Production example: Server-side rendering with escaping
pub struct HtmlBuilder {
    nodes: Vec<HtmlContent>,
}

impl HtmlBuilder {
    pub fn new() -> Self {
        Self { nodes: Vec::new() }
    }
    
    pub fn element(&mut self, tag: &str) -> ElementBuilder {
        ElementBuilder {
            tag: tag.to_string(),
            attributes: Vec::new(),
            children: Vec::new(),
        }
    }
    
    pub fn render(&self) -> String {
        self.nodes.iter()
            .map(|n| n.to_string())
            .collect::<String>()
    }
}

pub struct ElementBuilder {
    tag: String,
    attributes: Vec<(String, String)>,
    children: Vec<HtmlContent>,
}

impl ElementBuilder {
    pub fn attr(mut self, key: &str, value: &str) -> Self {
        self.attributes.push((key.to_string(), escape_html(value)));
        self
    }
    
    pub fn text(mut self, content: &str) -> Self {
        self.children.push(HtmlContent::Text(escape_html(content)));
        self
    }
    
    pub fn child(mut self, child: HtmlContent) -> Self {
        self.children.push(child);
        self
    }
    
    pub fn build(self) -> HtmlContent {
        HtmlContent::Node(HtmlNode {
            tag: self.tag,
            attributes: self.attributes,
            children: self.children,
        })
    }
}

fn escape_html(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#x27;")
}

// Real usage in web server
use std::collections::HashMap;

pub struct User {
    pub name: String,
    pub email: String,
    pub avatar_url: String,
}

pub struct Post {
    pub title: String,
    pub excerpt: String,
}

pub fn handle_profile_request(user_id: u64) -> String {
    let user = fetch_user(user_id);
    let posts = fetch_user_posts(user_id);
    
    let mut builder = HtmlBuilder::new();
    builder.element("html")
        .child(
            builder.element("head")
                .child(builder.element("title")
                    .text(&format!("{}'s Profile", user.name))
                    .build())
                .build()
        )
        .child(
            builder.element("body")
                .child(render_profile_content(&user, &posts))
                .build()
        );
    
    builder.render()
}

fn fetch_user(id: u64) -> User {
    // Database fetch
    User {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        avatar_url: "/avatars/alice.jpg".to_string(),
    }
}

fn fetch_user_posts(id: u64) -> Vec<Post> {
    vec![]
}

fn render_profile_content(user: &User, posts: &[Post]) -> HtmlContent {
    HtmlContent::Text(format!("Profile for {}", user.name))
}
Real-world impact:
  • Yew framework: Uses html! macro for React-like components
  • Dioxus: Similar DSL for cross-platform UIs
  • Maud: Compile-time HTML templates with type safety
  • Reduces XSS vulnerabilities: Automatic escaping by default
  • Compile-time validation: Catches malformed HTML at build time

Example 2: Query Builder DSL (Backend/Database)

Database libraries like Diesel and SeaORM use query DSLs to provide type-safe SQL generation. This eliminates SQL injection and catches schema mismatches at compile time:

// Query Builder DSL implementation
use std::marker::PhantomData;
use std::fmt::{self, Display};

// Type-state pattern ensures correct query construction
pub struct SelectStatement<T> {
    table: String,
    columns: Vec<String>,
    conditions: Vec<String>,
    order: Vec<String>,
    limit: Option<usize>,
    offset: Option<usize>,
    _phantom: PhantomData<T>,
}

pub struct WhereClause<T> {
    field: String,
    operator: String,
    value: String,
    _phantom: PhantomData<T>,
}

pub trait Table {
    fn table_name() -> &'static str;
}

pub trait Column<T>: Display {
    fn column_name(&self) -> &str;
}

// Type-safe column definitions
pub struct UserTable;

impl Table for UserTable {
    fn table_name() -> &'static str {
        "users"
    }
}

#[derive(Debug, Clone)]
pub struct UserId;
#[derive(Debug, Clone)]
pub struct UserName;
#[derive(Debug, Clone)]
pub struct UserEmail;
#[derive(Debug, Clone)]
pub struct UserActive;
#[derive(Debug, Clone)]
pub struct UserCreatedAt;

impl Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "id")
    }
}

impl Column<UserTable> for UserId {
    fn column_name(&self) -> &str {
        "id"
    }
}

impl Display for UserName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "name")
    }
}

impl Column<UserTable> for UserName {
    fn column_name(&self) -> &str {
        "name"
    }
}

impl Display for UserEmail {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "email")
    }
}

impl Column<UserTable> for UserEmail {
    fn column_name(&self) -> &str {
        "email"
    }
}

impl Display for UserActive {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "active")
    }
}

impl Column<UserTable> for UserActive {
    fn column_name(&self) -> &str {
        "active"
    }
}

impl Display for UserCreatedAt {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "created_at")
    }
}

impl Column<UserTable> for UserCreatedAt {
    fn column_name(&self) -> &str {
        "created_at"
    }
}

// Query builder with fluent API
pub fn select<T: Table>() -> SelectStatement<T> {
    SelectStatement {
        table: T::table_name().to_string(),
        columns: vec!["*".to_string()],
        conditions: Vec::new(),
        order: Vec::new(),
        limit: None,
        offset: None,
        _phantom: PhantomData,
    }
}

impl<T: Table> SelectStatement<T> {
    pub fn columns<C: Column<T>>(mut self, cols: &[C]) -> Self {
        self.columns = cols.iter()
            .map(|c| c.column_name().to_string())
            .collect();
        self
    }
    
    pub fn filter<C: Column<T>>(mut self, column: C, op: &str, value: &str) -> Self {
        self.conditions.push(format!("{} {} '{}'", 
            column.column_name(), op, value));
        self
    }
    
    pub fn and_filter<C: Column<T>>(mut self, column: C, op: &str, value: &str) -> Self {
        self.conditions.push(format!("AND {} {} '{}'", 
            column.column_name(), op, value));
        self
    }
    
    pub fn or_filter<C: Column<T>>(mut self, column: C, op: &str, value: &str) -> Self {
        self.conditions.push(format!("OR {} {} '{}'", 
            column.column_name(), op, value));
        self
    }
    
    pub fn order_by<C: Column<T>>(mut self, column: C, direction: &str) -> Self {
        self.order.push(format!("{} {}", column.column_name(), direction));
        self
    }
    
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = Some(limit);
        self
    }
    
    pub fn offset(mut self, offset: usize) -> Self {
        self.offset = Some(offset);
        self
    }
    
    pub fn to_sql(&self) -> String {
        let mut sql = format!("SELECT {} FROM {}", 
            self.columns.join(", "), 
            self.table);
        
        if !self.conditions.is_empty() {
            sql.push_str(" WHERE ");
            sql.push_str(&self.conditions.join(" "));
        }
        
        if !self.order.is_empty() {
            sql.push_str(" ORDER BY ");
            sql.push_str(&self.order.join(", "));
        }
        
        if let Some(limit) = self.limit {
            sql.push_str(&format!(" LIMIT {}", limit));
        }
        
        if let Some(offset) = self.offset {
            sql.push_str(&format!(" OFFSET {}", offset));
        }
        
        sql
    }
}

// Production example: Complex query with joins
pub mod users {
    use super::*;
    
    pub fn table() -> SelectStatement<UserTable> {
        select::<UserTable>()
    }
    
    pub fn id() -> UserId { UserId }
    pub fn name() -> UserName { UserName }
    pub fn email() -> UserEmail { UserEmail }
    pub fn active() -> UserActive { UserActive }
    pub fn created_at() -> UserCreatedAt { UserCreatedAt }
}

// Real-world usage examples
pub fn example_queries() {
    // Simple query
    let query1 = users::table()
        .columns(&[users::id(), users::name(), users::email()])
        .filter(users::active(), "=", "true")
        .order_by(users::created_at(), "DESC")
        .limit(10);
    
    println!("Query 1: {}", query1.to_sql());
    // SELECT id, name, email FROM users WHERE active = 'true' ORDER BY created_at DESC LIMIT 10
    
    // Complex filtering
    let query2 = users::table()
        .filter(users::active(), "=", "true")
        .and_filter(users::email(), "LIKE", "%@example.com")
        .or_filter(users::name(), "=", "admin")
        .order_by(users::name(), "ASC")
        .limit(20)
        .offset(40);
    
    println!("Query 2: {}", query2.to_sql());
    
    // Pagination helper
    fn paginated_users(page: usize, per_page: usize) -> SelectStatement<UserTable> {
        users::table()
            .filter(users::active(), "=", "true")
            .order_by(users::created_at(), "DESC")
            .limit(per_page)
            .offset(page * per_page)
    }
    
    let page_2 = paginated_users(2, 25);
    println!("Page 2: {}", page_2.to_sql());
}

// Advanced: Compile-time query validation with procedural macros
// This would be in a separate proc-macro crate
/*
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    // Parse SQL at compile time
    // Validate against schema
    // Generate type-safe query builder
    // Return optimized code
}

// Usage:
let query = sql! {
    SELECT id, name, email 
    FROM users 
    WHERE active = true 
    ORDER BY created_at DESC 
    LIMIT 10
};
// Compiler error if table/column doesn't exist!
*/

// Type-safe insert/update DSL
pub struct InsertStatement<T> {
    table: String,
    columns: Vec<String>,
    values: Vec<String>,
    _phantom: PhantomData<T>,
}

pub fn insert_into<T: Table>() -> InsertStatement<T> {
    InsertStatement {
        table: T::table_name().to_string(),
        columns: Vec::new(),
        values: Vec::new(),
        _phantom: PhantomData,
    }
}

impl<T: Table> InsertStatement<T> {
    pub fn value<C: Column<T>>(mut self, column: C, value: &str) -> Self {
        self.columns.push(column.column_name().to_string());
        self.values.push(format!("'{}'", value));
        self
    }
    
    pub fn to_sql(&self) -> String {
        format!("INSERT INTO {} ({}) VALUES ({})",
            self.table,
            self.columns.join(", "),
            self.values.join(", "))
    }
}

pub fn insert_example() {
    let insert = insert_into::<UserTable>()
        .value(users::name(), "Alice")
        .value(users::email(), "alice@example.com")
        .value(users::active(), "true");
    
    println!("Insert: {}", insert.to_sql());
    // INSERT INTO users (name, email, active) VALUES ('Alice', 'alice@example.com', 'true')
}
Real-world impact:
  • Diesel ORM: Compile-time query validation, zero-cost abstractions
  • SeaORM: Async-friendly query builder with similar patterns
  • Prevents SQL injection: All values are parameterized
  • Type safety: Wrong column types caught at compile time
  • Refactoring safety: Schema changes break the build, not production

Example 3: State Machine DSL (Systems Programming)

State machines are common in protocol implementations, game engines, and workflow systems. A DSL makes them declarative and verifiable:

// State Machine DSL implementation
use std::collections::HashMap;
use std::fmt::{self, Display};

// Macro for declarative state machine definition
macro_rules! state_machine {
    (
        states: { $($state:ident),* $(,)? }
        events: { $($event:ident),* $(,)? }
        transitions: {
            $($from:ident + $on:ident => $to:ident),* $(,)?
        }
        $(initial: $initial:ident)?
    ) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        pub enum State {
            $($state),*
        }
        
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        pub enum Event {
            $($event),*
        }
        
        impl Display for State {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                match self {
                    $(State::$state => write!(f, stringify!($state))),*
                }
            }
        }
        
        impl Display for Event {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                match self {
                    $(Event::$event => write!(f, stringify!($event))),*
                }
            }
        }
        
        pub struct StateMachine {
            current_state: State,
            transitions: HashMap<(State, Event), State>,
            history: Vec<(State, Event, State)>,
        }
        
        impl StateMachine {
            pub fn new() -> Self {
                let mut transitions = HashMap::new();
                $(
                    transitions.insert((State::$from, Event::$on), State::$to);
                )*
                
                Self {
                    current_state: state_machine!(@initial $($initial)?),
                    transitions,
                    history: Vec::new(),
                }
            }
            
            pub fn current_state(&self) -> State {
                self.current_state
            }
            
            pub fn can_transition(&self, event: Event) -> bool {
                self.transitions.contains_key(&(self.current_state, event))
            }
            
            pub fn transition(&mut self, event: Event) -> Result<State, String> {
                let key = (self.current_state, event);
                
                if let Some(&next_state) = self.transitions.get(&key) {
                    let old_state = self.current_state;
                    self.current_state = next_state;
                    self.history.push((old_state, event, next_state));
                    Ok(next_state)
                } else {
                    Err(format!(
                        "Invalid transition: {} + {} (no transition defined)",
                        self.current_state, event
                    ))
                }
            }
            
            pub fn history(&self) -> &[(State, Event, State)] {
                &self.history
            }
            
            pub fn reset(&mut self) {
                self.current_state = state_machine!(@initial $($initial)?);
                self.history.clear();
            }
        }
    };
    
    (@initial) => { State::Idle };
    (@initial $initial:ident) => { State::$initial };
}

// Example 1: Connection state machine (like TCP)
mod connection_fsm {
    use super::*;
    
    state_machine! {
        states: { Closed, Listen, SynSent, SynReceived, Established, FinWait, CloseWait, Closing, LastAck, TimeWait }
        events: { Open, Send, Receive, Close, Timeout }
        transitions: {
            Closed + Open => Listen,
            Listen + Send => SynSent,
            SynSent + Receive => Established,
            Established + Close => FinWait,
            FinWait + Receive => TimeWait,
            TimeWait + Timeout => Closed,
            Established + Receive => CloseWait,
            CloseWait + Close => LastAck,
            LastAck + Receive => Closed,
        }
        initial: Closed
    }
    
    // Add domain-specific methods
    impl StateMachine {
        pub fn is_connected(&self) -> bool {
            self.current_state() == State::Established
        }
        
        pub fn is_closed(&self) -> bool {
            self.current_state() == State::Closed
        }
        
        pub fn can_send_data(&self) -> bool {
            matches!(self.current_state(), State::Established | State::CloseWait)
        }
    }
}

// Example 2: Order processing state machine
mod order_fsm {
    use super::*;
    
    state_machine! {
        states: { Draft, Pending, PaymentProcessing, Confirmed, Shipped, Delivered, Cancelled, Refunded }
        events: { Submit, PaymentReceived, PaymentFailed, Approve, Ship, Deliver, Cancel, Refund }
        transitions: {
            Draft + Submit => Pending,
            Pending + PaymentReceived => PaymentProcessing,
            PaymentProcessing + Approve => Confirmed,
            Confirmed + Ship => Shipped,
            Shipped + Deliver => Delivered,
            Pending + Cancel => Cancelled,
            PaymentProcessing + PaymentFailed => Cancelled,
            Confirmed + Cancel => Cancelled,
            Delivered + Refund => Refunded,
        }
        initial: Draft
    }
    
    // Domain-specific validations
    impl StateMachine {
        pub fn can_modify(&self) -> bool {
            matches!(self.current_state(), State::Draft | State::Pending)
        }
        
        pub fn can_cancel(&self) -> bool {
            !matches!(self.current_state(), 
                State::Shipped | State::Delivered | State::Cancelled | State::Refunded)
        }
        
        pub fn is_final_state(&self) -> bool {
            matches!(self.current_state(), 
                State::Delivered | State::Cancelled | State::Refunded)
        }
    }
}

// Example 3: Media player state machine
mod player_fsm {
    use super::*;
    
    state_machine! {
        states: { Stopped, Playing, Paused, Buffering, Error }
        events: { Play, Pause, Stop, Buffer, BufferComplete, ErrorOccurred, Recover }
        transitions: {
            Stopped + Play => Playing,
            Playing + Pause => Paused,
            Paused + Play => Playing,
            Playing + Stop => Stopped,
            Paused + Stop => Stopped,
            Playing + Buffer => Buffering,
            Buffering + BufferComplete => Playing,
            Playing + ErrorOccurred => Error,
            Paused + ErrorOccurred => Error,
            Buffering + ErrorOccurred => Error,
            Error + Recover => Stopped,
        }
        initial: Stopped
    }
}

// Advanced: State machine with guards and actions
pub trait StateMachineGuard<S, E> {
    fn can_transition(&self, from: S, event: E, to: S) -> bool;
}

pub trait StateMachineAction<S, E> {
    fn on_transition(&mut self, from: S, event: E, to: S);
}

pub struct GuardedStateMachine<S, E, G, A> 
where
    S: Copy + Eq + std::hash::Hash,
    E: Copy + Eq + std::hash::Hash,
    G: StateMachineGuard<S, E>,
    A: StateMachineAction<S, E>,
{
    current_state: S,
    transitions: HashMap<(S, E), S>,
    guard: G,
    action: A,
}

impl<S, E, G, A> GuardedStateMachine<S, E, G, A>
where
    S: Copy + Eq + std::hash::Hash + Display,
    E: Copy + Eq + std::hash::Hash + Display,
    G: StateMachineGuard<S, E>,
    A: StateMachineAction<S, E>,
{
    pub fn new(initial: S, transitions: HashMap<(S, E), S>, guard: G, action: A) -> Self {
        Self {
            current_state: initial,
            transitions,
            guard,
            action,
        }
    }
    
    pub fn transition(&mut self, event: E) -> Result<S, String> {
        let key = (self.current_state, event);
        
        if let Some(&next_state) = self.transitions.get(&key) {
            // Check guard condition
            if !self.guard.can_transition(self.current_state, event, next_state) {
                return Err(format!(
                    "Transition guard failed: {} + {} => {}",
                    self.current_state, event, next_state
                ));
            }
            
            let old_state = self.current_state;
            self.current_state = next_state;
            
            // Execute action
            self.action.on_transition(old_state, event, next_state);
            
            Ok(next_state)
        } else {
            Err(format!(
                "Invalid transition: {} + {}",
                self.current_state, event
            ))
        }
    }
}

// Real-world usage: Authentication flow with logging
use std::time::{SystemTime, UNIX_EPOCH};

mod auth_fsm {
    use super::*;
    
    state_machine! {
        states: { Unauthenticated, Authenticating, Authenticated, Locked }
        events: { Login, LoginSuccess, LoginFail, Logout, Lock }
        transitions: {
            Unauthenticated + Login => Authenticating,
            Authenticating + LoginSuccess => Authenticated,
            Authenticating + LoginFail => Unauthenticated,
            Authenticated + Logout => Unauthenticated,
            Authenticated + Lock => Locked,
            Unauthenticated + Lock => Locked,
        }
        initial: Unauthenticated
    }
    
    pub struct AuthGuard {
        failed_attempts: usize,
        max_attempts: usize,
    }
    
    impl AuthGuard {
        pub fn new(max_attempts: usize) -> Self {
            Self {
                failed_attempts: 0,
                max_attempts,
            }
        }
    }
    
    impl StateMachineGuard<State, Event> for AuthGuard {
        fn can_transition(&self, _from: State, event: Event, _to: State) -> bool {
            // Prevent login attempts if locked
            if event == Event::Login && self.failed_attempts >= self.max_attempts {
                return false;
            }
            true
        }
    }
    
    pub struct AuthLogger {
        logs: Vec<String>,
    }
    
    impl AuthLogger {
        pub fn new() -> Self {
            Self { logs: Vec::new() }
        }
        
        pub fn logs(&self) -> &[String] {
            &self.logs
        }
    }
    
    impl StateMachineAction<State, Event> for AuthLogger {
        fn on_transition(&mut self, from: State, event: Event, to: State) {
            let timestamp = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs();
            
            let log = format!(
                "[{}] Transition: {} --[{}]--> {}",
                timestamp, from, event, to
            );
            
            self.logs.push(log);
            println!("Auth event: {} -> {} ({})", from, event, to);
        }
    }
}

// Test the state machines
pub fn test_state_machines() {
    // Connection state machine
    let mut conn = connection_fsm::StateMachine::new();
    println!("Connection: Initial state = {}", conn.current_state());
    
    conn.transition(connection_fsm::Event::Open).unwrap();
    println!("After Open: {}", conn.current_state());
    assert_eq!(conn.current_state(), connection_fsm::State::Listen);
    
    // Order processing
    let mut order = order_fsm::StateMachine::new();
    println!("\nOrder: Initial state = {}", order.current_state());
    
    order.transition(order_fsm::Event::Submit).unwrap();
    assert!(order.can_modify());
    
    order.transition(order_fsm::Event::PaymentReceived).unwrap();
    assert!(!order.can_modify());
    
    // Invalid transition should fail
    let result = order.transition(order_fsm::Event::Ship);
    assert!(result.is_err());
    println!("Invalid transition caught: {:?}", result);
    
    // Media player
    let mut player = player_fsm::StateMachine::new();
    println!("\nPlayer: Initial state = {}", player.current_state());
    
    player.transition(player_fsm::Event::Play).unwrap();
    player.transition(player_fsm::Event::Pause).unwrap();
    player.transition(player_fsm::Event::Play).unwrap();
    player.transition(player_fsm::Event::Stop).unwrap();
    
    println!("Player history:");
    for (from, event, to) in player.history() {
        println!("  {} --[{}]--> {}", from, event, to);
    }
}
Real-world impact:
  • tokio-tower: Service state management in async systems
  • async-trait: Protocol state machines for network services
  • Game engines: Character FSMs, AI behavior trees
  • Hardware drivers: Device state tracking
  • Workflow engines: Business process automation

Example 4: Testing DSL (Testing/BDD)

Testing DSLs make tests more readable and maintainable, especially for BDD-style testing:

// BDD Testing DSL implementation
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::fmt::Display;

pub struct TestSuite {
    name: String,
    tests: Vec<Test>,
    before_each: Option<fn()>,
    after_each: Option<fn()>,
}

pub struct Test {
    description: String,
    test_fn: Box<dyn Fn() -> Result<(), String>>,
}

pub struct Expectation<T> {
    actual: T,
}

impl<T> Expectation<T> {
    pub fn new(actual: T) -> Self {
        Self { actual }
    }
}

impl<T: PartialEq + Display> Expectation<T> {
    pub fn to_equal(&self, expected: T) -> Result<(), String> {
        if self.actual == expected {
            Ok(())
        } else {
            Err(format!("Expected {:?}, got {:?}", expected, self.actual))
        }
    }
}

impl<T: PartialOrd + Display> Expectation<T> {
    pub fn to_be_greater_than(&self, expected: T) -> Result<(), String> {
        if self.actual > expected {
            Ok(())
        } else {
            Err(format!("Expected {} > {}", self.actual, expected))
        }
    }
    
    pub fn to_be_less_than(&self, expected: T) -> Result<(), String> {
        if self.actual < expected {
            Ok(())
        } else {
            Err(format!("Expected {} < {}", self.actual, expected))
        }
    }
}

impl<T> Expectation<Option<T>> {
    pub fn to_be_some(&self) -> Result<(), String> {
        if self.actual.is_some() {
            Ok(())
        } else {
            Err("Expected Some, got None".to_string())
        }
    }
    
    pub fn to_be_none(&self) -> Result<(), String> {
        if self.actual.is_none() {
            Ok(())
        } else {
            Err("Expected None, got Some".to_string())
        }
    }
}

impl<T, E: Display> Expectation<Result<T, E>> {
    pub fn to_be_ok(&self) -> Result<(), String> {
        match &self.actual {
            Ok(_) => Ok(()),
            Err(e) => Err(format!("Expected Ok, got Err({})", e)),
        }
    }
    
    pub fn to_be_err(&self) -> Result<(), String> {
        match &self.actual {
            Ok(_) => Err("Expected Err, got Ok".to_string()),
            Err(_) => Ok(()),
        }
    }
}

pub fn expect<T>(actual: T) -> Expectation<T> {
    Expectation::new(actual)
}

// Macro for describing test suites
macro_rules! describe {
    ($name:expr, $body:block) => {{
        let mut suite = TestSuite {
            name: $name.to_string(),
            tests: Vec::new(),
            before_each: None,
            after_each: None,
        };
        
        // Helper to add tests
        let mut it = |description: &str, test: fn()| {
            suite.tests.push(Test {
                description: description.to_string(),
                test_fn: Box::new(move || {
                    let result = catch_unwind(AssertUnwindSafe(test));
                    match result {
                        Ok(()) => Ok(()),
                        Err(e) => Err(format!("Test panicked: {:?}", e)),
                    }
                }),
            });
        };
        
        $body
        
        suite
    }};
}

// Real-world testing DSL example
mod calculator {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    pub fn divide(a: i32, b: i32) -> Result<i32, String> {
        if b == 0 {
            Err("Division by zero".to_string())
        } else {
            Ok(a / b)
        }
    }
    
    pub fn is_even(n: i32) -> bool {
        n % 2 == 0
    }
}

pub fn run_calculator_tests() {
    use calculator::*;
    
    // Test suite 1: Basic operations
    let suite1 = describe!("Calculator - Addition", {
        it("adds positive numbers", || {
            expect(add(2, 3)).to_equal(5).unwrap();
        });
        
        it("adds negative numbers", || {
            expect(add(-2, -3)).to_equal(-5).unwrap();
        });
        
        it("handles zero", || {
            expect(add(0, 5)).to_equal(5).unwrap();
        });
    });
    
    // Test suite 2: Division with error handling
    let suite2 = describe!("Calculator - Division", {
        it("divides evenly", || {
            let result = divide(10, 2);
            expect(result).to_be_ok().unwrap();
            expect(result.unwrap()).to_equal(5).unwrap();
        });
        
        it("handles division by zero", || {
            let result = divide(10, 0);
            expect(result).to_be_err().unwrap();
        });
    });
    
    // Test suite 3: Predicates
    let suite3 = describe!("Calculator - Even check", {
        it("identifies even numbers", || {
            assert!(is_even(2));
            assert!(is_even(0));
            assert!(is_even(-4));
        });
        
        it("identifies odd numbers", || {
            assert!(!is_even(1));
            assert!(!is_even(-3));
        });
    });
    
    // Run all test suites
    for suite in [suite1, suite2, suite3] {
        println!("\n{}", suite.name);
        for test in suite.tests {
            print!("  {} ... ", test.description);
            match (test.test_fn)() {
                Ok(()) => println!("✓"),
                Err(e) => println!("✗\n    {}", e),
            }
        }
    }
}

// Advanced: Given-When-Then DSL for BDD
pub struct Scenario<T> {
    context: T,
    description: String,
}

impl<T> Scenario<T> {
    pub fn given(description: &str, setup: impl FnOnce() -> T) -> Self {
        Self {
            context: setup(),
            description: format!("Given {}", description),
        }
    }
    
    pub fn when<U>(self, description: &str, action: impl FnOnce(T) -> U) -> Scenario<U> {
        Scenario {
            context: action(self.context),
            description: format!("{}\n  When {}", self.description, description),
        }
    }
    
    pub fn then(self, description: &str, assertion: impl FnOnce(T) -> bool) -> Result<(), String> {
        let desc = format!("{}\n  Then {}", self.description, description);
        
        if assertion(self.context) {
            Ok(())
        } else {
            Err(format!("Scenario failed:\n{}", desc))
        }
    }
}

// Real-world BDD example: User registration
#[derive(Clone)]
struct UserRepository {
    users: Vec<String>,
}

impl UserRepository {
    fn new() -> Self {
        Self { users: Vec::new() }
    }
    
    fn register(&mut self, email: &str) -> Result<(), String> {
        if self.users.contains(&email.to_string()) {
            Err("User already exists".to_string())
        } else {
            self.users.push(email.to_string());
            Ok(())
        }
    }
    
    fn is_registered(&self, email: &str) -> bool {
        self.users.contains(&email.to_string())
    }
}

pub fn test_user_registration_bdd() {
    // Scenario 1: Successful registration
    let result = Scenario::given("a new user registration system", || {
            UserRepository::new()
        })
        .when("a user registers with valid email", |mut repo| {
            repo.register("alice@example.com").ok();
            repo
        })
        .then("the user should be registered", |repo| {
            repo.is_registered("alice@example.com")
        });
    
    match result {
        Ok(()) => println!("✓ Scenario 1 passed"),
        Err(e) => println!("✗ Scenario 1 failed: {}", e),
    }
    
    // Scenario 2: Duplicate registration
    let result = Scenario::given("a user already registered", || {
            let mut repo = UserRepository::new();
            repo.register("bob@example.com").ok();
            repo
        })
        .when("the same user tries to register again", |mut repo| {
            let result = repo.register("bob@example.com");
            (repo, result)
        })
        .then("registration should fail", |(repo, result)| {
            result.is_err() && repo.is_registered("bob@example.com")
        });
    
    match result {
        Ok(()) => println!("✓ Scenario 2 passed"),
        Err(e) => println!("✗ Scenario 2 failed: {}", e),
    }
}

// Property-based testing DSL
pub struct PropertyTest<T> {
    name: String,
    generator: Box<dyn Fn(usize) -> T>,
    property: Box<dyn Fn(&T) -> bool>,
}

impl<T> PropertyTest<T> {
    pub fn new<G, P>(name: &str, generator: G, property: P) -> Self
    where
        G: Fn(usize) -> T + 'static,
        P: Fn(&T) -> bool + 'static,
    {
        Self {
            name: name.to_string(),
            generator: Box::new(generator),
            property: Box::new(property),
        }
    }
    
    pub fn run(&self, iterations: usize) -> Result<(), String> {
        for i in 0..iterations {
            let value = (self.generator)(i);
            if !(self.property)(&value) {
                return Err(format!(
                    "Property '{}' failed at iteration {}",
                    self.name, i
                ));
            }
        }
        Ok(())
    }
}

pub fn test_properties() {
    // Property: addition is commutative
    let test1 = PropertyTest::new(
        "addition is commutative",
        |i| ((i as i32) % 100, ((i * 7) as i32) % 100),
        |(a, b)| a + b == b + a,
    );
    
    match test1.run(1000) {
        Ok(()) => println!("✓ Property 1 holds"),
        Err(e) => println!("✗ Property 1 failed: {}", e),
    }
    
    // Property: string length is non-negative
    let test2 = PropertyTest::new(
        "string length is non-negative",
        |i| format!("test_{}", i),
        |s| s.len() > 0,
    );
    
    match test2.run(1000) {
        Ok(()) => println!("✓ Property 2 holds"),
        Err(e) => println!("✗ Property 2 failed: {}", e),
    }
}
Real-world impact:
  • cucumber-rs: Gherkin-style BDD testing
  • rstest: Fixture-based test organization
  • proptest: Property-based testing framework
  • Improves test readability: Non-technical stakeholders can understand tests
  • Reduces test boilerplate: Common patterns extracted

Example 5: Router DSL (Web Framework)

Web frameworks use routing DSLs to map HTTP endpoints to handlers elegantly:

// Router DSL implementation
use std::collections::HashMap;
use std::sync::Arc;

pub type Handler = Arc<dyn Fn(Request) -> Response + Send + Sync>;

#[derive(Clone)]
pub struct Request {
    pub method: String,
    pub path: String,
    pub params: HashMap<String, String>,
    pub body: String,
}

#[derive(Clone)]
pub struct Response {
    pub status: u16,
    pub body: String,
    pub headers: HashMap<String, String>,
}

impl Response {
    pub fn ok(body: impl Into<String>) -> Self {
        Self {
            status: 200,
            body: body.into(),
            headers: HashMap::new(),
        }
    }
    
    pub fn not_found() -> Self {
        Self {
            status: 404,
            body: "Not Found".to_string(),
            headers: HashMap::new(),
        }
    }
    
    pub fn json(body: impl Into<String>) -> Self {
        let mut headers = HashMap::new();
        headers.insert("Content-Type".to_string(), "application/json".to_string());
        
        Self {
            status: 200,
            body: body.into(),
            headers,
        }
    }
}

pub struct Router {
    routes: HashMap<(String, String), Handler>,
    scopes: Vec<String>,
}

impl Router {
    pub fn new() -> Self {
        Self {
            routes: HashMap::new(),
            scopes: Vec::new(),
        }
    }
    
    pub fn get(&mut self, path: &str, handler: Handler) {
        self.add_route("GET", path, handler);
    }
    
    pub fn post(&mut self, path: &str, handler: Handler) {
        self.add_route("POST", path, handler);
    }
    
    pub fn put(&mut self, path: &str, handler: Handler) {
        self.add_route("PUT", path, handler);
    }
    
    pub fn delete(&mut self, path: &str, handler: Handler) {
        self.add_route("DELETE", path, handler);
    }
    
    pub fn scope(&mut self, prefix: &str) -> ScopedRouter {
        ScopedRouter {
            router: self,
            prefix: prefix.to_string(),
        }
    }
    
    fn add_route(&mut self, method: &str, path: &str, handler: Handler) {
        let full_path = if self.scopes.is_empty() {
            path.to_string()
        } else {
            format!("{}{}", self.scopes.join(""), path)
        };
        
        self.routes.insert((method.to_string(), full_path), handler);
    }
    
    pub fn handle(&self, request: Request) -> Response {
        let key = (request.method.clone(), request.path.clone());
        
        if let Some(handler) = self.routes.get(&key) {
            handler(request)
        } else {
            // Try pattern matching for parameterized routes
            self.find_parameterized_route(request)
        }
    }
    
    fn find_parameterized_route(&self, mut request: Request) -> Response {
        for ((method, pattern), handler) in &self.routes {
            if method == &request.method {
                if let Some(params) = match_path(pattern, &request.path) {
                    request.params = params;
                    return handler(request);
                }
            }
        }
        
        Response::not_found()
    }
}

pub struct ScopedRouter<'a> {
    router: &'a mut Router,
    prefix: String,
}

impl<'a> ScopedRouter<'a> {
    pub fn get(self, path: &str, handler: Handler) -> Self {
        self.router.scopes.push(self.prefix.clone());
        self.router.add_route("GET", path, handler);
        self.router.scopes.pop();
        self
    }
    
    pub fn post(self, path: &str, handler: Handler) -> Self {
        self.router.scopes.push(self.prefix.clone());
        self.router.add_route("POST", path, handler);
        self.router.scopes.pop();
        self
    }
}

fn match_path(pattern: &str, path: &str) -> Option<HashMap<String, String>> {
    let pattern_parts: Vec<&str> = pattern.split('/').collect();
    let path_parts: Vec<&str> = path.split('/').collect();
    
    if pattern_parts.len() != path_parts.len() {
        return None;
    }
    
    let mut params = HashMap::new();
    
    for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
        if pattern_part.starts_with(':') {
            let param_name = &pattern_part[1..];
            params.insert(param_name.to_string(), path_part.to_string());
        } else if pattern_part != path_part {
            return None;
        }
    }
    
    Some(params)
}

// Macro for declarative route definition
macro_rules! routes {
    ($router:ident, {
        $( $method:ident $path:expr => $handler:expr ),* $(,)?
    }) => {
        $(
            routes!(@route $router, $method, $path, $handler);
        )*
    };
    
    (@route $router:ident, GET, $path:expr, $handler:expr) => {
        $router.get($path, Arc::new($handler));
    };
    (@route $router:ident, POST, $path:expr, $handler:expr) => {
        $router.post($path, Arc::new($handler));
    };
    (@route $router:ident, PUT, $path:expr, $handler:expr) => {
        $router.put($path, Arc::new($handler));
    };
    (@route $router:ident, DELETE, $path:expr, $handler:expr) => {
        $router.delete($path, Arc::new($handler));
    };
}

// Real-world usage: REST API
pub fn create_api_router() -> Router {
    let mut router = Router::new();
    
    routes!(router, {
        GET "/" => |_req| Response::ok("Welcome to the API"),
        GET "/health" => |_req| Response::ok("OK"),
        GET "/users" => list_users,
        GET "/users/:id" => get_user,
        POST "/users" => create_user,
        PUT "/users/:id" => update_user,
        DELETE "/users/:id" => delete_user,
    });
    
    // Scoped routes
    router.scope("/api/v1")
        .get("/status", Arc::new(|_| Response::json(r#"{"status": "running"}"#)))
        .post("/login", Arc::new(handle_login));
    
    router
}

fn list_users(_req: Request) -> Response {
    Response::json(r#"[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]"#)
}

fn get_user(req: Request) -> Response {
    if let Some(id) = req.params.get("id") {
        Response::json(format!(r#"{{"id": {}, "name": "User {}", "email": "user{}@example.com"}}"#, id, id, id))
    } else {
        Response::not_found()
    }
}

fn create_user(req: Request) -> Response {
    Response::json(format!(r#"{{"id": 3, "created": true, "body": "{}"}}"#, req.body))
}

fn update_user(req: Request) -> Response {
    if let Some(id) = req.params.get("id") {
        Response::json(format!(r#"{{"id": {}, "updated": true}}"#, id))
    } else {
        Response::not_found()
    }
}

fn delete_user(req: Request) -> Response {
    if let Some(id) = req.params.get("id") {
        Response::json(format!(r#"{{"id": {}, "deleted": true}}"#, id))
    } else {
        Response::not_found()
    }
}

fn handle_login(req: Request) -> Response {
    Response::json(r#"{"token": "abc123xyz"}"#)
}

// Test the router
pub fn test_router() {
    let router = create_api_router();
    
    // Test basic routes
    let resp1 = router.handle(Request {
        method: "GET".to_string(),
        path: "/".to_string(),
        params: HashMap::new(),
        body: String::new(),
    });
    println!("GET /: {} - {}", resp1.status, resp1.body);
    
    // Test parameterized route
    let resp2 = router.handle(Request {
        method: "GET".to_string(),
        path: "/users/42".to_string(),
        params: HashMap::new(),
        body: String::new(),
    });
    println!("GET /users/42: {} - {}", resp2.status, resp2.body);
    
    // Test POST
    let resp3 = router.handle(Request {
        method: "POST".to_string(),
        path: "/users".to_string(),
        params: HashMap::new(),
        body: r#"{"name": "Charlie"}"#.to_string(),
    });
    println!("POST /users: {} - {}", resp3.status, resp3.body);
    
    // Test 404
    let resp4 = router.handle(Request {
        method: "GET".to_string(),
        path: "/nonexistent".to_string(),
        params: HashMap::new(),
        body: String::new(),
    });
    println!("GET /nonexistent: {} - {}", resp4.status, resp4.body);
}
Real-world impact:
  • Rocket: Uses proc macros for route attributes #[get("/")]
  • Actix-web: Macro-based routing with type-safe extractors
  • Axum: Type-safe routing with handler inference
  • Warp: Filter-based composable routing DSL

Deep Dive Explanation

DSL Design Principles

Creating effective DSLs requires balancing expressiveness, safety, and maintainability:

1. Domain Alignment: The DSL syntax should mirror how domain experts think and communicate:
// Good: Reads like natural language
users.filter(active.eq(true))
     .order_by(name.asc())

// Bad: Generic and unclear
users.apply_condition("active", Operator::Equal, Value::Bool(true))
     .sort(SortKey::new("name"), Direction::Ascending)
2. Type Safety: Leverage Rust's type system to catch errors at compile time:
// Type-safe: Compiler ensures valid transitions
state_machine.transition(Event::Start)?; // OK
state_machine.transition(Event::Invalid)?; // Compile error

// Unsafe: Runtime validation only
state_machine.transition("start")?; // Could be typo
3. Composability: DSL elements should combine naturally:
// Composable: Filters chain elegantly
users.filter(active.eq(true))
     .filter(role.eq("admin"))
     .or_filter(permissions.contains("sudo"))

// Not composable: Can't chain conditions
let query = Query::new();
query.add_filter(active_filter);
query.add_or_filter(role_filter); // Confusing precedence
4. Error Messages: DSL errors should be clear and actionable:
// Good error message:
// error: Invalid state transition: Running -> Paused
//        Event 'Stop' cannot transition from 'Running'
//        Valid events from 'Running': Pause, Complete

// Bad error message:
// error: Transition failed

Internal DSLs with Macros

Macros are the primary tool for creating internal DSLs in Rust:

Declarative Macros (macro_rules!): Best for pattern-based DSLs:
macro_rules! html {
    // Match tags with attributes and children
    (<$tag:ident $($attr:ident = $val:expr)* > $($children:tt)*) => {{
        // Generate code that builds HTML node
        let node = HtmlNode::new(stringify!($tag));
        $(
            node.add_attr(stringify!($attr), $val);
        )*
        // Recursively process children
        $(
            node.add_child(html!($children));
        )*
        node
    }};
}
Procedural Macros: Best for complex parsing and validation:
// Parse SQL at compile time, validate against schema
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let sql_query = parse_sql(&input);
    validate_against_schema(&sql_query);
    generate_type_safe_query(&sql_query)
}

Type-State Pattern for Safety

Type-state uses Rust's type system to enforce state machines at compile time:

// Connection states encoded in types
struct Disconnected;
struct Connected;
struct Authenticated;

struct Connection<State> {
    state: PhantomData<State>,
    socket: Socket,
}

impl Connection<Disconnected> {
    pub fn new() -> Self {
        Self {
            state: PhantomData,
            socket: Socket::new(),
        }
    }
    
    pub fn connect(self) -> Connection<Connected> {
        self.socket.connect();
        Connection {
            state: PhantomData,
            socket: self.socket,
        }
    }
}

impl Connection<Connected> {
    pub fn authenticate(self, credentials: Credentials) -> Connection<Authenticated> {
        self.socket.send_auth(credentials);
        Connection {
            state: PhantomData,
            socket: self.socket,
        }
    }
    
    // Can't call send_message() on Connected state - compile error!
}

impl Connection<Authenticated> {
    pub fn send_message(&self, msg: &str) {
        self.socket.send(msg);
    }
}

// Usage enforces correct state transitions
let conn = Connection::new()      // Disconnected
    .connect()                    // Connected
    .authenticate(credentials);   // Authenticated

conn.send_message("Hello");       // OK
// conn.connect(); // Compile error: no method 'connect' on Authenticated

Builder Pattern for Fluency

Builder patterns create fluent DSL-like APIs:

pub struct QueryBuilder<'a> {
    table: &'a str,
    columns: Vec<&'a str>,
    conditions: Vec<String>,
    order: Option<String>,
}

impl<'a> QueryBuilder<'a> {
    pub fn new(table: &'a str) -> Self {
        Self {
            table,
            columns: vec!["*"],
            conditions: Vec::new(),
            order: None,
        }
    }
    
    pub fn select(mut self, columns: &[&'a str]) -> Self {
        self.columns = columns.to_vec();
        self
    }
    
    pub fn where_eq(mut self, column: &str, value: &str) -> Self {
        self.conditions.push(format!("{} = '{}'", column, value));
        self
    }
    
    pub fn order_by(mut self, column: &str, dir: &str) -> Self {
        self.order = Some(format!("{} {}", column, dir));
        self
    }
    
    pub fn build(self) -> String {
        let mut sql = format!("SELECT {} FROM {}", 
            self.columns.join(", "), 
            self.table);
        
        if !self.conditions.is_empty() {
            sql.push_str(&format!(" WHERE {}", self.conditions.join(" AND ")));
        }
        
        if let Some(order) = self.order {
            sql.push_str(&format!(" ORDER BY {}", order));
        }
        
        sql
    }
}

// Fluent usage
let sql = QueryBuilder::new("users")
    .select(&["id", "name", "email"])
    .where_eq("active", "true")
    .order_by("created_at", "DESC")
    .build();

Method Chaining Techniques

Method chaining is crucial for DSL fluency:

// Return Self for chaining
impl Builder {
    pub fn option1(mut self, value: T) -> Self {
        self.field1 = value;
        self
    }
    
    pub fn option2(mut self, value: U) -> Self {
        self.field2 = value;
        self
    }
}

// Return different types for type-state transitions
impl BuilderState1 {
    pub fn next(self) -> BuilderState2 {
        BuilderState2 { data: self.data }
    }
}

// Return &mut Self for borrowing chains
impl Builder {
    pub fn add_item(&mut self, item: Item) -> &mut Self {
        self.items.push(item);
        self
    }
}

Compile-Time Validation

Use Rust's type system and macros for compile-time checks:

// Const evaluation for validation
const fn validate_pattern(pattern: &str) -> bool {
    // Validate at compile time
    true
}

macro_rules! validated_pattern {
    ($pattern:expr) => {{
        const VALID: bool = validate_pattern($pattern);
        assert!(VALID, "Invalid pattern");
        $pattern
    }};
}

// Procedural macros can perform complex validation
#[derive(QueryBuilder)]
#[table = "users"]
struct User {
    id: i32,
    name: String,
}
// Macro validates table exists at compile time

Syntax Design Best Practices

1. Consistency: Use consistent naming and patterns:
// Good: Consistent verb names
query.filter().order_by().limit()

// Bad: Inconsistent verbs
query.where_clause().sort().with_limit()
2. Discoverability: IDE autocomplete should guide users:
// Good: Clear method names
builder.with_timeout(30).with_retries(3)

// Bad: Unclear abbreviations
builder.to(30).r(3)
3. Contextual Keywords: Use domain-specific terms:
// For HTML DSL
html! { <div class="container"> }

// For SQL DSL
query.select().from().where_()

// For state machine DSL
on(Event::Start).goto(State::Running)

When to Use

DSLs are powerful when applied to the right problems. Use DSLs when:

1. Complex Domain Logic: The domain has intricate rules that benefit from specialized syntax:
// State machines with many transitions
state_machine! {
    // 20+ states and transitions
    // DSL makes them manageable
}
2. Repetitive Patterns: Code follows the same structure repeatedly:
// Without DSL: Repetitive route definitions
router.add_route(Method::GET, "/users", list_users);
router.add_route(Method::GET, "/users/:id", get_user);
router.add_route(Method::POST, "/users", create_user);

// With DSL: Concise and clear
routes! {
    GET "/users" => list_users,
    GET "/users/:id" => get_user,
    POST "/users" => create_user,
}
3. Type Safety Critical: Compile-time validation prevents entire classes of errors:
// SQL DSL catches typos at compile time
users.filter(users::name.eq("Alice")) // OK
users.filter(users::nam.eq("Alice"))  // Compile error
4. Domain Expert Communication: Non-programmers need to understand or write code:
// BDD tests readable by product managers
describe! {
    given "a user is logged in"
    when "they view their profile"
    then "they see their account details"
}
5. Configuration as Code: Complex configurations benefit from programmatic validation:
// Infrastructure DSL
deployment! {
    service "api" {
        replicas: 3,
        port: 8080,
        health_check: "/health",
    }
    database "postgres" {
        version: "14",
        storage: "100GB",
    }
}

When NOT to Use

Avoid DSLs when they add more complexity than they solve:

1. Simple APIs: Basic operations don't need DSL overhead:
// Bad: DSL overkill for simple operation
config! {
    set timeout = 30
    set retries = 3
}

// Good: Simple API is clearer
let config = Config::new()
    .timeout(30)
    .retries(3);
2. One-Off Operations: DSL development cost exceeds usage benefit:
// Bad: Creating DSL for single use case
custom_format! {
    output "{date}: {message}"
}

// Good: Just use format macro
println!("{}: {}", date, message);
3. Unclear Scope: Domain boundaries are fuzzy or changing:
// Bad: DSL for unstable domain
workflow! {
    // Requirements change weekly
    // DSL needs constant updates
}

// Good: Wait for domain to stabilize
4. Learning Curve: Team unfamiliar with DSL concepts:
// Bad: Complex DSL for junior team
advanced_query_dsl! {
    // Requires understanding of:
    // - Macros
    // - Type-state
    // - GATs
    // - etc.
}

// Good: Simple, documented APIs
5. Poor Error Messages: DSL errors are cryptic:
// Bad: Macro expansion errors are unreadable
html! { <div> } // Error: unexpected token at line 234 of macro expansion

// Good: Use builder if errors aren't clear

⚠️ Anti-patterns

⚠️ Anti-pattern 1: Over-Complex Syntax

Problem: DSL tries to do too much, becomes unreadable:
// Bad: Too much magic
query! {
    SELECT * FROM users
    WHERE (age > 18 AND country = "US") OR admin = true
    JOIN orders ON users.id = orders.user_id
    GROUP BY country
    HAVING count(*) > 10
    ORDER BY created_at DESC
    LIMIT 100 OFFSET 50
}
// Parsing complexity, error messages unclear
Solution: Keep DSL focused, use builder for complexity:
// Good: Simple DSL + builder for complex cases
let query = users::table()
    .select_all()
    .filter(age.gt(18).and(country.eq("US")).or(admin.eq(true)))
    .join(orders::table(), users::id.eq(orders::user_id))
    .group_by(country)
    .having(count().gt(10))
    .order_by(created_at.desc())
    .limit(100)
    .offset(50);

⚠️ Anti-pattern 2: Poor Error Messages

Problem: Compile errors from DSL are incomprehensible:
// Bad: Macro expansion errors
html! {
    <div class="container">
        <p> Missing closing tag
    </div>
}
// Error: recursion limit reached while expanding macro
//        expected `>`, found `}`
//        help: consider adding a `#![recursion_limit="256"]` attribute
Solution: Add validation layers with clear errors:
// Good: Validate input early, provide context
#[proc_macro]
pub fn html(input: TokenStream) -> TokenStream {
    match parse_html(&input) {
        Ok(ast) => generate_code(ast),
        Err(HtmlError::UnclosedTag { tag, line }) => {
            compile_error!("Unclosed HTML tag '{}' at line {}", tag, line)
        }
        Err(HtmlError::InvalidAttribute { attr, line }) => {
            compile_error!("Invalid attribute '{}' at line {}", attr, line)
        }
    }
}

⚠️ Anti-pattern 3: Mixing Abstraction Levels

Problem: DSL mixes high-level and low-level concepts:
// Bad: Mixing HTTP details with business logic
routes! {
    GET "/users" => {
        set_header("Cache-Control", "max-age=3600");
        let users = db.query("SELECT * FROM users");
        Response::new(200, serialize(users))
    },
}
Solution: Separate concerns, consistent abstraction:
// Good: Clean separation
routes! {
    GET "/users" => list_users,
}

#[cached(ttl = 3600)]
fn list_users() -> Json<Vec<User>> {
    User::all()
}

⚠️ Anti-pattern 4: Inflexible Design

Problem: DSL doesn't allow escape hatches for edge cases:
// Bad: No way to handle special cases
state_machine! {
    // Can only define simple transitions
    // No way to add guards, actions, or conditions
}
Solution: Provide extension points:
// Good: Extensible design
state_machine! {
    transitions: { /* ... */ }
    
    guards: {
        Running -> Paused if |ctx| ctx.can_pause(),
    }
    
    actions: {
        on_enter(Running) => |ctx| ctx.start_timer(),
        on_exit(Running) => |ctx| ctx.stop_timer(),
    }
}

⚠️ Anti-pattern 5: Insufficient Testing

Problem: DSL code paths not thoroughly tested:
// Bad: Only test happy path
#[test]
fn test_dsl() {
    let result = my_dsl! { simple case };
    assert!(result.is_ok());
}
Solution: Comprehensive test coverage:
// Good: Test all scenarios
#[test]
fn test_dsl_happy_path() { /* ... */ }

#[test]
fn test_dsl_edge_cases() { /* ... */ }

#[test]
fn test_dsl_error_messages() {
    let result = my_dsl! { invalid syntax };
    assert!(result.is_err());
    assert!(result.unwrap_err().contains("expected ';'"));
}

#[test]
fn test_dsl_compile_errors() {
    // Use trybuild to test compile-time errors
}

Performance Characteristics

Compile-Time Overhead

DSLs can increase compilation time:

// Macro expansion overhead
html! {
    // Large macro invocation
    // Expands to thousands of lines
    // Slows down compilation
}

// Mitigation: Use incremental compilation
// Mitigation: Extract common patterns to functions
// Mitigation: Consider proc macros for complex logic
Typical overhead:
  • Declarative macros: 5-20% compile time increase
  • Procedural macros: 10-50% compile time increase
  • Type-state patterns: Minimal (zero-cost abstraction)

Runtime Cost

Well-designed DSLs have zero runtime overhead:

// DSL code:
let query = users.filter(active.eq(true)).select(name);

// Compiles to same code as:
let query = Query::new("users")
    .add_filter("active", Operator::Eq, true)
    .add_select("name");

// Both produce identical machine code (zero-cost abstraction)
Performance comparison:
// Benchmark results (example)
// Direct API:      100 ns/iter
// Builder DSL:     100 ns/iter  (0% overhead)
// Macro DSL:       100 ns/iter  (0% overhead)
// Type-state DSL:  100 ns/iter  (0% overhead)

// Runtime overhead is typically zero for well-designed DSLs

Code Generation Size

DSLs can affect binary size:

// Monomorphization bloat
fn generic_dsl<T: Query>(query: T) {
    // Generates separate code for each T
}

// Mitigation: Use trait objects for large DSLs
fn dynamic_dsl(query: &dyn Query) {
    // Single code path
}

Comparison with Direct API Calls

| Aspect | DSL | Direct API | Winner |

|--------|-----|------------|---------|

| Readability | High | Medium | DSL |

| Type safety | High | Medium | DSL |

| Compile time | Slower | Faster | Direct |

| Runtime performance | Same | Same | Tie |

| Binary size | Larger | Smaller | Direct |

| Learning curve | Steeper | Gentler | Direct |

| Refactoring | Easier | Harder | DSL |

Exercises

Beginner: Router DSL for Web Framework

Task: Create a basic routing DSL for a web server.
// Implement this DSL:
let router = routes! {
    GET "/" => home,
    GET "/about" => about,
    POST "/contact" => contact,
};

// Requirements:
// 1. Support GET, POST, PUT, DELETE
// 2. Parse path and method
// 3. Map to handler functions
// 4. Handle 404 for unknown routes

// Starter code:
macro_rules! routes {
    // Your implementation here
}
Solution approach:
  1. Create Router struct with HashMap<(Method, Path), Handler>
  2. Parse macro input to extract method, path, handler
  3. Generate router.add_route() calls for each entry
  4. Implement Router::handle() to dispatch requests

Intermediate: Validation DSL with Type-State

Task: Build a form validation DSL with compile-time guarantees.
// Implement type-safe validation:
let form = validate! {
    field "email" as String {
        required,
        email_format,
        max_length(255),
    }
    field "age" as u32 {
        required,
        min_value(18),
        max_value(120),
    }
    field "bio" as Option<String> {
        max_length(1000),
    }
};

// Requirements:
// 1. Type-state ensures required fields validated
// 2. Validators specific to field types
// 3. Compile error if invalid validator used
// 4. Generate validation function

// Starter code:
pub struct FieldValidator<T, State> {
    // Your implementation here
}
Solution approach:
  1. Use PhantomData for type-state pattern
  2. Define Unvalidated/Validated states
  3. Each validator returns next state
  4. Only Validated state can call .finish()
  5. Use traits to restrict validators by type

Advanced: Async Workflow DSL

Task: Create a DSL for defining async workflows with dependencies.
// Implement async workflow DSL:
workflow! {
    task fetch_user(user_id: u64) -> User {
        database::get_user(user_id).await
    }
    
    task fetch_posts(user: User) -> Vec<Post> {
        depends_on: fetch_user,
        database::get_posts(user.id).await
    }
    
    task fetch_comments(posts: Vec<Post>) -> Vec<Comment> {
        depends_on: fetch_posts,
        let post_ids: Vec<_> = posts.iter().map(|p| p.id).collect();
        database::get_comments(&post_ids).await
    }
    
    parallel {
        fetch_posts,
        fetch_profile(user: User) -> Profile {
            depends_on: fetch_user,
            database::get_profile(user.id).await
        }
    }
}

// Requirements:
// 1. Parse task definitions with dependencies
// 2. Build dependency graph
// 3. Execute tasks in correct order
// 4. Parallelize independent tasks
// 5. Handle errors gracefully
// 6. Generate type-safe async code

// Hint: Use topological sort for dependencies
// Hint: Use tokio::spawn for parallelization
Solution approach:
  1. Parse workflow into AST
  2. Build dependency graph
  3. Topological sort for execution order
  4. Generate tokio::join! for parallel tasks
  5. Use Result propagation for errors
  6. Type-check dependencies at compile time

Real-World Usage

Rocket Web Framework

Rocket uses attribute macros for routing:

#[get("/users/<id>")]
fn get_user(id: u64) -> Json<User> {
    // Handler implementation
}

#[post("/users", data = "<user>")]
fn create_user(user: Json<NewUser>) -> Status {
    // Handler implementation
}

// DSL generates route registration code
// Type-safe parameter extraction
// Automatic serialization/deserialization

Diesel Query Builder

Diesel provides compile-time SQL validation:

use diesel::prelude::*;

// Schema definition (generated from database)
table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
        email -> Varchar,
        created_at -> Timestamp,
    }
}

// Type-safe queries
let results = users::table
    .filter(users::name.like("%Alice%"))
    .order(users::created_at.desc())
    .limit(10)
    .load::<User>(&mut conn)?;

// Compiler validates:
// - Table exists
// - Columns exist
// - Types match
// - SQL is valid

Serde Data Formats

Serde uses derive macros for serialization DSL:

#[derive(Serialize, Deserialize)]
struct User {
    id: u64,
    name: String,
    
    #[serde(rename = "emailAddress")]
    email: String,
    
    #[serde(skip_serializing_if = "Option::is_none")]
    phone: Option<String>,
    
    #[serde(default)]
    active: bool,
}

// DSL generates serialization code for any format
let json = serde_json::to_string(&user)?;
let yaml = serde_yaml::to_string(&user)?;
let toml = toml::to_string(&user)?;

Tokio Select Macro

Tokio's select! macro is a concurrency DSL:

use tokio::select;

select! {
    result = async_operation_1() => {
        println!("Operation 1 completed: {:?}", result);
    }
    result = async_operation_2() => {
        println!("Operation 2 completed: {:?}", result);
    }
    _ = tokio::time::sleep(Duration::from_secs(5)) => {
        println!("Timeout!");
    }
}

// DSL generates:
// - Future polling
// - Cancellation of unselected branches
// - Efficient runtime integration

Yew HTML Macro

Yew's html! macro for web UIs:

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    let counter = use_state(|| 0);
    let onclick = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1))
    };
    
    html! {
        <div>
            <h1>{ "Counter App" }</h1>
            <button {onclick}>{ "+1" }</button>
            <p>{ "Count: " }{ *counter }</p>
        </div>
    }
}

// DSL generates virtual DOM code
// Type-safe component composition
// Efficient diffing and updates

Further Reading

Books and Papers

  • "Domain-Specific Languages" by Martin Fowler - Comprehensive DSL design guide
  • "The Rust Programming Language" - Macros Chapter - Official Rust macro documentation
  • "Implementing Domain-Specific Languages with Xtext and Xtend" - DSL implementation patterns

Rust Resources

  • The Little Book of Rust Macros - Deep dive into Rust macro system
  • Proc-macro Workshop - Hands-on procedural macro tutorial
  • syn and quote documentation - Essential tools for proc macros

Articles

  • "Internal vs External DSLs" - Fowler's comparison of DSL approaches
  • "The Power of Type-State Programming" - Using types to encode state machines
  • "Zero-Cost Abstractions in Rust" - How DSLs can be performant

Example Projects

  • diesel - Type-safe query builder
  • rocket - Web framework with routing DSL
  • yew - Frontend framework with HTML DSL
  • serde - Serialization DSL
  • tokio - Async runtime with select! macro

Tools and Libraries

  • syn - Rust parser for procedural macros
  • quote - Code generation for macros
  • darling - Attribute parsing for derive macros
  • nom - Parser combinators for custom parsers
  • pest - PEG parser generator

---

Next Steps: After mastering DSL creation, explore:
  1. Parser Combinators - Build external DSLs
  2. Compiler Design - Understand parsing and code generation
  3. Type Theory - Leverage advanced type system features
  4. Language Design - Study PL principles for better DSLs

DSLs are one of Rust's most powerful features, enabling you to create intuitive, type-safe APIs that feel like custom languages while maintaining all of Rust's safety guarantees. Master DSL creation to build frameworks and libraries that developers love to use.

🎮 Try it Yourself

🎮

DSL Creation - Playground

Run this code in the official Rust Playground