Skip to main content

anvil_dev/
lib.rs

1//! Anvilforge dev-mode hot-reload runtime.
2//!
3//! Wraps the structural complexity of dylib hot-patching behind a single
4//! typed ABI. User handler crates expose one function:
5//!
6//! ```ignore
7//! // in handlers crate src/lib.rs
8//! use anvilforge::prelude::*;
9//!
10//! #[no_mangle]
11//! pub extern "Rust" fn anvil_register_routes(r: &mut RouteSink) {
12//!     r.route("GET", "/posts", routes::list_posts);
13//!     r.route("POST", "/posts", routes::create_post);
14//! }
15//! ```
16//!
17//! The host binary uses `anvil_dev::live_server(...)` (or implicitly via
18//! `anvil dev --hot`) to load this function, watch the dylib for changes, and
19//! re-register routes on reload. The framework Container stays alive across
20//! reloads — DB pools, sessions, Spark snapshots, WebSocket subscribers all
21//! survive.
22//!
23//! Compromise budget:
24//! - State INSIDE the dylib (statics, thread-locals, lazy_static) is reset on
25//!   reload. Move state into the framework Container if you need it to persist.
26//! - ABI changes (signature of a registered route) require a full restart.
27//!   The launcher detects this and prints a clean message.
28//! - Debuggers may lose breakpoint state across reloads; see README.
29
30use std::collections::HashMap;
31use std::sync::Arc;
32
33use anvil_core::Container;
34use axum::body::Body;
35use axum::http::{Request, Response};
36use axum::routing::{any, MethodRouter};
37use axum::Router as AxumRouter;
38use parking_lot::Mutex;
39
40/// The typed ABI a handler dylib exports. A `RouteSink` is handed to the
41/// dylib on each (re)load; the dylib calls `.route(...)` once per route it
42/// owns. Routes registered on reload replace the previous set atomically.
43pub struct RouteSink {
44    entries: Vec<RouteEntry>,
45}
46
47pub struct RouteEntry {
48    pub method: String,
49    pub path: String,
50    pub handler: HandlerBox,
51}
52
53/// Type-erased async handler. The dylib returns a future-producing closure.
54pub type HandlerFn = Box<
55    dyn Fn(Request<Body>) -> futures::future::BoxFuture<'static, Response<Body>>
56        + Send
57        + Sync
58        + 'static,
59>;
60
61pub struct HandlerBox(pub HandlerFn);
62
63impl RouteSink {
64    pub fn new() -> Self {
65        Self {
66            entries: Vec::new(),
67        }
68    }
69
70    /// Register a route. The handler is the user's normal axum handler boxed
71    /// into a uniform `HandlerFn` shape.
72    pub fn route(&mut self, method: &str, path: &str, handler: HandlerFn) {
73        self.entries.push(RouteEntry {
74            method: method.to_string(),
75            path: path.to_string(),
76            handler: HandlerBox(handler),
77        });
78    }
79
80    pub fn into_entries(self) -> Vec<RouteEntry> {
81        self.entries
82    }
83}
84
85impl Default for RouteSink {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91/// Build an axum `MethodRouter` for a single registered route. Used by the
92/// runtime to construct the live router after each reload.
93pub fn handler_to_method_router(method: &str, handler_box: HandlerBox) -> MethodRouter {
94    let m = method.to_ascii_uppercase();
95    let handler = Arc::new(handler_box.0);
96    let handler_clone = handler.clone();
97
98    // We delegate every supported HTTP method to the same handler — axum's
99    // `any` works for arbitrary methods. For specific methods, use
100    // `method_router_for`.
101    let mr: MethodRouter = match m.as_str() {
102        "GET" => axum::routing::get(move |req: Request<Body>| {
103            let h = handler_clone.clone();
104            async move { (h)(req).await }
105        }),
106        "POST" => axum::routing::post(move |req: Request<Body>| {
107            let h = handler_clone.clone();
108            async move { (h)(req).await }
109        }),
110        "PUT" => axum::routing::put(move |req: Request<Body>| {
111            let h = handler_clone.clone();
112            async move { (h)(req).await }
113        }),
114        "PATCH" => axum::routing::patch(move |req: Request<Body>| {
115            let h = handler_clone.clone();
116            async move { (h)(req).await }
117        }),
118        "DELETE" => axum::routing::delete(move |req: Request<Body>| {
119            let h = handler_clone.clone();
120            async move { (h)(req).await }
121        }),
122        _ => any(move |req: Request<Body>| {
123            let h = handler.clone();
124            async move { (h)(req).await }
125        }),
126    };
127    mr
128}
129
130/// The shared state between the launcher and dylib. The Container persists
131/// across reloads; routes get rebuilt every time the dylib registers itself.
132pub struct LiveState {
133    pub container: Container,
134    pub current_router: Mutex<AxumRouter>,
135}
136
137impl LiveState {
138    pub fn new(container: Container) -> Self {
139        Self {
140            container,
141            current_router: Mutex::new(AxumRouter::new()),
142        }
143    }
144
145    /// Replace the live router with a new one built from `entries`. Called by
146    /// the watcher after the dylib reloads and re-runs `anvil_register_routes`.
147    pub fn install(&self, entries: Vec<RouteEntry>) {
148        let mut router = AxumRouter::new();
149        for e in entries {
150            let mr = handler_to_method_router(&e.method, e.handler);
151            router = router.route(&e.path, mr);
152        }
153        *self.current_router.lock() = router;
154    }
155}
156
157/// A re-loadable registry index used so reloads can replace previously
158/// registered entries by class/path key (when needed). Not strictly required
159/// for routes (we just rebuild the whole table) but kept here for parity with
160/// other inventory-driven anvil registries.
161#[derive(Default)]
162pub struct RegistryGeneration {
163    pub seq: parking_lot::Mutex<u64>,
164}
165
166impl RegistryGeneration {
167    pub fn bump(&self) -> u64 {
168        let mut g = self.seq.lock();
169        *g += 1;
170        *g
171    }
172    pub fn current(&self) -> u64 {
173        *self.seq.lock()
174    }
175}
176
177pub static GENERATION: once_cell::sync::Lazy<RegistryGeneration> =
178    once_cell::sync::Lazy::new(RegistryGeneration::default);
179
180/// Helper used by the `anvil dev --hot` runtime to discover whether the host
181/// process is running in hot-reload mode.
182pub fn is_hot_mode() -> bool {
183    std::env::var("ANVIL_HOT").ok().as_deref() == Some("1")
184}
185
186/// Used by anvil-dev's bundled handler types so consumers don't have to depend
187/// on raw http types directly.
188pub use http;
189
190// Re-export so derive macros / inventory submissions can refer to the same
191// concrete types the host expects.
192pub use parking_lot;
193
194#[allow(unused_imports)]
195use serde::{Deserialize, Serialize};
196
197/// One-time payload the dylib sends to the host on registration, carrying
198/// metadata about what it registered. Useful for diagnostics + auto-restart
199/// detection (we can spot ABI mismatches by checking version + entry count).
200#[derive(Debug, Clone)]
201pub struct RegistrationManifest {
202    pub generation: u64,
203    pub route_count: usize,
204    pub abi_version: u32,
205}
206
207pub const ABI_VERSION: u32 = 1;
208
209#[allow(dead_code)]
210fn _route_handlers_link() {
211    let _ = HashMap::<String, u32>::new();
212}