Skip to main content

jerrycan_core/
module.rs

1//! `Module` (spec §4.2): the unit of routing, packaging, and ownership.
2//! Bundles routes, nested subroutes, module-scoped dependencies and middleware.
3//! Flattening composes URL prefixes and layers environments (inner wins).
4
5use crate::dep::{DepEnv, DepFactory};
6use crate::middleware::Middleware;
7use crate::router::MethodRouter;
8use std::sync::Arc;
9
10/// Flask's Blueprint, Rust-grade. Built by route crates' `pub fn module()`.
11pub struct Module {
12    pub(crate) name: String,
13    pub(crate) routes: Vec<(String, MethodRouter)>,
14    pub(crate) mounts: Vec<(String, Module)>,
15    pub(crate) env: DepEnv,
16    pub(crate) middleware: Vec<Arc<dyn Middleware>>,
17}
18
19impl Module {
20    pub fn new(name: impl Into<String>) -> Self {
21        Self {
22            name: name.into(),
23            routes: Vec::new(),
24            mounts: Vec::new(),
25            env: DepEnv::default(),
26            middleware: Vec::new(),
27        }
28    }
29
30    /// Register a path relative to the module's mount point.
31    pub fn route(mut self, path: &str, methods: MethodRouter) -> Self {
32        self.routes.push((path.to_string(), methods));
33        self
34    }
35
36    /// Mount a child module (subroute) under a relative prefix. Nests arbitrarily.
37    pub fn mount(mut self, prefix: &str, child: Module) -> Self {
38        self.mounts.push((prefix.to_string(), child));
39        self
40    }
41
42    /// Module-scoped singleton value; shadows any parent provider of the same type.
43    pub fn provide<T: Send + Sync + 'static>(mut self, value: T) -> Self {
44        self.env.insert_value(value);
45        self
46    }
47
48    /// Module-scoped async factory (request scope); shadows parents likewise.
49    pub fn provide_dep<F, Args, T>(mut self, factory: F) -> Self
50    where
51        F: DepFactory<Args, T>,
52        T: Send + Sync + 'static,
53    {
54        self.env.insert_factory(factory);
55        self
56    }
57
58    /// Module-scoped middleware; runs after the app's and parents' middleware.
59    pub fn middleware<M: Middleware>(mut self, mw: M) -> Self {
60        self.middleware.push(Arc::new(mw));
61        self
62    }
63
64    /// The module's name. Reserved for diagnostics and route-map introspection
65    /// in a later phase; it currently has no runtime consumer.
66    pub fn name(&self) -> &str {
67        &self.name
68    }
69}
70
71/// One route after flattening: absolute path + effective env + middleware chain.
72pub(crate) struct FlatRoute {
73    pub(crate) path: String,
74    pub(crate) methods: MethodRouter,
75    pub(crate) env: Arc<DepEnv>,
76    pub(crate) middleware: Arc<[Arc<dyn Middleware>]>,
77    /// Per-route body cap carried from the `MethodRouter` to the `Endpoint`.
78    pub(crate) body_limit: Option<usize>,
79    /// Per-route handler-time budget carried from `MethodRouter` to `Endpoint` (#111).
80    pub(crate) handler_timeout: Option<std::time::Duration>,
81    /// Per-route body-read deadline carried from `MethodRouter` to `Endpoint` (#111).
82    pub(crate) body_read_timeout: Option<std::time::Duration>,
83}
84
85pub(crate) fn join_paths(prefix: &str, rel: &str) -> String {
86    let a = prefix.trim_end_matches('/');
87    let b = rel.trim_start_matches('/');
88    match (a.is_empty(), b.is_empty()) {
89        (true, true) => "/".to_string(),
90        (false, true) => a.to_string(),
91        (true, false) => format!("/{b}"),
92        (false, false) => format!("{a}/{b}"),
93    }
94}
95
96impl Module {
97    /// Resolution order baked at build time: app env ← parent modules ← this
98    /// module (inner wins); middleware: app's, then parents', then this module's.
99    pub(crate) fn flatten(
100        self,
101        prefix: &str,
102        parent_env: &DepEnv,
103        parent_mw: &[Arc<dyn Middleware>],
104    ) -> Vec<FlatRoute> {
105        let mut merged = parent_env.clone();
106        merged.merge_from(&self.env);
107
108        let mut mw: Vec<Arc<dyn Middleware>> = parent_mw.to_vec();
109        mw.extend(self.middleware);
110
111        let env = Arc::new(merged.clone());
112        let mw_arc: Arc<[Arc<dyn Middleware>]> = Arc::from(mw.clone());
113
114        let mut out = Vec::new();
115        for (path, methods) in self.routes {
116            let body_limit = methods.body_limit;
117            let handler_timeout = methods.handler_timeout;
118            let body_read_timeout = methods.body_read_timeout;
119            out.push(FlatRoute {
120                path: join_paths(prefix, &path),
121                methods,
122                env: env.clone(),
123                middleware: mw_arc.clone(),
124                body_limit,
125                handler_timeout,
126                body_read_timeout,
127            });
128        }
129        for (sub_prefix, child) in self.mounts {
130            let child_prefix = join_paths(prefix, &sub_prefix);
131            out.extend(child.flatten(&child_prefix, &merged, &mw));
132        }
133        out
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::router::get;
141
142    struct Cfg {
143        tag: &'static str,
144    }
145
146    fn leaf_paths(routes: &[FlatRoute]) -> Vec<String> {
147        routes.iter().map(|r| r.path.clone()).collect()
148    }
149
150    #[test]
151    fn nesting_composes_prefixes() {
152        let comments = Module::new("comments").route("/", get(|| async { "list" }));
153        let todos = Module::new("todos")
154            .route("/", get(|| async { "list" }))
155            .route("/{id}", get(|| async { "one" }))
156            .mount("/{id}/comments", comments);
157
158        let flat = todos.flatten("/todos", &DepEnv::default(), &[]);
159        assert_eq!(
160            leaf_paths(&flat),
161            vec!["/todos", "/todos/{id}", "/todos/{id}/comments"]
162        );
163    }
164
165    #[test]
166    fn module_env_shadows_parent_env() {
167        let parent = {
168            let mut e = DepEnv::default();
169            e.insert_value(Cfg { tag: "app" });
170            e
171        };
172        let child = Module::new("sub")
173            .provide(Cfg { tag: "module" })
174            .route("/", get(|| async { "x" }));
175        let flat = child.flatten("/sub", &parent, &[]);
176        let env = &flat[0].env;
177        let got = env
178            .singletons
179            .get(&std::any::TypeId::of::<Cfg>())
180            .and_then(|v| v.clone().downcast::<Cfg>().ok())
181            .unwrap();
182        assert_eq!(got.tag, "module");
183    }
184
185    #[test]
186    fn middleware_chains_accumulate_parent_first() {
187        struct Named(#[allow(dead_code)] &'static str);
188        impl Middleware for Named {
189            fn handle<'a>(
190                &'a self,
191                ctx: &'a mut crate::RequestCtx,
192                next: crate::middleware::Next<'a>,
193            ) -> crate::middleware::MiddlewareFuture<'a> {
194                next.run(ctx)
195            }
196        }
197        let inner = Module::new("inner")
198            .middleware(Named("inner"))
199            .route("/", get(|| async { "x" }));
200        let outer = Module::new("outer")
201            .middleware(Named("outer"))
202            .mount("/inner", inner);
203        let flat = outer.flatten("/outer", &DepEnv::default(), &[]);
204        assert_eq!(flat[0].middleware.len(), 2, "outer then inner");
205    }
206}