Skip to main content

doido_controller/
route_table.rs

1//! Process-global table of the application's routes.
2//!
3//! The `routes!` macro registers every `(method, path)` it expands via
4//! [`register_routes`] as the router is built. Because the generated app builds
5//! its router (`routes::router()`) before handing it to the CLI, the table is
6//! populated by the time `doido server` or `doido routes` runs — letting both
7//! print the route list without introspecting axum's opaque `Router`.
8
9use std::sync::Mutex;
10
11/// One registered route (or method group) of the application.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct RouteEntry {
14    /// HTTP method(s), e.g. `GET` or `PUT|PATCH`.
15    pub method: String,
16    /// URL path pattern, e.g. `/posts/{id}`.
17    pub path: String,
18}
19
20static ROUTES: Mutex<Vec<RouteEntry>> = Mutex::new(Vec::new());
21
22/// Records the application's routes, replacing any previously registered set.
23/// Called by `routes!`-generated code.
24pub fn register_routes(entries: Vec<RouteEntry>) {
25    if let Ok(mut guard) = ROUTES.lock() {
26        *guard = entries;
27    }
28}
29
30/// Returns a snapshot of the registered routes, in declaration order.
31pub fn all_routes() -> Vec<RouteEntry> {
32    ROUTES.lock().map(|g| g.clone()).unwrap_or_default()
33}
34
35/// Renders the route table as aligned `METHOD  PATH` lines.
36pub fn format_routes() -> String {
37    let routes = all_routes();
38    if routes.is_empty() {
39        return "No routes defined.".to_string();
40    }
41    let width = routes.iter().map(|r| r.method.len()).max().unwrap_or(0);
42    let mut out = String::new();
43    out.push_str(&format!(
44        "{:<width$}  {}\n",
45        "METHOD",
46        "PATH",
47        width = width
48    ));
49    for r in &routes {
50        out.push_str(&format!(
51            "{:<width$}  {}\n",
52            r.method,
53            r.path,
54            width = width
55        ));
56    }
57    out
58}
59
60/// Prints the route table to stdout. This is the `doido routes` command's
61/// primary data output (like `ls` listing files), so it is written directly
62/// rather than through the logger. Server startup logs the table via tracing
63/// instead — see [`format_routes`].
64pub fn print_routes() {
65    print!("{}", format_routes());
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn register_and_format_round_trip() {
74        register_routes(vec![
75            RouteEntry {
76                method: "GET".to_string(),
77                path: "/".to_string(),
78            },
79            RouteEntry {
80                method: "PUT|PATCH".to_string(),
81                path: "/posts/{id}".to_string(),
82            },
83        ]);
84
85        let entries = all_routes();
86        assert_eq!(entries.len(), 2);
87        assert_eq!(entries[0].path, "/");
88
89        let table = format_routes();
90        assert!(table.contains("METHOD"));
91        assert!(table.contains("GET        /")); // padded to the wider "PUT|PATCH"
92        assert!(table.contains("PUT|PATCH  /posts/{id}"));
93    }
94}