Skip to main content

autumn_web/
lib.rs

1//! # Autumn
2//!
3//! An opinionated, convention-over-configuration web framework for Rust.
4//!
5//! Autumn assembles proven Rust crates ([Axum], [Maud], [Diesel], htmx, Tailwind)
6//! into a Spring Boot-style developer experience with proc-macro-driven
7//! conventions and escape hatches at every level.
8//!
9//! ## Quick start
10//!
11//! ```rust,no_run
12//! use autumn_web::prelude::*;
13//!
14//! #[get("/")]
15//! async fn index() -> Markup {
16//!     html! { h1 { "Hello, Autumn!" } }
17//! }
18//!
19//! #[autumn_web::main]
20//! async fn main() {
21//!     autumn_web::app()
22//!         .routes(routes![index])
23//!         .run()
24//!         .await;
25//! }
26//! ```
27//!
28//! ## Architecture overview
29//!
30//! | Layer | Crate | Purpose |
31//! |-------|-------|---------|
32//! | HTTP server | [Axum] | Routing, extractors, middleware |
33//! | HTML templates | [Maud] | Type-safe, compiled HTML via `html!` macro |
34//! | Database | [Diesel] | Async Postgres via `diesel-async` + deadpool |
35//! | Client interactivity | htmx | Embedded JS served at `/static/js/htmx.min.js` |
36//! | Styling | Tailwind CSS | Downloaded + managed by `autumn-cli` |
37//!
38//! ## Modules
39//!
40//! - [`mod@app`] -- Application builder for configuring and launching the server.
41//! - [`config`] -- Layered configuration: defaults, `autumn.toml`, env overrides.
42//! - [`db`] -- Database connection pool and the [`Db`] request extractor.
43//! - [`error`] -- Framework error type ([`AutumnError`]) and result alias.
44//! - [`extract`] -- Re-exported Axum extractors ([`Form`](axum::extract::Form),
45//!   [`Json`], [`Path`](axum::extract::Path), [`Query`](axum::extract::Query)).
46//! - [`health`] -- Auto-mounted health check endpoint.
47//! - [`logging`] -- Structured logging via `tracing-subscriber`.
48//! - [`middleware`] -- Built-in middleware (request IDs).
49//! - [`prelude`] -- Glob import for the most common types.
50//! - [`route`] -- Route descriptor used by macro-generated code.
51//!
52//! ## Zero-config defaults
53//!
54//! An Autumn app runs out of the box with no configuration file. Every
55//! setting has a sensible default (port 3000, `info` log level, etc.).
56//! Override via `autumn.toml` or `AUTUMN_*` environment variables.
57//! See [`config::AutumnConfig`] for the full list.
58//!
59//! [Axum]: https://docs.rs/axum
60//! [Maud]: https://maud.lambda.xyz
61//! [Diesel]: https://diesel.rs
62
63pub mod app;
64pub mod config;
65#[cfg(feature = "db")]
66pub mod db;
67pub mod error;
68pub mod extract;
69pub mod health;
70#[cfg(feature = "htmx")]
71pub(crate) mod htmx;
72pub mod logging;
73pub mod middleware;
74pub mod prelude;
75pub mod route;
76
77/// Create a new [`app::AppBuilder`] for configuring and launching an Autumn server.
78///
79/// This is the primary entry point for every Autumn application.
80///
81/// # Examples
82///
83/// ```rust,no_run
84/// use autumn_web::prelude::*;
85///
86/// #[get("/")]
87/// async fn index() -> &'static str { "hello" }
88///
89/// #[autumn_web::main]
90/// async fn main() {
91///     autumn_web::app()
92///         .routes(routes![index])
93///         .run()
94///         .await;
95/// }
96/// ```
97pub use app::app;
98/// Async database connection extractor.
99///
100/// Declare `db: Db` in a handler signature to get a pooled Postgres
101/// connection. See [`db::Db`] for full documentation and examples.
102#[cfg(feature = "db")]
103pub use db::Db;
104
105/// Framework error type and result alias.
106///
107/// [`AutumnError`] wraps any `Error + Send + Sync` with an HTTP status code.
108/// [`AutumnResult<T>`] is `Result<T, AutumnError>`.
109/// See the [`error`] module for details.
110pub use error::{AutumnError, AutumnResult};
111/// htmx version string embedded in the binary.
112///
113/// Useful for cache-busting or diagnostic logging. The corresponding
114/// minified JS is served automatically at `/static/js/htmx.min.js`.
115#[cfg(feature = "htmx")]
116pub use htmx::HTMX_VERSION;
117
118// ── Proc-macro re-exports ──────────────────────────────────────────
119
120/// Annotate an async function as a `DELETE` route handler.
121///
122/// Generates a companion function that returns a [`route::Route`]
123/// pairing the path with an Axum handler. In debug builds
124/// `#[axum::debug_handler]` is applied automatically for better error
125/// messages (zero cost in release).
126///
127/// # Examples
128///
129/// ```rust,no_run
130/// use autumn_web::prelude::*;
131///
132/// #[delete("/items/{id}")]
133/// async fn remove_item() -> &'static str {
134///     "removed"
135/// }
136/// ```
137pub use autumn_macros::delete;
138
139/// Annotate an async function as a `GET` route handler.
140///
141/// Generates a companion function that returns a [`route::Route`]
142/// pairing the path with an Axum handler. In debug builds
143/// `#[axum::debug_handler]` is applied automatically for better error
144/// messages (zero cost in release).
145///
146/// # Examples
147///
148/// ```rust,no_run
149/// use autumn_web::prelude::*;
150///
151/// #[get("/hello")]
152/// async fn hello() -> &'static str {
153///     "Hello, Autumn!"
154/// }
155/// ```
156pub use autumn_macros::get;
157
158/// Set up the Tokio async runtime for an Autumn application.
159///
160/// A thin wrapper around `#[tokio::main]`. The real framework setup
161/// happens inside [`app::AppBuilder::run`].
162///
163/// # Examples
164///
165/// ```rust,no_run
166/// use autumn_web::prelude::*;
167///
168/// #[get("/")]
169/// async fn index() -> &'static str { "hi" }
170///
171/// #[autumn_web::main]
172/// async fn main() {
173///     autumn_web::app()
174///         .routes(routes![index])
175///         .run()
176///         .await;
177/// }
178/// ```
179pub use autumn_macros::main;
180
181/// Derive Diesel and Serde traits for a database model struct.
182///
183/// Applies `Queryable`, `Selectable`, `Insertable`, `Serialize`, and
184/// `Deserialize` derives plus a `#[diesel(table_name = ...)]` attribute.
185/// The table name is either specified explicitly or inferred from the
186/// struct name (`PascalCase` -> `snake_case` + `s`).
187///
188/// # Examples
189///
190/// Explicit table name:
191///
192/// ```rust,ignore
193/// use autumn_web::model;
194///
195/// #[model(table = "users")]
196/// pub struct User {
197///     pub id: i32,
198///     pub name: String,
199/// }
200/// ```
201///
202/// Inferred table name (`BlogPost` -> `blog_posts`):
203///
204/// ```rust,ignore
205/// use autumn_web::model;
206///
207/// #[model]
208/// pub struct BlogPost {
209///     pub id: i32,
210///     pub title: String,
211/// }
212/// ```
213#[cfg(feature = "db")]
214pub use autumn_macros::model;
215
216/// Annotate an async function as a `POST` route handler.
217///
218/// Generates a companion function that returns a [`route::Route`]
219/// pairing the path with an Axum handler. In debug builds
220/// `#[axum::debug_handler]` is applied automatically for better error
221/// messages (zero cost in release).
222///
223/// # Examples
224///
225/// ```rust,no_run
226/// use autumn_web::prelude::*;
227///
228/// #[post("/items")]
229/// async fn create_item() -> &'static str {
230///     "created"
231/// }
232/// ```
233pub use autumn_macros::post;
234
235/// Annotate an async function as a `PUT` route handler.
236///
237/// Generates a companion function that returns a [`route::Route`]
238/// pairing the path with an Axum handler. In debug builds
239/// `#[axum::debug_handler]` is applied automatically for better error
240/// messages (zero cost in release).
241///
242/// # Examples
243///
244/// ```rust,no_run
245/// use autumn_web::prelude::*;
246///
247/// #[put("/items/{id}")]
248/// async fn update_item() -> &'static str {
249///     "updated"
250/// }
251/// ```
252pub use autumn_macros::put;
253
254/// Collect route-annotated handlers into a `Vec<Route>`.
255///
256/// Each handler must have been annotated with a route macro ([`get`],
257/// [`post`], [`put`], [`delete`]) which generates a companion
258/// `__autumn_route_info_{name}()` function.
259///
260/// # Examples
261///
262/// ```rust,no_run
263/// use autumn_web::prelude::*;
264///
265/// #[get("/hello")]
266/// async fn hello() -> &'static str { "hello" }
267///
268/// #[post("/create")]
269/// async fn create() -> &'static str { "created" }
270///
271/// # #[autumn_web::main]
272/// # async fn main() {
273/// let all_routes = routes![hello, create];
274/// autumn_web::app().routes(all_routes).run().await;
275/// # }
276/// ```
277pub use autumn_macros::routes;
278
279// ── Maud re-exports ────────────────────────────────────────────────
280
281/// Rendered HTML fragment produced by the [`html!`] macro.
282///
283/// This is the standard return type for handlers that render HTML.
284/// Re-exported from [Maud](https://maud.lambda.xyz).
285///
286/// # Examples
287///
288/// ```rust,no_run
289/// use autumn_web::prelude::*;
290///
291/// #[get("/")]
292/// async fn index() -> Markup {
293///     html! { h1 { "Welcome" } }
294/// }
295/// ```
296#[cfg(feature = "maud")]
297pub use maud::Markup;
298
299/// Wrap a pre-escaped string so Maud renders it verbatim.
300///
301/// Use this when you have HTML that was already escaped or generated
302/// by another system and you want to embed it in a Maud template
303/// without double-escaping.
304///
305/// Re-exported from [Maud](https://maud.lambda.xyz).
306///
307/// # Examples
308///
309/// ```rust
310/// use autumn_web::PreEscaped;
311///
312/// let raw_html = PreEscaped("<em>already escaped</em>".to_string());
313/// ```
314#[cfg(feature = "maud")]
315pub use maud::PreEscaped;
316
317/// Type-safe HTML templating macro.
318///
319/// Produces a [`Markup`] value containing compiled HTML.
320/// Re-exported from [Maud](https://maud.lambda.xyz). See the
321/// [Maud book](https://maud.lambda.xyz) for full syntax reference.
322///
323/// # Examples
324///
325/// ```rust
326/// use autumn_web::html;
327///
328/// let greeting = "world";
329/// let page = html! {
330///     h1 { "Hello, " (greeting) "!" }
331/// };
332/// ```
333#[cfg(feature = "maud")]
334pub use maud::html;
335
336/// JSON request body extractor and response type.
337///
338/// When used as a handler parameter, deserializes the request body as JSON.
339/// When returned from a handler, serializes the value as JSON with
340/// `Content-Type: application/json`.
341///
342/// Re-exported from [Axum](https://docs.rs/axum). See
343/// [`axum::Json`] for full documentation.
344///
345/// # Examples
346///
347/// ```rust,no_run
348/// use autumn_web::prelude::*;
349/// use serde::{Deserialize, Serialize};
350///
351/// #[derive(Deserialize)]
352/// struct CreateItem { name: String }
353///
354/// #[derive(Serialize)]
355/// struct Item { id: i32, name: String }
356///
357/// #[post("/items")]
358/// async fn create(Json(input): Json<CreateItem>) -> Json<Item> {
359///     Json(Item { id: 1, name: input.name })
360/// }
361/// ```
362pub use crate::extract::Json;
363
364/// Re-exports of upstream crates used in macro-generated code.
365///
366/// These are public so that code generated by `autumn-macros` can reference
367/// them as `autumn_web::reexports::axum`, etc. without requiring the user to
368/// add those crates as direct dependencies.
369///
370/// **For advanced use cases only.** Prefer the types re-exported in
371/// [`prelude`] or at the crate root. Reach into `reexports` when you
372/// need direct access to the underlying framework types (e.g.,
373/// `autumn_web::reexports::axum::Router` for custom middleware).
374///
375/// # Available crates
376///
377/// | Crate | Re-exported as | Use case |
378/// |-------|---------------|----------|
379/// | `axum` | `autumn_web::reexports::axum` | Custom routers, middleware, extractors |
380/// | `diesel` | `autumn_web::reexports::diesel` | Raw Diesel queries, schema types |
381/// | `http` | `autumn_web::reexports::http` | HTTP types (`StatusCode`, `Method`, headers) |
382/// | `tokio` | `autumn_web::reexports::tokio` | Async runtime, spawn, timers |
383pub mod reexports {
384    pub use axum;
385    #[cfg(feature = "db")]
386    pub use diesel;
387    pub use http;
388    pub use tokio;
389}
390
391/// Shared application state passed to all route handlers.
392///
393/// Holds framework-managed resources such as the database connection pool.
394/// Axum requires handler state to be [`Clone`], so internal resources use
395/// `Arc` or are already cheaply cloneable (`deadpool::Pool` is `Arc`-wrapped
396/// internally).
397///
398/// This struct is normally constructed by [`app::AppBuilder::run`] and
399/// should not need to be created manually. It is public so that custom
400/// Axum extractors can access framework resources via
401/// `State<AppState>`.
402///
403/// # Examples
404///
405/// ```rust
406/// use autumn_web::AppState;
407///
408/// // State without a database (e.g., for testing)
409/// let state = AppState { pool: None };
410/// ```
411#[derive(Clone)]
412pub struct AppState {
413    /// Database connection pool, or `None` when no `database.url` is configured.
414    #[cfg(feature = "db")]
415    pub pool:
416        Option<diesel_async::pooled_connection::deadpool::Pool<diesel_async::AsyncPgConnection>>,
417}
418
419impl std::fmt::Debug for AppState {
420    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421        #[cfg(feature = "db")]
422        {
423            f.debug_struct("AppState")
424                .field(
425                    "pool",
426                    &self
427                        .pool
428                        .as_ref()
429                        .map(|p| format!("Pool(max={})", p.status().max_size)),
430                )
431                .finish()
432        }
433        #[cfg(not(feature = "db"))]
434        {
435            f.debug_struct("AppState").finish()
436        }
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn app_state_debug_without_pool() {
446        let state = AppState {
447            #[cfg(feature = "db")]
448            pool: None,
449        };
450        let debug = format!("{state:?}");
451        assert!(debug.contains("AppState"));
452    }
453
454    #[cfg(feature = "db")]
455    #[test]
456    fn app_state_debug_with_pool() {
457        let config = config::DatabaseConfig {
458            url: Some("postgres://localhost/test".into()),
459            pool_size: 5,
460            ..Default::default()
461        };
462        let pool = db::create_pool(&config).unwrap().unwrap();
463        let state = AppState { pool: Some(pool) };
464        let debug = format!("{state:?}");
465        assert!(debug.contains("Pool(max=5)"));
466    }
467
468    fn require_clone<T: Clone>(t: &T) -> T {
469        t.clone()
470    }
471
472    #[test]
473    fn app_state_is_clone() {
474        let state = AppState {
475            #[cfg(feature = "db")]
476            pool: None,
477        };
478        let _cloned = require_clone(&state);
479    }
480
481    #[test]
482    fn app_fn_creates_builder() {
483        let builder = app::app();
484        // Just verify it compiles and can accept routes
485        let _builder = builder.routes(vec![]);
486    }
487}