Domain-specific languages in Rust
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:
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('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
// 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:
html! macro for React-like componentsDatabase 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:
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:
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:
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:
#[get("/")]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
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 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 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 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
}
}
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
// 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)
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",
}
}
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
// 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);
// 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)
}
}
}
// 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()
}
// 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(),
}
}
// 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
}
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:
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
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
}
| 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 |
// 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:
// 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:
// 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:
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 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 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'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'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
---
Next Steps: After mastering DSL creation, explore: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.
Run this code in the official Rust Playground