argot-cmd 0.2.0

An agent-first command interface framework for Rust
Documentation
# Validation Patterns in Argot

Argot validates command input at two distinct points: the **parser layer** enforces structural constraints declared in the command definition, and the **handler** performs semantic validation that requires business logic or external resources. This guide covers both levels with practical examples.

---

## Built-in Validation

The parser enforces built-in constraints automatically before the handler is ever called. Prefer built-in validation over handler validation whenever the constraint can be expressed declaratively — it produces consistent error messages, is reflected in generated help text, and keeps handler code focused on business logic.

### Required Arguments

Mark a positional argument as required with `.required()`. The parser returns `ParseError::MissingArgument` if the argument is absent.

```rust
use argot::Argument;

Argument::builder("env")
    .description("Target environment (e.g. staging, prod)")
    .required()
    .build()
    .unwrap()
```

The parser enforces this after all tokens have been processed, so a missing required argument always produces an error — there is no way for the handler to receive a `ParsedCommand` with the argument absent.

### Required Flags

Flags are made required the same way. The parser returns `ParseError::MissingFlag` if the flag is absent from the command line and no environment variable fallback provides a value.

```rust
use argot::Flag;

Flag::builder("token")
    .takes_value()
    .required()
    .env("API_TOKEN")
    .build()
    .unwrap()
```

With `.env("API_TOKEN")`, the lookup order is:

1. `--token <value>` on the command line
2. The `API_TOKEN` environment variable
3. A `.default_value(...)` if one was set
4. `ParseError::MissingFlag` if none of the above provided a value

### Enum/Choice Constraints

Restrict a value-taking flag to a fixed set of choices with `.choices(...)`. The parser returns `ParseError::InvalidChoice` for any value not in the list.

```rust
use argot::Flag;

Flag::builder("strategy")
    .takes_value()
    .choices(["rolling", "blue-green", "canary"])
    .default_value("rolling")
    .build()
    .unwrap()
```

The choice constraint is applied to both command-line values and environment variable fallbacks, so the validation is consistent regardless of how the flag value arrives.

If you call `.choices(...)` with an empty iterator, `build()` returns `BuildError::EmptyChoices`. This catches the mistake at definition time rather than silently accepting all values.

### Environment Variable Fallback

Any value-taking flag can be backed by an environment variable using `.env(var_name)`. This is useful for secrets and configuration that operators prefer not to pass on the command line.

```rust
use argot::Flag;

Flag::builder("database-url")
    .takes_value()
    .required()
    .env("DATABASE_URL")
    .build()
    .unwrap()
```

The env var name is stored on the `Flag` struct's `env` field and is reflected in generated help output, so users know which variable to set.

### Repeatable Flags

Flags that may be specified more than once are declared with `.repeatable()`.

For boolean flags, repeated occurrences are counted and stored as a numeric string:

```rust
// -v -v -v → flags["verbose"] == "3"
Flag::builder("verbose")
    .short('v')
    .repeatable()
    .build()
    .unwrap()
```

For value-taking flags, values are collected into a JSON array string:

```rust
// --tag foo --tag bar → flags["tag"] == r#"["foo","bar"]"#
Flag::builder("tag")
    .takes_value()
    .repeatable()
    .build()
    .unwrap()
```

### Boolean Flag Negation

Boolean flags support `--no-{name}` negation syntax automatically. Passing `--no-dry-run` sets `flags["dry-run"]` to `"false"`. No extra declaration is needed.

### Variadic Arguments

When a command accepts a variable number of positional arguments, declare the last argument as variadic. The parser collects all remaining positional tokens into a JSON array string stored under that argument's name.

```rust
use argot::Argument;

Argument::builder("files")
    .description("One or more files to process")
    .required()
    .variadic()
    .build()
    .unwrap()
// argv: process a.txt b.txt c.txt
// args["files"] == r#"["a.txt","b.txt","c.txt"]"#
```

`CommandBuilder::build()` enforces that the variadic argument is the last argument defined, returning `BuildError::VariadicNotLast` if that constraint is violated.

---

## Handler-Level Validation

After the parser succeeds, the handler receives a `ParsedCommand` with all arguments and flags bound. Use handler validation for:

