arcature 0.1.0

Arcature: an opinionated full-stack Rust web framework. One package, batteries included.
Documentation
# Getting started

## Requirements

- Rust **1.97.1** or newer (edition 2024). `rust-toolchain.toml` pins `stable`.
- **PostgreSQL 17** for anything using the database or the job queue.
- Node.js, only if you are building a frontend with Vite. Arcature itself
  publishes no npm package.

## Installing

Arcature is not on crates.io yet, so depend on the repository:

```toml
[dependencies]
arcature = { git = "https://github.com/ArcatureLabs/Arcature" }
```

Once it is published, `cargo add arcature` will be the whole install.

## The smallest application

```rust,ignore
use arcature::application::EngineResult;
use arcature::prelude::*;

#[arcature::main]
async fn main() -> EngineResult<()> {
    Application::new()
        .routes(Routes::new([Route::get("/", index).name("home")]))
        .build()
        .run()
        .await
}

async fn index() -> Result<Response> {
    Ok(text(StatusCode::OK, "hello"))
}
```

Three things to notice.

`.build()` is required. `Application::new()` returns an `ApplicationBuilder`;
`.run()` lives on `Application`. Forgetting `.build()` is a type error, not a
runtime surprise.

`run()` returns `EngineResult<()>`, not the framework's `Result<()>`, and
`EngineResult` is not in the prelude — it lives at
`arcature::application::EngineResult`. Engine failures (a port already bound, a
database that will not connect) are a different kind of failure from a
handler's, and they deliberately do not share an error type.

Handlers return `Result<Response>`, where `Result` is Arcature's. `text`,
`json`, `redirect` and `no_content` build the common shapes.

## The generated application

`arc new` writes a Laravel-shaped project rather than a single file:

```text
app/
  controllers/   models/     services/
  requests/      policies/   resources/
bootstrap/
  app.rs         state.rs
config/
database/migrations/
routes/mod.rs
resources/js/    resources/css/
public/
storage/
src/main.rs      src/lib.rs
tests/smoke.rs
.env
```

`bootstrap/app.rs` is the composition root. It loads `.env`, reads typed
configuration, and wires the subsystems:

```rust,ignore
pub fn app() -> Result<Application<crate::bootstrap::AppState>> {
    dotenvy::dotenv().ok();
    let config = crate::config::load()?;
    Ok(Application::new()
        .routes(crate::routes::routes())
        .bind(&config.bind_addr)
        .port(config.port)
        .database(config.database)
        .cache(config.cache)
        .storage(config.storage)
        .mail(config.mail)
        .jobs(jobs_registry())
        .build())
}
```

`bootstrap/state.rs` defines `AppState`, the cloneable bundle every handler
reaches through `State<AppState>`. Each field is an `Option`, because a
subsystem that was never configured contributes `None` rather than a panic:

```rust,ignore
#[derive(Clone)]
pub struct AppState {
    pub db: Option<Db>,
    pub jobs: Option<Jobs>,
    pub cache: Option<Cache>,
    pub storage: Option<Storage>,
    pub mail: Option<Mailer>,
}
```

The state is produced *after* startup, from the started `Resources`, which is
why it is a closure rather than a value:

```rust,ignore
pub fn state_fn() -> Arc<dyn Fn(&Resources, &Lifecycle) -> AppState + Send + Sync> {
    Arc::new(|res, _lc| AppState {
        db: res.db().cloned(),
        jobs: res.jobs().cloned(),
        cache: res.cache().cloned(),
        storage: res.storage().cloned(),
        mail: res.mail().cloned(),
    })
}
```

`src/lib.rs` puts the two together with `run_with_state`.

## Features

Arcature's features reduce the compile surface; they are not a self-assembly
kit. `default` is a working full-stack application. Turn features *off* to
compile less, not on to reach a usable state.

```toml
# The whole framework.
arcature = { git = "...", features = ["fullstack"] }

# An API server: no Inertia, no static assets pipeline.
arcature = { git = "...", default-features = false, features = ["api", "database", "auth", "validation"] }
```

The database driver is split three ways — `db-postgres`, `db-sqlite`,
`db-mysql` — so a SQLite application does not compile the PostgreSQL protocol.
The job queue requires PostgreSQL.

## Next

[Routing](routing.md) for how requests reach handlers, or
[Inertia](inertia.md) if you are building a page-driven frontend.