# App State and Extensions
`CommandContext` provides two mechanisms for state injection: **app state** (shared, immutable) and **extensions** (per-request, mutable). Understanding the distinction is key to building clean, testable CLI applications.
---
## The Two State Types
| **Mutability** | Immutable (`&`) | Mutable (`&mut`) |
| **Lifetime** | App lifetime | Per-request |
| **Set by** | `AppBuilder::app_state()` | Pre-dispatch hooks |
| **Storage** | `Rc<Extensions>` | `Extensions` |
| **Use for** | Database, Config, API clients | User sessions, request IDs |
---
## App State: Shared Resources
App state is configured once at build time and shared immutably across all command dispatches. Use it for long-lived resources that are expensive to create or need to be shared.
### Setup
```rust
use standout::cli::App;
struct Database { pool: Pool }
struct Config { api_url: String, debug: bool }
struct ApiClient { base_url: String }
let app = App::builder()
.app_state(Database::connect()?)
.app_state(Config::load()?)
.app_state(ApiClient { base_url: "https://api.example.com".into() })
.command("list", list_handler, "{{ items }}")
.build()?;
```
### Access in Handlers
```rust
fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
// Get required state (returns error if not found)
let db = ctx.app_state.get_required::<Database>()?;
let config = ctx.app_state.get_required::<Config>()?;
// Optional state (returns None if not found)
let api = ctx.app_state.get::<ApiClient>();
let items = db.list_items(&config.api_url)?;
Ok(Output::Render(items))
}
```
### Type Safety
Each type can only be stored once. Storing a second value of the same type replaces the first:
```rust
App::builder()
.app_state(Config { debug: false })
.app_state(Config { debug: true }) // Replaces previous Config
```
If you need multiple instances of the same type, wrap them in distinct newtype wrappers:
```rust
struct PrimaryDb(Pool);
struct AnalyticsDb(Pool);
App::builder()
.app_state(PrimaryDb(primary_pool))
.app_state(AnalyticsDb(analytics_pool))
```
---
## Extensions: Per-Request State
Extensions are mutable and scoped to a single command dispatch. Pre-dispatch hooks inject state that handlers consume. Each dispatch starts with empty extensions.
### Injection via Hooks
```rust
use standout_dispatch::{Hooks, HookError};
struct UserScope { user_id: String, permissions: Vec<String> }
struct RequestId(String);
let hooks = Hooks::new()
.pre_dispatch(|matches, ctx| {
// Parse user from args or environment
let user_id = matches.get_one::<String>("user")
.cloned()
.unwrap_or_else(|| std::env::var("USER").unwrap_or_default());
// Look up permissions (could use app_state here!)
let db = ctx.app_state.get_required::<Database>()?;
let permissions = db.get_permissions(&user_id)?;
// Inject per-request state
ctx.extensions.insert(UserScope { user_id, permissions });
ctx.extensions.insert(RequestId(uuid::Uuid::new_v4().to_string()));
Ok(())
});
```
### Access in Handlers
```rust
fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
// App state: shared database
let db = ctx.app_state.get_required::<Database>()?;
// Extensions: per-request user scope
let scope = ctx.extensions.get_required::<UserScope>()?;
// Use both
let items = db.list_items_for_user(&scope.user_id)?;
Ok(Output::Render(items))
}
```
---
## When to Use Which
### Use App State For:
- **Database connections** - Expensive to create, should be pooled
- **Configuration** - Loaded once at startup
- **API clients** - Shared HTTP clients with connection pooling
- **Caches** - Shared lookup tables or memoization
- **Feature flags** - Global toggles loaded at startup
### Use Extensions For:
- **User context** - Current user, session, permissions
- **Request metadata** - Request ID, timing, correlation ID
- **Scoped overrides** - Per-request configuration overrides
- **Transient state** - Data computed by one hook, used by handler
---
## The Hook + Handler Pattern
A common pattern is using pre-dispatch hooks to set up request-scoped state that handlers consume:
```rust
// In builder setup
App::builder()
.app_state(Database::connect()?)
.app_state(PermissionService::new())
.command("admin.delete", admin_delete_handler, "Deleted {{ id }}")
.hooks("admin.delete", Hooks::new()
.pre_dispatch(|matches, ctx| {
// Validate admin permissions using app state
let perms = ctx.app_state.get_required::<PermissionService>()?;
let user = std::env::var("USER").unwrap_or_default();
if !perms.is_admin(&user)? {
return Err(HookError::pre_dispatch("Admin access required"));
}
// Inject validated user context
ctx.extensions.insert(AdminUser { name: user });
Ok(())
}))
.build()?
```
```rust
// Handler can assume validation passed
fn admin_delete_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<DeleteResult> {
let db = ctx.app_state.get_required::<Database>()?;
let admin = ctx.extensions.get_required::<AdminUser>()?;
let id = matches.get_one::<String>("id").unwrap();
db.delete_with_audit(id, &admin.name)?;
Ok(Output::Render(DeleteResult { id: id.clone() }))
}
```
---
## Error Handling
### get_required vs get
Use `get_required` when the state must be present (fail fast):
```rust
// Fails with clear error if Database not configured
let db = ctx.app_state.get_required::<Database>()?;
```
Use `get` when state is optional:
```rust
// Returns None if optional feature not configured
if let Some(cache) = ctx.app_state.get::<Cache>() {
if let Some(cached) = cache.get(key) {
return Ok(Output::Render(cached));
}
}
```
### Error Messages
`get_required` produces descriptive errors:
```
Extension missing: type myapp::Database not found in context
```
---
## Testing with App State
App state makes handlers easily testable by allowing dependency injection:
```rust
#[test]
fn test_list_handler() {
// Create test fixtures
let mock_db = MockDatabase::with_items(vec![
Item { id: "1", name: "Test" }
]);
// Build context with test state
let mut app_state = Extensions::new();
app_state.insert(mock_db);
let ctx = CommandContext {
command_path: vec!["list".into()],
app_state: Arc::new(app_state),
extensions: Extensions::new(),
};
// Test handler
let cmd = Command::new("test");
let matches = cmd.get_matches_from(["test"]);
let result = list_handler(&matches, &ctx);
assert!(result.is_ok());
}
```
---
## Single-Threaded Design
App state is wrapped in `Rc<Extensions>` for cheap cloning within the single-threaded dispatch system. Since CLI apps are fundamentally single-threaded (parse → run one handler → output → exit), there are no thread-safety requirements on app state values.
```rust
// Both work - no Send + Sync requirements
app_state(Database { pool: Pool::new() })
app_state(Wrapper { rc: Rc::new(data) }) // Works fine
```
---
## Summary
- **App state** = shared, immutable, configured at build time
- **Extensions** = per-request, mutable, set by hooks
- Use `get_required` for mandatory dependencies
- Hooks can read app state to populate extensions
- Both types use the same `Extensions` API for access