- Numeric range or format checks
- File/path existence checks
- Cross-field constraint checks (combinations of flags that are mutually invalid)
- Business rule validation (valid environment names, non-empty strings, network reachability)

Return descriptive errors from handlers — they surface directly to the user via `CliError::Handler`.

### Numeric Range Checking

```rust
use std::sync::Arc;
use argot::{Command, Argument};

Command::builder("scale")
    .argument(Argument::builder("workers").required().build().unwrap())
    .handler(Arc::new(|parsed| {
        let workers: u32 = parsed
            .arg("workers")
            .unwrap_or("1")
            .parse()
            .map_err(|_| "workers must be a positive integer")?;

        if workers == 0 || workers > 64 {
            return Err("workers must be between 1 and 64".into());
        }

        println!("Scaling to {} workers", workers);
        Ok(())
    }))
    .build()
    .unwrap();
```

### Path Validation

```rust
use std::sync::Arc;
use argot::{Command, Argument};

Command::builder("load")
    .argument(Argument::builder("file").required().build().unwrap())
    .handler(Arc::new(|parsed| {
        let path = std::path::Path::new(parsed.arg("file").unwrap());

        if !path.exists() {
            return Err(format!("file not found: {}", path.display()).into());
        }
        if !path.is_file() {
            return Err(format!("not a regular file: {}", path.display()).into());
        }

        println!("Loading {}", path.display());
        Ok(())
    }))
    .build()
    .unwrap();
```

### Cross-Field Validation

When two or more flags are mutually exclusive in a way that depends on their values or combined semantics, validate in the handler:

```rust
use std::sync::Arc;
use argot::{Command, Flag};

Command::builder("deploy")
    .flag(Flag::builder("dry-run").short('n').build().unwrap())
    .flag(Flag::builder("force").build().unwrap())
    .handler(Arc::new(|parsed| {
        let dry_run = parsed.flag_bool("dry-run");
        let force = parsed.flag_bool("force");

        if dry_run && force {
            return Err("--dry-run and --force cannot be combined".into());
        }

        if force {
            println!("Deploying with --force");
        } else if dry_run {
            println!("[DRY RUN] No changes will be made");
        } else {
            println!("Deploying");
        }
        Ok(())
    }))
    .build()
    .unwrap();
```

### Validated Struct Pattern

For commands with several flags and arguments, extract a typed configuration struct from `ParsedCommand` before running any business logic. This separates input parsing from execution, makes the handler easier to test, and gives validation errors a single location.

```rust
use std::error::Error;
use argot::ParsedCommand;

struct DeployConfig {
    env: String,
    strategy: String,
    dry_run: bool,
    timeout_secs: Option<u64>,
}

impl DeployConfig {
    fn from_parsed(parsed: &ParsedCommand) -> Result<Self, Box<dyn Error>> {
        let env = parsed
            .arg("environment")
            .ok_or("missing required argument: environment")?
            .to_string();

        let strategy = parsed
            .flag("strategy")
            .unwrap_or("rolling")
            .to_string();

        let valid_strategies = ["rolling", "blue-green", "canary"];
        if !valid_strategies.contains(&strategy.as_str()) {
            return Err(format!(
                "unknown strategy `{}`: expected one of {}",
                strategy,
                valid_strategies.join(", ")
            ).into());
        }

        let timeout_secs = match parsed.flag("timeout") {
            Some(t) => Some(
                t.parse::<u64>()
                    .map_err(|_| format!("timeout must be a positive integer, got `{}`", t))?,
            ),
            None => None,
        };

        Ok(Self {
            env,
            strategy,
            dry_run: parsed.flag_bool("dry-run"),
            timeout_secs,
        })
    }
}

// In the handler:
use std::sync::Arc;
use argot::{Command, Argument, Flag};

Command::builder("deploy")
    .argument(Argument::builder("environment").required().build().unwrap())
    .flag(Flag::builder("strategy").takes_value().default_value("rolling").build().unwrap())
    .flag(Flag::builder("dry-run").short('n').build().unwrap())
    .flag(Flag::builder("timeout").short('t').takes_value().build().unwrap())
    .handler(Arc::new(|parsed| {
        let config = DeployConfig::from_parsed(parsed)?;

        if config.dry_run {
            println!("[DRY RUN] Would deploy to {} using {}", config.env, config.strategy);
            return Ok(());
        }

        println!("Deploying to {} using {} strategy", config.env, config.strategy);
        if let Some(t) = config.timeout_secs {
            println!("Timeout: {}s", t);
        }
        Ok(())
    }))
    .build()
    .unwrap();
```

