veer 0.3.0

Inertia.js v3 server-side protocol superset for Rust
Documentation
# Props

Props are the data of a page. Veer follows the [prop evaluation model](https://inertiajs.com/docs/v3/core-concepts/the-protocol#prop-evaluation-model) of the Inertia protocol: each prop has a category, and the category decides when the server resolves it and what metadata the page object carries.

## Overview

```rust,ignore
inertia
    .render("Users/Index", UsersIndexProps { users, notifications })
    .lazy("stats", || async { load_stats().await })
    .deferred("activity", "default", || async { load_activity().await })
    .once("plans", || async { load_plans().await })
    .prepend("notifications")
    .match_on("notifications.id")
```

Plain props (the struct) are always resolved. The builder methods attach **closure props**: the closure runs only when the response needs the value. A closure returns any `Serialize` value.

| Method | When the closure runs |
|---|---|
| `lazy(key, f)` / `optional(key, f)` | Only when a partial reload asks for the key |
| `deferred(key, group, f)` | Not on the first response; the client asks for it after the first render |
| `once(key, f)` | One time; the client remembers the value across pages |
| `prop(key, Prop)` | As the `Prop` says (see [Composing with `Prop`](#composing-with-prop)) |

## Partial reloads

The client can ask for a subset of the props of the page it is on (`router.reload({ only: ['users'] })`). Veer applies the rules of the protocol:

- `only` and `except` take dot paths: `only: ['user.name']` returns only that nested value.
- With only `except`, all other props are returned.
- `errors` is always returned.
- A value wrapped in [`Always`](#always-and-merge-wrappers) is always returned.
- If the component of the request is not the component of the response, the reload is a full visit.

## Deferred props

```rust,ignore
.deferred("activity", "default", || async { load_activity().await })
.deferred("related", "sidebar", || async { load_related().await })
```

The first response lists the keys under `deferredProps`, by group. The client then makes one request for each group. On the frontend, use `<Deferred data="activity" fallback={…}>`.

### Rescued props

A deferred prop that can fail should not fail the page. Use `Prop::try_new` with `.rescue()`:

```rust,ignore
.prop("permissions", Prop::try_new(|| async { load_permissions().await }).defer().rescue())
```

On `Err`, veer logs the error, leaves the prop out, and lists its key in `rescuedProps`. The client shows the `rescue` slot of `<Deferred>`. Without `.rescue()`, an `Err` gives a `500`.

## Once props

```rust,ignore
.once("plans", || async { load_plans().await })
```

The value is resolved one time. The client remembers it and sends its key in `X-Inertia-Except-Once-Props` on later requests, and veer then skips the closure. To share a once prop with every page, set it on the config:

```rust,ignore
let cfg = InertiaConfig::new()
    .share_once("countries", |_req| async { load_countries().await });
```

For a custom key, an expiry, or a forced refresh, use `Prop`: `.once_as("key")`, `.until(duration)`, `.fresh()`.

## Merging props

By default, a partial reload replaces a prop. A merge label tells the client to combine the new value with the one it has.

```rust,ignore
inertia
    .render("Feed", json!({ "posts": posts, "notifications": notifications, "chat": chat }))
    .merge("posts")                 // append
    .prepend("notifications")       // prepend
    .deep_merge("chat")             // merge objects recursively
    .match_on("posts.id")           // update an item in place when the id is the same
```

A dot path merges a nested array: `.merge("posts.data")`.

When the client sends `X-Inertia-Reset` for a prop (`router.reload({ reset: ['posts'] })`), veer sends the prop without its merge label, so the client replaces the value.

## Infinite scroll

```rust,ignore
.prop("posts", Prop::scroll(move || async move {
    let page = load_posts(page_no).await;
    (
        PostsPage { data: page.items },
        ScrollMetadata::paged("page", page_no, page.has_more),
    )
}))
```

The closure returns the page value and its cursor. The value holds the items under the wrapper key (`data` by default; change it with `.wrapper("items")`). Veer emits `scrollProps` and a merge label for `<key>.data`, and follows the client's append / prepend intent and reset. On the frontend, use `<InfiniteScroll data="posts">`.

`ScrollMetadata::paged(name, current, has_more)` is for numbered pages. For cursor pagination, build it by hand: `ScrollMetadata::new("cursor").current(cur).next(next).previous(prev)`.

## Composing with `Prop`

`Prop` is the general form. The categories compose, as they do in the protocol: a prop can be deferred and merged, or once and deferred.

| Constructor | Behavior |
|---|---|
| `Prop::new(f)` | Resolved on full visits, and on partial reloads that select it |
| `Prop::try_new(f)` | `f` returns `Result`. `Err` gives a `500`, or a rescued prop with `.rescue()` |
| `Prop::scroll(f)` | An infinite-scroll prop; `f` returns `(value, ScrollMetadata)` |

| Modifier | Behavior |
|---|---|
| `.optional()` | Resolve only on a partial reload that selects the prop |
| `.defer()` / `.group("name")` | Defer, in the `default` group or a named group |
| `.once()` / `.once_as("key")` / `.until(duration)` / `.fresh()` | Once behavior: custom key, expiry, forced refresh |
| `.merge()` / `.prepend()` / `.deep_merge()` | Merge behavior for the prop itself |
| `.append_at("data")` / `.prepend_at("data")` | Merge behavior for a nested path |
| `.match_on("id")` | Field that identifies an item, relative to the prop |
| `.rescue()` | Rescue an `Err` from `try_new` |
| `.wrapper("items")` | Wrapper key of a scroll prop |

A dot path as key puts the prop inside a nested object: `.prop("auth.permissions", …)`.

## Shared props

Shared props are sent with every page: the signed-in user, the app name, feature flags.

```rust,ignore
let cfg = InertiaConfig::new().share(|req| {
    // Put in the request extensions by your auth middleware.
    let user = req.extension::<CurrentUser>().cloned();
    async move {
        serde_json::json!({
            "auth": { "user": user },
            "app": { "name": "Acme" },
        })
    }
});
```

The closure returns any `Serialize` value that serializes to an object, so a struct works too. It gets the `RequestInfo`: the URL, the method, the Inertia headers, and the request extensions (`req.extension::<T>()`).

The extensions are those that the request had when it reached `InertiaLayer`. A middleware that sets one must thus be outside it, which in axum means that its layer comes later:

```rust,ignore
let app = router()
    .layer(InertiaLayer::new(cfg))
    .layer(auth_layer);   // runs first; sets `CurrentUser`
```

A page prop with the same key wins. The page object lists the shared keys in `sharedProps`, which the client uses for instant visits. For a type of your own, implement the `SharedProps` trait and use `InertiaConfig::shared`.

## `Always` and `Merge` wrappers

Two wrapper types mark a value inside your props struct:

```rust,ignore
#[derive(serde::Serialize)]
struct DashboardProps {
    csrf_ready: Always<bool>,          // always sent, also on partial reloads
    notifications: Merge<Vec<Notice>>, // merged by the client
}
```

They work at any depth and through any serialization path (structs, `json!`, hand-built values). A nested wrapper acts at its dot path. A nested `Always` value is kept when the partial reload selects its parent (for example `only: ['auth']` with `except: ['auth.user']`); when the parent is not selected, the parent is not sent, because the client replaces top-level props as a whole. For data that every response must carry, put `Always` on a top-level prop. On the TypeScript side they collapse to the inner type.

## Big integers

JavaScript numbers lose precision above 2^53. With big-integer support on, veer sends each integer outside the safe range as `{"$bigint": "…"}`, and the client (3.8.0 or later) turns it into a `BigInt`.

```rust,ignore
// For one response:
inertia.render("Orders/Show", props).preserve_big_integers(true)

// For every response:
let cfg = InertiaConfig::new().preserve_big_integers(true);
```

Integers inside the safe range stay normal numbers, so the same prop can arrive as a `number` or a `bigint`. Flash data gets the same treatment.