Encapsulate requests as objects with undo/redo support
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
Key concepts:FnOnce, FnMut, Fn traits provide built-in command abstractions// Simple command trait
pub trait Command {
fn execute(&mut self);
fn undo(&mut self);
}
// Or using closures for simple cases
type SimpleCommand = Box<dyn FnMut()>;
A full-featured text editor command system supporting undo, redo, and macro recording.
use std::collections::VecDeque;
/// The document being edited (Receiver)
#[derive(Debug, Clone)]
pub struct Document {
content: String,
cursor: usize,
selection: Option<(usize, usize)>,
}
impl Document {
pub fn new() -> Self {
Self {
content: String::new(),
cursor: 0,
selection: None,
}
}
pub fn content(&self) -> &str {
&self.content
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn insert_at(&mut self, pos: usize, text: &str) {
self.content.insert_str(pos, text);
}
pub fn delete_range(&mut self, start: usize, end: usize) -> String {
let removed: String = self.content[start..end].to_string();
self.content.replace_range(start..end, "");
removed
}
pub fn set_cursor(&mut self, pos: usize) {
self.cursor = pos.min(self.content.len());
}
pub fn select(&mut self, start: usize, end: usize) {
self.selection = Some((start.min(end), start.max(end)));
}
pub fn clear_selection(&mut self) {
self.selection = None;
}
}
impl Default for Document {
fn default() -> Self {
Self::new()
}
}
/// Command trait with undo support
pub trait Command: std::fmt::Debug {
fn execute(&mut self, doc: &mut Document);
fn undo(&mut self, doc: &mut Document);
fn description(&self) -> &str;
}
/// Insert text command
#[derive(Debug)]
pub struct InsertCommand {
position: usize,
text: String,
}
impl InsertCommand {
pub fn new(position: usize, text: impl Into<String>) -> Self {
Self {
position,
text: text.into(),
}
}
}
impl Command for InsertCommand {
fn execute(&mut self, doc: &mut Document) {
doc.insert_at(self.position, &self.text);
doc.set_cursor(self.position + self.text.len());
}
fn undo(&mut self, doc: &mut Document) {
doc.delete_range(self.position, self.position + self.text.len());
doc.set_cursor(self.position);
}
fn description(&self) -> &str {
"Insert text"
}
}
/// Delete text command
#[derive(Debug)]
pub struct DeleteCommand {
start: usize,
end: usize,
deleted_text: Option<String>,
}
impl DeleteCommand {
pub fn new(start: usize, end: usize) -> Self {
Self {
start,
end,
deleted_text: None,
}
}
}
impl Command for DeleteCommand {
fn execute(&mut self, doc: &mut Document) {
self.deleted_text = Some(doc.delete_range(self.start, self.end));
doc.set_cursor(self.start);
}
fn undo(&mut self, doc: &mut Document) {
if let Some(ref text) = self.deleted_text {
doc.insert_at(self.start, text);
doc.set_cursor(self.start + text.len());
}
}
fn description(&self) -> &str {
"Delete text"
}
}
/// Replace text command (for find & replace)
#[derive(Debug)]
pub struct ReplaceCommand {
start: usize,
end: usize,
new_text: String,
old_text: Option<String>,
}
impl ReplaceCommand {
pub fn new(start: usize, end: usize, new_text: impl Into<String>) -> Self {
Self {
start,
end,
new_text: new_text.into(),
old_text: None,
}
}
}
impl Command for ReplaceCommand {
fn execute(&mut self, doc: &mut Document) {
self.old_text = Some(doc.delete_range(self.start, self.end));
doc.insert_at(self.start, &self.new_text);
doc.set_cursor(self.start + self.new_text.len());
}
fn undo(&mut self, doc: &mut Document) {
if let Some(ref old) = self.old_text {
doc.delete_range(self.start, self.start + self.new_text.len());
doc.insert_at(self.start, old);
doc.set_cursor(self.start + old.len());
}
}
fn description(&self) -> &str {
"Replace text"
}
}
/// Composite command for grouping multiple commands
#[derive(Debug)]
pub struct MacroCommand {
name: String,
commands: Vec<Box<dyn Command>>,
}
impl MacroCommand {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
commands: Vec::new(),
}
}
pub fn add(&mut self, command: Box<dyn Command>) {
self.commands.push(command);
}
}
impl Command for MacroCommand {
fn execute(&mut self, doc: &mut Document) {
for cmd in &mut self.commands {
cmd.execute(doc);
}
}
fn undo(&mut self, doc: &mut Document) {
// Undo in reverse order
for cmd in self.commands.iter_mut().rev() {
cmd.undo(doc);
}
}
fn description(&self) -> &str {
&self.name
}
}
/// Command history manager (Invoker)
pub struct CommandHistory {
undo_stack: Vec<Box<dyn Command>>,
redo_stack: Vec<Box<dyn Command>>,
max_history: usize,
}
impl CommandHistory {
pub fn new(max_history: usize) -> Self {
Self {
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_history,
}
}
pub fn execute(&mut self, mut command: Box<dyn Command>, doc: &mut Document) {
command.execute(doc);
self.undo_stack.push(command);
self.redo_stack.clear(); // Clear redo stack on new command
// Limit history size
while self.undo_stack.len() > self.max_history {
self.undo_stack.remove(0);
}
}
pub fn undo(&mut self, doc: &mut Document) -> Option<&str> {
if let Some(mut command) = self.undo_stack.pop() {
command.undo(doc);
let desc = command.description();
self.redo_stack.push(command);
Some(desc)
} else {
None
}
}
pub fn redo(&mut self, doc: &mut Document) -> Option<&str> {
if let Some(mut command) = self.redo_stack.pop() {
command.execute(doc);
let desc = command.description();
self.undo_stack.push(command);
Some(desc)
} else {
None
}
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn undo_description(&self) -> Option<&str> {
self.undo_stack.last().map(|c| c.description())
}
pub fn redo_description(&self) -> Option<&str> {
self.redo_stack.last().map(|c| c.description())
}
}
/// The text editor (Client)
pub struct TextEditor {
document: Document,
history: CommandHistory,
}
impl TextEditor {
pub fn new() -> Self {
Self {
document: Document::new(),
history: CommandHistory::new(100),
}
}
pub fn insert(&mut self, position: usize, text: &str) {
let command = Box::new(InsertCommand::new(position, text));
self.history.execute(command, &mut self.document);
}
pub fn delete(&mut self, start: usize, end: usize) {
let command = Box::new(DeleteCommand::new(start, end));
self.history.execute(command, &mut self.document);
}
pub fn replace(&mut self, start: usize, end: usize, new_text: &str) {
let command = Box::new(ReplaceCommand::new(start, end, new_text));
self.history.execute(command, &mut self.document);
}
pub fn undo(&mut self) -> bool {
self.history.undo(&mut self.document).is_some()
}
pub fn redo(&mut self) -> bool {
self.history.redo(&mut self.document).is_some()
}
pub fn content(&self) -> &str {
self.document.content()
}
}
impl Default for TextEditor {
fn default() -> Self {
Self::new()
}
}
fn main() {
let mut editor = TextEditor::new();
// Type some text
editor.insert(0, "Hello");
println!("After insert 'Hello': '{}'", editor.content());
editor.insert(5, " World");
println!("After insert ' World': '{}'", editor.content());
editor.insert(11, "!");
println!("After insert '!': '{}'", editor.content());
// Undo
editor.undo();
println!("After undo: '{}'", editor.content());
editor.undo();
println!("After undo: '{}'", editor.content());
// Redo
editor.redo();
println!("After redo: '{}'", editor.content());
// Replace
editor.replace(6, 11, "Rust");
println!("After replace: '{}'", editor.content());
// Delete
editor.delete(0, 6);
println!("After delete: '{}'", editor.content());
// Undo all
while editor.undo() {
println!("Undo: '{}'", editor.content());
}
}
A transactional command system for database operations.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
/// Simple in-memory database for demonstration
#[derive(Debug, Clone)]
pub struct Database {
tables: HashMap<String, Table>,
}
#[derive(Debug, Clone)]
pub struct Table {
rows: HashMap<u64, Row>,
next_id: u64,
}
#[derive(Debug, Clone)]
pub struct Row {
id: u64,
data: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Int(i64),
Float(f64),
Text(String),
Bool(bool),
}
impl Database {
pub fn new() -> Self {
Self {
tables: HashMap::new(),
}
}
pub fn create_table(&mut self, name: &str) {
self.tables.insert(
name.to_string(),
Table {
rows: HashMap::new(),
next_id: 1,
},
);
}
pub fn get_table(&self, name: &str) -> Option<&Table> {
self.tables.get(name)
}
pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
self.tables.get_mut(name)
}
}
impl Default for Database {
fn default() -> Self {
Self::new()
}
}
/// Database command trait
pub trait DbCommand: std::fmt::Debug + Send {
fn execute(&mut self, db: &mut Database) -> Result<(), String>;
fn rollback(&mut self, db: &mut Database) -> Result<(), String>;
fn description(&self) -> String;
}
/// Insert row command
#[derive(Debug)]
pub struct InsertRowCommand {
table: String,
data: HashMap<String, Value>,
inserted_id: Option<u64>,
}
impl InsertRowCommand {
pub fn new(table: impl Into<String>, data: HashMap<String, Value>) -> Self {
Self {
table: table.into(),
data,
inserted_id: None,
}
}
}
impl DbCommand for InsertRowCommand {
fn execute(&mut self, db: &mut Database) -> Result<(), String> {
let table = db
.get_table_mut(&self.table)
.ok_or_else(|| format!("Table '{}' not found", self.table))?;
let id = table.next_id;
table.next_id += 1;
table.rows.insert(
id,
Row {
id,
data: self.data.clone(),
},
);
self.inserted_id = Some(id);
Ok(())
}
fn rollback(&mut self, db: &mut Database) -> Result<(), String> {
if let Some(id) = self.inserted_id {
if let Some(table) = db.get_table_mut(&self.table) {
table.rows.remove(&id);
}
}
Ok(())
}
fn description(&self) -> String {
format!("INSERT INTO {}", self.table)
}
}
/// Update row command
#[derive(Debug)]
pub struct UpdateRowCommand {
table: String,
id: u64,
new_data: HashMap<String, Value>,
old_data: Option<HashMap<String, Value>>,
}
impl UpdateRowCommand {
pub fn new(
table: impl Into<String>,
id: u64,
new_data: HashMap<String, Value>,
) -> Self {
Self {
table: table.into(),
id,
new_data,
old_data: None,
}
}
}
impl DbCommand for UpdateRowCommand {
fn execute(&mut self, db: &mut Database) -> Result<(), String> {
let table = db
.get_table_mut(&self.table)
.ok_or_else(|| format!("Table '{}' not found", self.table))?;
let row = table
.rows
.get_mut(&self.id)
.ok_or_else(|| format!("Row {} not found", self.id))?;
// Save old data for rollback
self.old_data = Some(row.data.clone());
// Apply updates
for (key, value) in &self.new_data {
row.data.insert(key.clone(), value.clone());
}
Ok(())
}
fn rollback(&mut self, db: &mut Database) -> Result<(), String> {
if let Some(ref old_data) = self.old_data {
if let Some(table) = db.get_table_mut(&self.table) {
if let Some(row) = table.rows.get_mut(&self.id) {
row.data = old_data.clone();
}
}
}
Ok(())
}
fn description(&self) -> String {
format!("UPDATE {} WHERE id = {}", self.table, self.id)
}
}
/// Delete row command
#[derive(Debug)]
pub struct DeleteRowCommand {
table: String,
id: u64,
deleted_row: Option<Row>,
}
impl DeleteRowCommand {
pub fn new(table: impl Into<String>, id: u64) -> Self {
Self {
table: table.into(),
id,
deleted_row: None,
}
}
}
impl DbCommand for DeleteRowCommand {
fn execute(&mut self, db: &mut Database) -> Result<(), String> {
let table = db
.get_table_mut(&self.table)
.ok_or_else(|| format!("Table '{}' not found", self.table))?;
self.deleted_row = table.rows.remove(&self.id);
if self.deleted_row.is_none() {
return Err(format!("Row {} not found", self.id));
}
Ok(())
}
fn rollback(&mut self, db: &mut Database) -> Result<(), String> {
if let Some(ref row) = self.deleted_row {
if let Some(table) = db.get_table_mut(&self.table) {
table.rows.insert(row.id, row.clone());
}
}
Ok(())
}
fn description(&self) -> String {
format!("DELETE FROM {} WHERE id = {}", self.table, self.id)
}
}
/// Transaction manager
pub struct Transaction {
commands: Vec<Box<dyn DbCommand>>,
executed: Vec<Box<dyn DbCommand>>,
}
impl Transaction {
pub fn new() -> Self {
Self {
commands: Vec::new(),
executed: Vec::new(),
}
}
pub fn add(&mut self, command: Box<dyn DbCommand>) {
self.commands.push(command);
}
pub fn commit(&mut self, db: &mut Database) -> Result<(), String> {
// Execute all commands
while let Some(mut cmd) = self.commands.pop() {
match cmd.execute(db) {
Ok(()) => {
println!("Executed: {}", cmd.description());
self.executed.push(cmd);
}
Err(e) => {
// Rollback executed commands on failure
self.rollback(db)?;
return Err(format!("Transaction failed: {}", e));
}
}
}
self.executed.clear();
Ok(())
}
pub fn rollback(&mut self, db: &mut Database) -> Result<(), String> {
// Rollback in reverse order
while let Some(mut cmd) = self.executed.pop() {
println!("Rolling back: {}", cmd.description());
cmd.rollback(db)?;
}
Ok(())
}
}
impl Default for Transaction {
fn default() -> Self {
Self::new()
}
}
fn main() {
let mut db = Database::new();
db.create_table("users");
// Successful transaction
let mut tx = Transaction::new();
let mut user_data = HashMap::new();
user_data.insert("name".to_string(), Value::Text("Alice".to_string()));
user_data.insert("age".to_string(), Value::Int(30));
tx.add(Box::new(InsertRowCommand::new("users", user_data)));
let mut user_data2 = HashMap::new();
user_data2.insert("name".to_string(), Value::Text("Bob".to_string()));
user_data2.insert("age".to_string(), Value::Int(25));
tx.add(Box::new(InsertRowCommand::new("users", user_data2)));
match tx.commit(&mut db) {
Ok(()) => println!("Transaction committed successfully"),
Err(e) => println!("Transaction failed: {}", e),
}
println!("Users table: {:?}", db.get_table("users"));
// Transaction with failure (demonstrating rollback)
let mut tx2 = Transaction::new();
let mut update = HashMap::new();
update.insert("age".to_string(), Value::Int(31));
tx2.add(Box::new(UpdateRowCommand::new("users", 1, update)));
// This will fail - non-existent row
tx2.add(Box::new(DeleteRowCommand::new("users", 999)));
match tx2.commit(&mut db) {
Ok(()) => println!("Transaction 2 committed"),
Err(e) => println!("Transaction 2 failed (expected): {}", e),
}
// Original data should be preserved due to rollback
println!("Users after rollback: {:?}", db.get_table("users"));
}
A command system for game input with replay support.
use std::collections::VecDeque;
use std::time::Duration;
/// Game state
#[derive(Debug, Clone)]
pub struct GameState {
player_x: f32,
player_y: f32,
health: i32,
score: u64,
inventory: Vec<String>,
frame: u64,
}
impl GameState {
pub fn new() -> Self {
Self {
player_x: 0.0,
player_y: 0.0,
health: 100,
score: 0,
inventory: Vec::new(),
frame: 0,
}
}
}
impl Default for GameState {
fn default() -> Self {
Self::new()
}
}
/// Input command enum - more efficient than trait objects
#[derive(Debug, Clone)]
pub enum GameCommand {
Move { dx: f32, dy: f32 },
Jump { height: f32 },
Attack { direction: f32 },
UseItem { item_index: usize },
PickupItem { item: String },
DropItem { item_index: usize },
Save,
Pause,
}
/// Timestamped command for replay
#[derive(Debug, Clone)]
pub struct TimestampedCommand {
pub frame: u64,
pub command: GameCommand,
}
impl GameCommand {
pub fn execute(&self, state: &mut GameState) -> CommandResult {
match self {
GameCommand::Move { dx, dy } => {
let old_x = state.player_x;
let old_y = state.player_y;
state.player_x += dx;
state.player_y += dy;
CommandResult::Moved {
from: (old_x, old_y),
to: (state.player_x, state.player_y),
}
}
GameCommand::Jump { height } => {
// Simulate jump
CommandResult::Jumped { height: *height }
}
GameCommand::Attack { direction } => {
// Simulate attack
state.score += 10;
CommandResult::Attacked {
direction: *direction,
hit: true,
}
}
GameCommand::UseItem { item_index } => {
if *item_index < state.inventory.len() {
let item = state.inventory.remove(*item_index);
if item == "health_potion" {
state.health = (state.health + 25).min(100);
}
CommandResult::ItemUsed { item }
} else {
CommandResult::Failed {
reason: "Invalid item index".to_string(),
}
}
}
GameCommand::PickupItem { item } => {
state.inventory.push(item.clone());
CommandResult::ItemPickedUp { item: item.clone() }
}
GameCommand::DropItem { item_index } => {
if *item_index < state.inventory.len() {
let item = state.inventory.remove(*item_index);
CommandResult::ItemDropped { item }
} else {
CommandResult::Failed {
reason: "Invalid item index".to_string(),
}
}
}
GameCommand::Save => CommandResult::Saved,
GameCommand::Pause => CommandResult::Paused,
}
}
}
#[derive(Debug, Clone)]
pub enum CommandResult {
Moved { from: (f32, f32), to: (f32, f32) },
Jumped { height: f32 },
Attacked { direction: f32, hit: bool },
ItemUsed { item: String },
ItemPickedUp { item: String },
ItemDropped { item: String },
Saved,
Paused,
Failed { reason: String },
}
/// Command queue for processing inputs
pub struct CommandQueue {
queue: VecDeque<TimestampedCommand>,
history: Vec<TimestampedCommand>,
max_history: usize,
}
impl CommandQueue {
pub fn new(max_history: usize) -> Self {
Self {
queue: VecDeque::new(),
history: Vec::new(),
max_history,
}
}
pub fn enqueue(&mut self, frame: u64, command: GameCommand) {
self.queue.push_back(TimestampedCommand { frame, command });
}
pub fn process_all(&mut self, state: &mut GameState) -> Vec<CommandResult> {
let mut results = Vec::new();
while let Some(ts_cmd) = self.queue.pop_front() {
let result = ts_cmd.command.execute(state);
results.push(result);
// Save to history for replay
self.history.push(ts_cmd);
if self.history.len() > self.max_history {
self.history.remove(0);
}
}
state.frame += 1;
results
}
/// Get recorded commands for replay
pub fn get_history(&self) -> &[TimestampedCommand] {
&self.history
}
/// Replay commands from history
pub fn replay(&self, initial_state: GameState) -> Vec<(GameState, CommandResult)> {
let mut state = initial_state;
let mut replay = Vec::new();
for ts_cmd in &self.history {
let result = ts_cmd.command.execute(&mut state);
state.frame = ts_cmd.frame;
replay.push((state.clone(), result));
}
replay
}
}
/// Input buffer with command coalescing
pub struct InputBuffer {
pending_move: Option<(f32, f32)>,
pending_commands: Vec<GameCommand>,
}
impl InputBuffer {
pub fn new() -> Self {
Self {
pending_move: None,
pending_commands: Vec::new(),
}
}
/// Buffer a move command (coalesces multiple moves)
pub fn buffer_move(&mut self, dx: f32, dy: f32) {
match &mut self.pending_move {
Some((x, y)) => {
*x += dx;
*y += dy;
}
None => {
self.pending_move = Some((dx, dy));
}
}
}
/// Buffer other commands
pub fn buffer_command(&mut self, command: GameCommand) {
self.pending_commands.push(command);
}
/// Flush buffered commands to queue
pub fn flush(&mut self, frame: u64, queue: &mut CommandQueue) {
// Flush coalesced move first
if let Some((dx, dy)) = self.pending_move.take() {
queue.enqueue(frame, GameCommand::Move { dx, dy });
}
// Flush other commands
for command in self.pending_commands.drain(..) {
queue.enqueue(frame, command);
}
}
}
impl Default for InputBuffer {
fn default() -> Self {
Self::new()
}
}
fn main() {
let mut state = GameState::new();
let mut queue = CommandQueue::new(1000);
let mut input = InputBuffer::new();
// Simulate game loop
for frame in 0..10 {
// Buffer inputs (could come from keyboard, controller, network)
match frame {
0 => {
input.buffer_move(1.0, 0.0);
input.buffer_move(0.5, 0.0); // Will be coalesced
}
1 => {
input.buffer_command(GameCommand::PickupItem {
item: "health_potion".to_string(),
});
}
3 => {
input.buffer_command(GameCommand::Attack { direction: 0.0 });
}
5 => {
input.buffer_command(GameCommand::UseItem { item_index: 0 });
}
7 => {
input.buffer_move(0.0, -1.0);
input.buffer_command(GameCommand::Jump { height: 2.0 });
}
_ => {}
}
// Flush inputs and process
input.flush(frame, &mut queue);
let results = queue.process_all(&mut state);
for result in results {
println!("Frame {}: {:?}", frame, result);
}
}
println!("\nFinal state: {:?}", state);
// Replay demonstration
println!("\n--- Replay ---");
let replay = queue.replay(GameState::new());
for (replay_state, result) in replay {
println!(
"Frame {}: pos=({:.1}, {:.1}), hp={}, score={} | {:?}",
replay_state.frame,
replay_state.player_x,
replay_state.player_y,
replay_state.health,
replay_state.score,
result
);
}
}
A command-based task scheduler with retries and dependencies.
use std::collections::HashMap;
use std::time::{Duration, Instant};
/// Task status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
}
/// Task command trait
pub trait TaskCommand: std::fmt::Debug + Send + Sync {
fn name(&self) -> &str;
fn execute(&self) -> Result<String, String>;
fn estimated_duration(&self) -> Duration;
}
/// Shell command task
#[derive(Debug)]
pub struct ShellCommand {
name: String,
command: String,
args: Vec<String>,
}
impl ShellCommand {
pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
Self {
name: name.into(),
command: command.into(),
args: Vec::new(),
}
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
}
impl TaskCommand for ShellCommand {
fn name(&self) -> &str {
&self.name
}
fn execute(&self) -> Result<String, String> {
// Simulate command execution
println!("Executing: {} {:?}", self.command, self.args);
Ok(format!("Output of {}", self.command))
}
fn estimated_duration(&self) -> Duration {
Duration::from_secs(5)
}
}
/// HTTP request task
#[derive(Debug)]
pub struct HttpRequestCommand {
name: String,
url: String,
method: String,
body: Option<String>,
}
impl HttpRequestCommand {
pub fn get(name: impl Into<String>, url: impl Into<String>) -> Self {
Self {
name: name.into(),
url: url.into(),
method: "GET".to_string(),
body: None,
}
}
pub fn post(name: impl Into<String>, url: impl Into<String>, body: impl Into<String>) -> Self {
Self {
name: name.into(),
url: url.into(),
method: "POST".to_string(),
body: Some(body.into()),
}
}
}
impl TaskCommand for HttpRequestCommand {
fn name(&self) -> &str {
&self.name
}
fn execute(&self) -> Result<String, String> {
println!("HTTP {} {}", self.method, self.url);
Ok(format!("Response from {}", self.url))
}
fn estimated_duration(&self) -> Duration {
Duration::from_secs(2)
}
}
/// Task with retry configuration
pub struct Task {
command: Box<dyn TaskCommand>,
retries: u32,
retry_delay: Duration,
dependencies: Vec<String>,
status: TaskStatus,
attempts: u32,
output: Option<Result<String, String>>,
}
impl Task {
pub fn new(command: Box<dyn TaskCommand>) -> Self {
Self {
command,
retries: 0,
retry_delay: Duration::from_secs(1),
dependencies: Vec::new(),
status: TaskStatus::Pending,
attempts: 0,
output: None,
}
}
pub fn with_retries(mut self, retries: u32, delay: Duration) -> Self {
self.retries = retries;
self.retry_delay = delay;
self
}
pub fn depends_on(mut self, task_name: impl Into<String>) -> Self {
self.dependencies.push(task_name.into());
self
}
pub fn run(&mut self) -> Result<String, String> {
self.status = TaskStatus::Running;
loop {
self.attempts += 1;
println!(
"Running task '{}' (attempt {}/{})",
self.command.name(),
self.attempts,
self.retries + 1
);
match self.command.execute() {
Ok(output) => {
self.status = TaskStatus::Completed;
self.output = Some(Ok(output.clone()));
return Ok(output);
}
Err(e) => {
if self.attempts <= self.retries {
println!(
"Task '{}' failed, retrying in {:?}...",
self.command.name(),
self.retry_delay
);
std::thread::sleep(self.retry_delay);
} else {
self.status = TaskStatus::Failed;
self.output = Some(Err(e.clone()));
return Err(e);
}
}
}
}
}
}
/// Task scheduler
pub struct Scheduler {
tasks: HashMap<String, Task>,
execution_order: Vec<String>,
}
impl Scheduler {
pub fn new() -> Self {
Self {
tasks: HashMap::new(),
execution_order: Vec::new(),
}
}
pub fn add_task(&mut self, task: Task) {
let name = task.command.name().to_string();
self.tasks.insert(name.clone(), task);
self.execution_order.push(name);
}
pub fn run_all(&mut self) -> HashMap<String, Result<String, String>> {
let mut results = HashMap::new();
let mut completed = std::collections::HashSet::new();
// Topological sort would be better, but this works for simple cases
let order = self.execution_order.clone();
for task_name in order {
// Check dependencies
if let Some(task) = self.tasks.get(&task_name) {
let deps_satisfied = task
.dependencies
.iter()
.all(|dep| completed.contains(dep));
if !deps_satisfied {
println!("Skipping '{}': dependencies not satisfied", task_name);
continue;
}
}
if let Some(task) = self.tasks.get_mut(&task_name) {
let result = task.run();
if result.is_ok() {
completed.insert(task_name.clone());
}
results.insert(task_name, result);
}
}
results
}
}
impl Default for Scheduler {
fn default() -> Self {
Self::new()
}
}
fn main() {
let mut scheduler = Scheduler::new();
// Define tasks with dependencies
scheduler.add_task(Task::new(Box::new(
ShellCommand::new("checkout", "git")
.arg("checkout")
.arg("main"),
)));
scheduler.add_task(
Task::new(Box::new(ShellCommand::new("build", "cargo").arg("build")))
.depends_on("checkout")
.with_retries(2, Duration::from_secs(1)),
);
scheduler.add_task(
Task::new(Box::new(ShellCommand::new("test", "cargo").arg("test")))
.depends_on("build"),
);
scheduler.add_task(
Task::new(Box::new(HttpRequestCommand::post(
"deploy",
"https://api.deploy.com/trigger",
r#"{"env": "production"}"#,
)))
.depends_on("test")
.with_retries(3, Duration::from_secs(5)),
);
// Run all tasks
println!("=== Starting Task Scheduler ===\n");
let results = scheduler.run_all();
println!("\n=== Results ===");
for (name, result) in results {
match result {
Ok(output) => println!("{}: SUCCESS - {}", name, output),
Err(e) => println!("{}: FAILED - {}", name, e),
}
}
}
| Approach | Flexibility | Performance | Type Safety | Use Case |
|----------|-------------|-------------|-------------|----------|
| Trait objects | High | Medium | High | Complex commands, plugins |
| Enums | Medium | High | Highest | Fixed set of commands |
| Closures | High | High | Medium | Simple callbacks |
| Function pointers | Low | Highest | Low | C interop, hot paths |
// DON'T: Commands with hidden state dependencies
struct BadCommand {
// No way to know what external state this modifies
}
impl BadCommand {
fn execute(&self) {
// Modifies global state secretly
unsafe { GLOBAL_COUNTER += 1; }
}
}
// DON'T: Mutable receiver for undo
struct BadUndoCommand {
data: String,
}
impl BadUndoCommand {
// Can't undo if data is consumed
fn execute(self) -> String {
self.data
}
}
// DO: Commands own their undo state
struct GoodCommand {
new_value: String,
old_value: Option<String>, // Captured during execute
}
| Operation | Trait Object | Enum | Closure |
|-----------|-------------|------|---------|
| Dispatch | Virtual call | Match | Indirect call |
| Memory | Heap + vtable | Stack | Heap (if boxed) |
| Inline | No | Yes | Sometimes |
| Clone | Requires Clone | Easy | Usually not |
LightSwitch with TurnOn and TurnOff commandsCreate, Delete, Move commandsRun this code in the official Rust Playground