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}