### Structured Handler Errors with `thiserror`

For commands with several distinct failure modes, define a dedicated error enum so each case has a clear name and message:

```rust
use std::sync::Arc;
use argot::{Command, Argument};

#[derive(Debug, thiserror::Error)]
enum DeployError {
    #[error("environment `{0}` is not recognized; valid environments: prod, staging, dev")]
    UnknownEnvironment(String),
    #[error("environment `{0}` is locked for maintenance")]
    EnvironmentLocked(String),
    #[error("deployment failed: {0}")]
    Io(#[from] std::io::Error),
}

Command::builder("deploy")
    .argument(Argument::builder("env").required().build().unwrap())
    .handler(Arc::new(|parsed| {
        let env = parsed.arg("env").unwrap();

        if !["prod", "staging", "dev"].contains(&env) {
            return Err(Box::new(DeployError::UnknownEnvironment(env.to_string())));
        }

        // Hypothetical check against a lock file:
        let lock_path = format!("/var/locks/{}.lock", env);
        if std::path::Path::new(&lock_path).exists() {
            return Err(Box::new(DeployError::EnvironmentLocked(env.to_string())));
        }

        println!("Deploying to {}", env);
        Ok(())
    }))
    .build()
    .unwrap();
```

The `#[from] std::io::Error` attribute on `Io` allows `?` to convert `std::io::Error` directly into `DeployError::Io` within the handler.

---

## Validation Order

When a user invokes a command, argot applies validation in this order:

1. **Tokenization** — the argv slice is tokenized into words, long flags, short flags, and the `--` separator.
2. **Command resolution** — the first token is resolved to a top-level `Command` via exact match, alias match, or unambiguous prefix. Subcommand tokens are resolved recursively.
3. **Flag binding** — flag tokens are matched against the resolved command's flag definitions. `UnknownFlag`, `FlagMissingValue`, and `InvalidChoice` errors are raised here.
4. **Argument binding** — remaining word tokens are bound to positional argument definitions in declaration order. `UnexpectedArgument` is raised here.
5. **Required validation** — after all tokens are consumed, missing required arguments and flags are detected. `MissingArgument` and `MissingFlag` are raised here. Environment variable fallbacks and defaults are applied during this step.
6. **Handler** — if all of the above steps succeed, the handler is called with the fully-bound `ParsedCommand`. Semantic validation (business rules, cross-field logic, external resource checks) happens here.

If any step fails, the pipeline stops and the error is returned to the caller. The handler is only called when every parser-layer check passes.

---

## Best Practices

**Prefer built-in validation over handler validation when possible.** Using `.required()`, `.choices(...)`, and `.env(...)` is always preferable to repeating those checks in the handler. Built-in constraints are reflected in help output and produce consistent error messages across every command that uses them.

**Use `.choices(...)` for fixed enumerated values.** Rather than checking `if strategy != "rolling" && strategy != "blue-green" ...` in the handler, declare the choices on the flag. The parser enforces them and the help text lists the valid options automatically.

**Reserve handler validation for semantic checks.** File existence, network availability, business rules, numeric ranges, and cross-field constraints belong in the handler. These are things the parser cannot know about.

**Return descriptive errors from handlers.** Handler errors become `CliError::Handler` messages printed to the user. "environment `xyz` is not recognized" is far more useful than "invalid input".

**Use `thiserror` for structured handler errors.** A typed error enum with clear variants makes handler logic easier to read and test. The `#[from]` attribute eliminates manual error conversion boilerplate.

**Extract a config struct for complex commands.** When a handler needs more than two or three values from `ParsedCommand`, introduce a validated config struct. It localizes all input validation, makes the handler body read like business logic, and is easy to unit-test independently of the parser.