argot-cmd 0.2.0

An agent-first command interface framework for Rust
Documentation
# Middleware Guide

Argot's middleware system lets you intercept the parse-and-dispatch lifecycle without modifying command handlers. Use it for logging, auditing, authentication checks, metrics, or any cross-cutting concern.

---

## What Middleware Is

When `Cli::run` receives a command, it:

1. Parses `argv` with `Parser`
2. Calls each registered middleware's `before_dispatch` hook
3. Invokes the command handler
4. Calls each registered middleware's `after_dispatch` hook

If parsing fails, `on_parse_error` is called instead of the dispatch hooks.

Middleware fits cleanly into this lifecycle without touching individual command handlers.

---

## The Three Hooks

### `before_dispatch`

```rust
fn before_dispatch(
    &self,
    parsed: &ParsedCommand<'_>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
```

- Fires after a successful parse, before the handler runs.
- Receives the fully-resolved `ParsedCommand` (command metadata + parsed args/flags).
- Return `Ok(())` to proceed with dispatch.
- Return `Err(...)` to abort dispatch; the error surfaces as `CliError::Handler`.

### `after_dispatch`

```rust
fn after_dispatch(
    &self,
    parsed: &ParsedCommand<'_>,
    result: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
)
```

- Fires after the handler returns, whether it succeeded or failed.
- Receives the same `ParsedCommand` and the handler's result.
- Always fires — even when `before_dispatch` aborted or the handler errored.
- Return value is `()` — this hook cannot affect the outcome.

### `on_parse_error`

```rust
fn on_parse_error(&self, error: &ParseError)
```

- Fires when `Parser::parse` returns an error (unknown command, missing argument, etc.).
- The error is also printed to stderr and returned from `Cli::run` as `CliError::Parse`.
- Use this hook to route parse failures to an audit log or error tracker.

---

## Registering Middleware

Use `Cli::with_middleware` to register one or more middlewares. Calls chain fluently:

```rust
use argot::{Cli, Command};

Cli::new(commands)
    .with_middleware(Logger)
    .with_middleware(Audit)
    .run_env_args_and_exit();
```

Middlewares fire in registration order. `Logger`'s `before_dispatch` runs before `Audit`'s.

---

## Example: Request Logging

Log the command name and a Unix timestamp every time a command is dispatched.

```rust
use std::sync::Arc;
use std::time::SystemTime;
use argot::{Argument, Cli, Command, ParsedCommand};
use argot::middleware::Middleware;

struct Logger;

impl Middleware for Logger {
    fn before_dispatch(
        &self,
        parsed: &ParsedCommand<'_>,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let ts = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);
        eprintln!("[LOG {}] dispatching: {}", ts, parsed.command.canonical);
        Ok(())
    }
}

fn main() {
    let deploy = Command::builder("deploy")
        .summary("Deploy the service")
        .argument(Argument::builder("env").required().build().unwrap())
        .handler(Arc::new(|parsed| {
            println!("Deploying to {}", parsed.arg("env").unwrap_or("unknown"));
            Ok(())
        }))
        .build()
        .unwrap();

    Cli::new(vec![deploy])
        .with_middleware(Logger)
        .run_env_args_and_exit();
}
```

Sample stderr output for `mytool deploy staging`:

```
[LOG 1714000000] dispatching: deploy
Deploying to staging
```

---

## Example: Aborting Dispatch

A `before_dispatch` hook that returns `Err` prevents the handler from running. Use this for authentication checks, rate limiting, or dry-run guards.

```rust
use argot::{Cli, Command, ParsedCommand};
use argot::middleware::Middleware;
use std::sync::Arc;

struct AuthGuard;

impl Middleware for AuthGuard {
    fn before_dispatch(
        &self,
        parsed: &ParsedCommand<'_>,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        // Read a token from the environment.
        let token = std::env::var("API_TOKEN").unwrap_or_default();
        if token.is_empty() {
            return Err(format!(
                "command '{}' requires API_TOKEN to be set",
                parsed.command.canonical
            ).into());
        }
        Ok(())
    }
}

fn main() {
    let deploy = Command::builder("deploy")
        .summary("Deploy the service")
        .handler(Arc::new(|_| {
            println!("Deploying...");
            Ok(())
        }))
        .build()
        .unwrap();

    Cli::new(vec![deploy])
        .with_middleware(AuthGuard)
        .run_env_args_and_exit();
}
```

When `API_TOKEN` is not set:

```
error: handler error: command 'deploy' requires API_TOKEN to be set
```

The handler never runs.

---

## Example: Error Tracking

Use `on_parse_error` and `after_dispatch` together to capture both parse failures and handler failures in one place.

```rust
use argot::{Cli, Command, ParsedCommand};
use argot::middleware::Middleware;
use argot::parser::ParseError;
use std::sync::Arc;

struct ErrorTracker;

impl Middleware for ErrorTracker {
    fn on_parse_error(&self, err: &ParseError) {
        // Send to your error tracker, e.g. Sentry, Datadog, etc.
        eprintln!("[TRACKER] parse failure: {}", err);
    }

    fn after_dispatch(
        &self,
        parsed: &ParsedCommand<'_>,
        result: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
    ) {
        if let Err(e) = result {
            eprintln!(
                "[TRACKER] handler failure: command={} error={}",
                parsed.command.canonical, e
            );
        }
    }
}

fn main() {
    let deploy = Command::builder("deploy")
        .summary("Deploy the service")
        .handler(Arc::new(|_| {
            Err("connection refused".into())
        }))
        .build()
        .unwrap();

    Cli::new(vec![deploy])
        .with_middleware(ErrorTracker)
        .run_env_args_and_exit();
}
```

Running `mytool deploy` with this middleware:

```
[TRACKER] handler failure: command=deploy error=connection refused
error: handler error: connection refused
```

---

## Multiple Middlewares

All middlewares registered with `with_middleware` fire in the order they were registered.

```rust
Cli::new(commands)
    .with_middleware(Logger)    // fires first
    .with_middleware(Audit)     // fires second
    .with_middleware(Metrics)   // fires third
    .run_env_args_and_exit();
```

For `before_dispatch`: if `Logger` returns `Err`, `Audit` and `Metrics` do not fire, and the handler does not run.

For `after_dispatch` and `on_parse_error`: all registered middlewares always fire, regardless of what earlier ones returned.

---

## Key Points

- All three hooks have **default no-op implementations**. Implement only the hooks you need.
- `before_dispatch` is the **only hook that can abort dispatch**. Returning `Err` prevents the handler from running and returns `CliError::Handler` from `Cli::run`.
- `after_dispatch` **always fires** — even when `before_dispatch` aborted or the handler errored. The `result` parameter tells you what happened.
- `on_parse_error` fires for **parser-level failures only** (before any dispatch occurs).
- Middleware must implement `Send + Sync` to be safely shared across threads.
- Add as many middlewares as needed; there is no limit on registrations.