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}
80
81pub(crate) fn join_paths(prefix: &str, rel: &str) -> String {
82    let a = prefix.trim_end_matches('/');
83    let b = rel.trim_start_matches('/');
84    match (a.is_empty(), b.is_empty()) {
85        (true, true) => "/".to_string(),
86        (false, true) => a.to_string(),
87        (true, false) => format!("/{b}"),
88        (false, false) => format!("{a}/{b}"),
89    }
90}
91
92impl Module {
93    /// Resolution order baked at build time: app env ← parent modules ← this
94    /// module (inner wins); middleware: app's, then parents', then this module's.
95    pub(crate) fn flatten(
96        self,
97        prefix: &str,
98        parent_env: &DepEnv,
99        parent_mw: &[Arc<dyn Middleware>],
100    ) -> Vec<FlatRoute> {
101        let mut merged = parent_env.clone();
102        merged.merge_from(&self.env);
103
104        let mut mw: Vec<Arc<dyn Middleware>> = parent_mw.to_vec();
105        mw.extend(self.middleware);
106
107        let env = Arc::new(merged.clone());
108        let mw_arc: Arc<[Arc<dyn Middleware>]> = Arc::from(mw.clone());
109
110        let mut out = Vec::new();
111        for (path, methods) in self.routes {
112            let body_limit = methods.body_limit;
113            out.push(FlatRoute {
114                path: join_paths(prefix, &path),
115                methods,
116                env: env.clone(),
117                middleware: mw_arc.clone(),
118                body_limit,
119            });
120        }
121        for (sub_prefix, child) in self.mounts {
122            let child_prefix = join_paths(prefix, &sub_prefix);
123            out.extend(child.flatten(&child_prefix, &merged, &mw));
124        }
125        out
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::router::get;
133
134    struct Cfg {
135        tag: &'static str,
136    }
137
138    fn leaf_paths(routes: &[FlatRoute]) -> Vec<String> {
139        routes.iter().map(|r| r.path.clone()).collect()
140    }
141
142    #[test]
143    fn nesting_composes_prefixes() {
144        let comments = Module::new("comments").route("/", get(|| async { "list" }));
145        let todos = Module::new("todos")
146            .route("/", get(|| async { "list" }))
147            .route("/{id}", get(|| async { "one" }))
148            .mount("/{id}/comments", comments);
149
150        let flat = todos.flatten("/todos", &DepEnv::default(), &[]);
151        assert_eq!(
152            leaf_paths(&flat),
153            vec!["/todos", "/todos/{id}", "/todos/{id}/comments"]
154        );
155    }
156
157    #[test]
158    fn module_env_shadows_parent_env() {
159        let parent = {
160            let mut e = DepEnv::default();
161            e.insert_value(Cfg { tag: "app" });
162            e
163        };
164        let child = Module::new("sub")
165            .provide(Cfg { tag: "module" })
166            .route("/", get(|| async { "x" }));
167        let flat = child.flatten("/sub", &parent, &[]);
168        let env = &flat[0].env;
169        let got = env
170            .singletons
171            .get(&std::any::TypeId::of::<Cfg>())
172            .and_then(|v| v.clone().downcast::<Cfg>().ok())
173            .unwrap();
174        assert_eq!(got.tag, "module");
175    }
176
177    #[test]
178    fn middleware_chains_accumulate_parent_first() {
179        struct Named(#[allow(dead_code)] &'static str);
180        impl Middleware for Named {
181            fn handle<'a>(
182                &'a self,
183                ctx: &'a mut crate::RequestCtx,
184                next: crate::middleware::Next<'a>,
185            ) -> crate::middleware::MiddlewareFuture<'a> {
186                next.run(ctx)
187            }
188        }
189        let inner = Module::new("inner")
190            .middleware(Named("inner"))
191            .route("/", get(|| async { "x" }));
192        let outer = Module::new("outer")
193            .middleware(Named("outer"))
194            .mount("/inner", inner);
195        let flat = outer.flatten("/outer", &DepEnv::default(), &[]);
196        assert_eq!(flat[0].middleware.len(), 2, "outer then inner");
197    }
198}