# Guards

> A node's precondition, written once as a Rust function, with a refusal sentence every surface shows.

Some screens should only open when something is true: the trash when it has something in it, checkout when the cart is not empty. A guard is that precondition, registered with the node it protects. A node guards its own door.

## Writing a guard

A guard is a plain function from the store to a result:

```rust
use carte::{Refusal, Scope, Store};

fn trash_has_something(store: &Store) -> Result<(), Refusal> {
    match store.read::<Shared>(Scope::Process) {
        Some(s) if !s.trashed.is_empty() => Ok(()),
        _ => Err(Refusal::new("The trash is empty.")),
    }
}
```

Its type is `Guard`, which is `fn(&Store) -> Result<(), Refusal>`. Register it in the table:

```rust
t.under("trash", "home")
    .guard("trash", trash_has_something);
```

## The refusal is a sentence

A `Refusal` carries one sentence, written once where the rule lives. `Refusal::new` takes anything that converts to a `String`, so a refusal can name the facts: "Needs two more players." Read it back with `sentence()`, or print it, since `Refusal` implements `Display`.

Every place that explains the refusal shows that same sentence. No view composes its own, so the reason on a greyed-out button can never disagree with the reason the screen did not open.

## When guards run

**In the walk.** Before the walk steps onto a child, it asks that child's guard over the committed store. If the guard refuses, the walk stops at the parent. The parent keeps the frame, as if it had named no child. Entering is a request the guard rules on, never something that happens first and is checked after.

The router keeps the last refusal until the next walk:

```rust
if let Some((id, why)) = router.refused() {
    // e.g. show a toast: "The trash is empty."
}
```

It is re-derived on every walk, never remembered. A refusal is a reading of the facts right now.

**Ahead of time.** `Router::ask(id, store)` runs the same guard without going anywhere. Use it for the button that greys itself out:

```rust
let door = v.router.ask("trash", v.store);
let button = ui.add_enabled(door.is_ok(), egui::Button::new(label));
let button = match &door {
    Err(why) => button.on_disabled_hover_text(why.sentence()),
    Ok(()) => button,
};
```

One function, one evaluation, two surfaces. An unguarded node always answers `Ok(())`.

Roots are never asked. Your root function put them on the path, and only it can take them off.

## A refused claim is still a claim

A guard stops the walk. It does not change your model. If `home_child` is `Some("trash")` and the trash is empty, the path stops at `home`, but `home` is still claiming `trash`. The moment the guard passes, the trash opens, whether or not the user just asked for it.

Clear the claim yourself when that is not what you want. The notes example does this when the last trashed note is purged:

```rust
Action::Purge(i) => {
    self.notes.remove(*i);
    if self.notes.iter().all(|n| !n.trashed) && self.home_child == Some("trash") {
        self.home_child = None;
    }
}
```

## Limits

- **One guard per node.** Registering a second one is a startup error, `TableError::Redeclared`, even if it is the same function. Two places that both think they own a door is the bug.
- **A function pointer, not a closure.** A guard cannot capture anything. That keeps the table `Clone` and registration free of allocation. If a guard needs configuration, put the configuration in the store, where the facts it rules on already live.
- **Store only.** A guard sees the `Store` and nothing else, not your model. If a fact matters to a guard, a producer must put it in a scope.
- **No rule language.** The table will never grow a predicate DSL. Anything you can write in Rust over the store, a guard can check.

## Next

That is the routing half. The state half starts with [Scopes and lifetimes](/docs/state/scopes).
