Skip to main content

rustapi_core/
auto_route.rs

1//! Auto-route registration using linkme distributed slices
2//!
3//! **Implementation detail**: This module provides the current backend for
4//! automatic, zero-boilerplate route collection using the `linkme` crate's
5//! distributed slice mechanism.
6//!
7//! This is an internal implementation detail of `RustApi::auto()`.
8//! The public contract is only:
9//! - Handlers annotated with the route macros are discovered automatically.
10//! - `collect_auto_routes()` and `auto_route_count()` for introspection.
11//!
12//! We may change the underlying mechanism in the future (while keeping the
13//! observable behavior stable).
14//!
15//! This module enables **zero-config route registration**. Routes decorated with
16//! `#[rustapi_rs::get]`, `#[rustapi_rs::post]`, etc. are automatically collected at
17//! **link time** using the [`linkme`](https://docs.rs/linkme) crate.
18//!
19//! `RustApi::auto()` (and `RustApi::config()`) rely on this mechanism.
20//!
21//! # How It Works
22//!
23//! The attribute macros emit a small static initializer that appends a factory
24//! function into a `linkme::distributed_slice`. At runtime we simply iterate the
25//! slice and build the router.
26//!
27//! After collection we sort routes into a `BTreeMap` so registration order is
28//! deterministic regardless of link order.
29//!
30//! # Public API
31//!
32//! - [`collect_auto_routes`] – returns all discovered routes as `Vec<Route>`
33//! - [`auto_route_count`] – cheap way to check how many handlers were linked in
34//!
35//! # Example
36//!
37//! ```rust,ignore
38//! use rustapi_rs::prelude::*;
39//!
40//! #[rustapi_rs::get("/users")]
41//! async fn list_users() -> Json<Vec<User>> {
42//!     Json(vec![])
43//! }
44//!
45//! #[rustapi_rs::post("/users")]
46//! async fn create_user(Json(body): Json<CreateUser>) -> Created<User> {
47//!     // ...
48//! }
49//!
50//! #[rustapi_rs::main]
51//! async fn main() -> Result<()> {
52//!     // No manual .route() calls needed!
53//!     RustApi::auto()
54//!         .run("0.0.0.0:8080")
55//!         .await
56//! }
57//! ```
58//!
59//! # Limitations & Known Gotchas
60//!
61//! Link-time registration is powerful but comes with trade-offs:
62//!
63//! - **Tests can be flaky** — the test binary sometimes links differently than the
64//!   main binary. You may see `auto_route_count() == 0` inside `#[test]` even if
65//!   your annotated functions exist. Use `collect_auto_routes()` + filtering or
66//!   fall back to manual `.route()` in tests when necessary.
67//!
68//! - **Non-executable artifacts** (cdylib, rlib, staticlib, wasm32, etc.) often do
69//!   **not** populate distributed slices reliably because the linker may discard
70//!   the registration code.
71//!
72//! - **Multiple separate binaries** in the same workspace each get their own
73//!   independent slice. Routes defined in one binary are invisible to another.
74//!
75//! - There is **no runtime unregistration**. Once linked, the routes are there for
76//!   the lifetime of the process.
77//!
78//! - If you see zero routes at runtime with `RustApi::auto()`, the most common
79//!   causes are:
80//!   1. No handlers were annotated with the route attributes.
81//!   2. The module containing the handler was not linked into this binary.
82//!   3. You are inside a test, library, or cdylib target.
83//!
84//! A clear warning is now emitted automatically when `RustApi::auto()` collects
85//! zero routes (see `mount_auto_routes_grouped`).
86//!
87//! You can always inspect the situation with:
88//!
89//! ```rust,ignore
90//! println!("Auto routes discovered: {}", rustapi_rs::auto_route_count());
91//! ```
92//!
93//! # Manual Registration (always available)
94//!
95//! If auto-registration causes problems in your environment, you can ignore the
96//! attribute macros entirely and use the classic builder:
97//!
98//! ```rust,ignore
99//! RustApi::new()
100//!     .route("/users", get(list_users).post(create_user))
101//!     .run("0.0.0.0:8080")
102//!     .await
103//! ```
104//!
105//! Both styles can be mixed freely.
106
107use crate::handler::Route;
108use linkme::distributed_slice;
109
110/// Distributed slice containing all auto-registered route factory functions.
111///
112/// Each element is a function that returns a [`Route`] when called.
113/// The macro `#[rustapi::get]`, `#[rustapi::post]`, etc. automatically
114/// add entries to this slice at compile/link time.
115#[distributed_slice]
116pub static AUTO_ROUTES: [fn() -> Route];
117
118/// Collect all auto-registered routes.
119///
120/// This function iterates over the distributed slice and calls each
121/// route factory function to produce the actual [`Route`] instances.
122///
123/// # Returns
124///
125/// A vector containing all routes that were registered using the
126/// `#[rustapi::get]`, `#[rustapi::post]`, etc. macros.
127///
128/// # Example
129///
130/// ```rust,ignore
131/// let routes = collect_auto_routes();
132/// println!("Found {} auto-registered routes", routes.len());
133/// ```
134pub fn collect_auto_routes() -> Vec<Route> {
135    AUTO_ROUTES.iter().map(|f| f()).collect()
136}
137
138/// Get the count of auto-registered routes without collecting them.
139///
140/// This is useful for:
141/// - Debugging (e.g. in tests or startup logs)
142/// - Asserting that your annotated handlers were actually linked in
143///
144/// # Example
145///
146/// ```rust,ignore
147/// let count = rustapi_core::auto_route_count();
148/// if count == 0 {
149///     eprintln!("Warning: No auto-routes were discovered!");
150/// }
151/// ```
152pub fn auto_route_count() -> usize {
153    AUTO_ROUTES.len()
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_auto_routes_slice_exists() {
162        // The slice always exists (even if empty). This test mainly ensures
163        // the linkme static was emitted correctly by the build.
164        let _count = auto_route_count();
165    }
166
167    #[test]
168    fn test_collect_auto_routes_does_not_panic() {
169        // Collection must be safe even when no annotated handlers are present
170        // in the current test binary (very common situation).
171        let routes = collect_auto_routes();
172        // We don't assert a specific count here because linkme behavior in
173        // test binaries is not guaranteed to be the same as in the final binary.
174        let _ = routes;
175    }
176
177    #[test]
178    fn test_auto_route_count_is_accessible() {
179        // Public API smoke test – users and integration tests should be able
180        // to call this without importing internal modules.
181        let _ = auto_route_count();
182    }
183}