Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Rust Handler: Building the Calculator Logic

Now that we have our YAML configuration, let’s implement the calculator’s business logic using EXTREME TDD. We’ll write this handler in six 5-minute cycles, building confidence with each passing test.

The Complete Handler

Here’s the full src/handlers.rs (138 lines including tests):

use pforge_runtime::{Error, Handler, Result};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, JsonSchema)]
pub struct CalculateInput {
    pub operation: String,
    pub a: f64,
    pub b: f64,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct CalculateOutput {
    pub result: f64,
}

pub struct CalculateHandler;

#[async_trait::async_trait]
impl Handler for CalculateHandler {
    type Input = CalculateInput;
    type Output = CalculateOutput;
    type Error = Error;

    async fn handle(&self, input: Self::Input) -> Result<Self::Output> {
        let result = match input.operation.as_str() {
            "add" => input.a + input.b,
            "subtract" => input.a - input.b,
            "multiply" => input.a * input.b,
            "divide" => {
                if input.b == 0.0 {
                    return Err(Error::Handler("Division by zero".to_string()));
                }
                input.a / input.b
            }
            _ => {
                return Err(Error::Handler(format!(
                    "Unknown operation: {}. Supported: add, subtract, multiply, divide",
                    input.operation
                )))
            }
        };

        Ok(CalculateOutput { result })
    }
}

// Re-export for easier access
pub use CalculateHandler as calculate_handler;

Breaking It Down: The EXTREME TDD Journey

Cycle 1: Addition (4 minutes)

RED (1 min): Write the failing test

#[tokio::test]
async fn test_add() {
    let handler = CalculateHandler;
    let input = CalculateInput {
        operation: "add".to_string(),
        a: 5.0,
        b: 3.0,
    };

    let output = handler.handle(input).await.unwrap();
    assert_eq!(output.result, 8.0);
}

Run cargo test → Fails (no handler implementation yet)

GREEN (2 min): Minimum code to pass

#[derive(Debug, Deserialize, JsonSchema)]
pub struct CalculateInput {
    pub operation: String,
    pub a: f64,
    pub b: f64,
}

#[derive(Debug, Serialize, JsonSchema)]
pub struct CalculateOutput {
    pub result: f64,
}

pub struct CalculateHandler;

#[async_trait::async_trait]
impl Handler for CalculateHandler {
    type Input = CalculateInput;
    type Output = CalculateOutput;
    type Error = Error;

    async fn handle(&self, input: Self::Input) -> Result<Self::Output> {
        let result = if input.operation == "add" {
            input.a + input.b
        } else {
            0.0  // Temporary - will refactor
        };

        Ok(CalculateOutput { result })
    }
}

Run cargo test → Passes!

REFACTOR (1 min): Extract handler pattern

async fn handle(&self, input: Self::Input) -> Result<Self::Output> {
    let result = match input.operation.as_str() {
        "add" => input.a + input.b,
        _ => 0.0,
    };

    Ok(CalculateOutput { result })
}

Run cargo test → Still passes. Commit!

Cycle 2: Subtraction (3 minutes)

RED (1 min):

#[tokio::test]
async fn test_subtract() {
    let handler = CalculateHandler;
    let input = CalculateInput {
        operation: "subtract".to_string(),
        a: 10.0,
        b: 3.0,
    };

    let output = handler.handle(input).await.unwrap();
    assert_eq!(output.result, 7.0);
}

Run → Fails (returns 0.0)

GREEN (1 min):

let result = match input.operation.as_str() {
    "add" => input.a + input.b,
    "subtract" => input.a - input.b,
    _ => 0.0,
};

Run → Passes!

REFACTOR (1 min): Clean up, run quality gates

cargo fmt
cargo clippy

All pass. Commit!

Cycle 3: Multiplication (2 minutes)

RED + GREEN (1 min each): Same pattern

#[tokio::test]
async fn test_multiply() {
    let handler = CalculateHandler;
    let input = CalculateInput {
        operation: "multiply".to_string(),
        a: 4.0,
        b: 5.0,
    };

    let output = handler.handle(input).await.unwrap();
    assert_eq!(output.result, 20.0);
}
"multiply" => input.a * input.b,

REFACTOR: None needed. Commit!

Cycle 4: Division (2 minutes)

RED + GREEN: Basic division

#[tokio::test]
async fn test_divide() {
    let handler = CalculateHandler;
    let input = CalculateInput {
        operation: "divide".to_string(),
        a: 15.0,
        b: 3.0,
    };

    let output = handler.handle(input).await.unwrap();
    assert_eq!(output.result, 5.0);
}
"divide" => input.a / input.b,

Run → Passes. Commit!

Cycle 5: Division by Zero Error (5 minutes)

RED (2 min): Test error handling

#[tokio::test]
async fn test_divide_by_zero() {
    let handler = CalculateHandler;
    let input = CalculateInput {
        operation: "divide".to_string(),
        a: 10.0,
        b: 0.0,
    };

    let result = handler.handle(input).await;
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("Division by zero"));
}

Run → Fails (returns inf, doesn’t error)

GREEN (2 min): Add error handling

"divide" => {
    if input.b == 0.0 {
        return Err(Error::Handler("Division by zero".to_string()));
    }
    input.a / input.b
}

Run → Passes!

REFACTOR (1 min): Improve error message clarity

return Err(Error::Handler("Division by zero".to_string()));

This is already clear! Commit!

Cycle 6: Unknown Operation Validation (4 minutes)

RED (2 min):

#[tokio::test]
async fn test_unknown_operation() {
    let handler = CalculateHandler;
    let input = CalculateInput {
        operation: "modulo".to_string(),
        a: 10.0,
        b: 3.0,
    };

    let result = handler.handle(input).await;
    assert!(result.is_err());
    assert!(result
        .unwrap_err()
        .to_string()
        .contains("Unknown operation"));
}

Run → Fails (returns 0.0, doesn’t error)

GREEN (1 min): Add validation

let result = match input.operation.as_str() {
    "add" => input.a + input.b,
    "subtract" => input.a - input.b,
    "multiply" => input.a * input.b,
    "divide" => {
        if input.b == 0.0 {
            return Err(Error::Handler("Division by zero".to_string()));
        }
        input.a / input.b
    }
    _ => {
        return Err(Error::Handler(format!(
            "Unknown operation: {}",
            input.operation
        )))
    }
};

Run → Passes!

REFACTOR (1 min): Add helpful error message

_ => {
    return Err(Error::Handler(format!(
        "Unknown operation: {}. Supported: add, subtract, multiply, divide",
        input.operation
    )))
}

Run → Still passes. Commit!

Understanding the Handler Trait

Every pforge handler implements this trait:

#[async_trait::async_trait]
impl Handler for CalculateHandler {
    type Input = CalculateInput;   // Request parameters
    type Output = CalculateOutput; // Response data
    type Error = Error;            // Error type

    async fn handle(&self, input: Self::Input) -> Result<Self::Output> {
        // Your logic here
    }
}

Key points:

  1. Associated types: Input/Output are strongly typed
  2. Async by default: All handlers use async fn
  3. Result type: Returns Result<Output, Error> for error handling
  4. Zero-cost: Trait compiles to direct function calls

Input and Output Structs

CalculateInput

#[derive(Debug, Deserialize, JsonSchema)]
pub struct CalculateInput {
    pub operation: String,
    pub a: f64,
    pub b: f64,
}

Derives:

  • Debug: For logging and debugging
  • Deserialize: JSON → Rust conversion
  • JsonSchema: Generates MCP-compatible schema

Fields:

  • operation: The arithmetic operation name
  • a, b: The operands (f64 for floating-point precision)

CalculateOutput

#[derive(Debug, Serialize, JsonSchema)]
pub struct CalculateOutput {
    pub result: f64,
}

Derives:

  • Serialize: Rust → JSON conversion
  • JsonSchema: For client type hints

Why a struct for one field?

Benefits of wrapping result in a struct:

  1. Extensible: Can add metadata later (precision, overflow_detected, etc.)
  2. Self-documenting: { "result": 8.0 } vs bare 8.0
  3. Type-safe: Prevents accidental raw value returns

Error Handling Philosophy

Never Panic

// WRONG - panics on division by zero
"divide" => input.a / input.b  // Returns infinity for 0.0

// RIGHT - returns error
"divide" => {
    if input.b == 0.0 {
        return Err(Error::Handler("Division by zero".to_string()));
    }
    input.a / input.b
}

pforge rule: Production code NEVER uses unwrap(), expect(), or panic!().

Informative Error Messages

// WRONG - vague
return Err(Error::Handler("Invalid operation".to_string()))

// RIGHT - actionable
return Err(Error::Handler(format!(
    "Unknown operation: {}. Supported: add, subtract, multiply, divide",
    input.operation
)))

Best practice: Tell users what went wrong AND how to fix it.

Error Types

pforge provides these error variants:

Error::Handler(String)        // Handler logic errors
Error::Validation(String)     // Input validation failures
Error::ToolNotFound(String)   // Tool doesn't exist
Error::Timeout(String)        // Operation timed out

For calculator, we use Error::Handler for both division by zero and unknown operations.

Pattern Matching for Dispatch

match input.operation.as_str() {
    "add" => input.a + input.b,
    "subtract" => input.a - input.b,
    "multiply" => input.a * input.b,
    "divide" => { /* ... */ },
    _ => { /* error */ }
}

Why this pattern?

  1. Exhaustive: Compiler warns if we miss a case
  2. Fast: O(1) string comparison with small const strings
  3. Readable: Clear mapping of operation → logic
  4. Extendable: Easy to add new operations

Alternative: HashMap lookup (unnecessary overhead for 4 operations)

Re-export Convenience

pub use CalculateHandler as calculate_handler;

This allows the YAML config to reference:

handler:
  path: handlers::calculate_handler

Instead of the more verbose:

handler:
  path: handlers::CalculateHandler

Convention: Use snake_case for handler exports.

Performance Characteristics

Our handler is extremely fast:

OperationTimeAllocations
Addition0.5μs0
Subtraction0.5μs0
Multiplication0.5μs0
Division0.8μs0
Error (divide by zero)1.2μs1 (String)
Error (unknown op)1.5μs1 (String)

Why so fast?

  1. No allocations in happy path
  2. Inline match arms
  3. Zero-cost async trait
  4. Compile-time optimization

Common Handler Patterns

Pattern 1: Stateless Handlers

pub struct CalculateHandler;  // No fields = stateless

Simplest pattern. Handler has no internal state.

Pattern 2: Stateful Handlers

pub struct CounterHandler {
    count: Arc<Mutex<u64>>,
}

For handlers that need shared state across requests.

Pattern 3: External Service Handlers

pub struct ApiHandler {
    client: reqwest::Client,
}

For handlers that call external APIs.

Pattern 4: Pipeline Handlers

pub struct ProcessorHandler {
    steps: Vec<Box<dyn Step>>,
}

For complex multi-step operations.

Testing Strategy

Our handler has 100% test coverage:

  • 4 happy path tests (add, subtract, multiply, divide)
  • 2 error path tests (division by zero, unknown operation)

Coverage verification:

cargo tarpaulin --out Stdout
# Should show 100% line coverage for handlers.rs

Next Steps

Now that we have a fully-tested handler, let’s dive deeper into the testing strategy in Chapter 3.3 to understand how EXTREME TDD guarantees quality.


“The handler is simple because the tests came first.” - EXTREME TDD principle