pavex/blueprint/
nesting.rs

1//! Customize how nested routes should behave.
2
3use pavex_bp_schema::{
4    Blueprint as BlueprintSchema, Domain, Location, NestedBlueprint, PathPrefix,
5};
6
7use crate::Blueprint;
8
9use super::Import;
10
11/// The type returned by [`Blueprint::prefix`] and [`Blueprint::domain`].
12///
13/// Customize routing behaviour for a subset of routes.
14///
15/// [`Blueprint::prefix`]: crate::Blueprint::prefix
16/// [`Blueprint::domain`]: crate::Blueprint::domain
17#[must_use = "`prefix` and `domain` do nothing unless you invoke `nest` to register some routes under them"]
18pub struct RoutingModifiers<'a> {
19    pub(super) blueprint: &'a mut BlueprintSchema,
20    pub(super) path_prefix: Option<PathPrefix>,
21    pub(super) domain: Option<Domain>,
22}
23
24impl<'a> RoutingModifiers<'a> {
25    pub(super) fn empty(blueprint: &'a mut BlueprintSchema) -> Self {
26        Self {
27            blueprint,
28            path_prefix: None,
29            domain: None,
30        }
31    }
32
33    /// Only requests to the specified domain will be forwarded to routes nested under this condition.
34    ///
35    /// Check out [`Blueprint::domain`](crate::Blueprint::domain) for more details.
36    #[track_caller]
37    pub fn domain(mut self, domain: &str) -> Self {
38        let location = Location::caller();
39        self.domain = Some(Domain {
40            domain: domain.into(),
41            registered_at: location,
42        });
43        self
44    }
45
46    /// Prepends a common prefix to all routes nested under this condition.
47    ///
48    /// If a prefix has already been set, it will be overridden.
49    ///
50    /// Check out [`Blueprint::prefix`](crate::Blueprint::prefix) for more details.
51    #[track_caller]
52    pub fn prefix(mut self, prefix: &str) -> Self {
53        let location = Location::caller();
54        self.path_prefix = Some(PathPrefix {
55            path_prefix: prefix.into(),
56            registered_at: location,
57        });
58        self
59    }
60
61    #[track_caller]
62    #[doc(alias("scope"))]
63    /// Nest a [`Blueprint`], optionally applying a [common prefix](`Self::prefix`) and a [domain restriction](`Self::domain`) to all its routes.
64    ///
65    /// Nesting also has consequences when it comes to constructors' visibility.
66    ///
67    /// # Constructors
68    ///
69    /// Constructors registered against the parent blueprint will be available to the nested
70    /// blueprint—they are **inherited**.
71    /// Constructors registered against the nested blueprint will **not** be available to other
72    /// sibling blueprints that are nested under the same parent—they are **private**.
73    ///
74    /// Check out the example below to better understand the implications of nesting blueprints.
75    ///
76    /// ## Visibility
77    ///
78    /// ```rust
79    /// use pavex::{blueprint::from, Blueprint};
80    ///
81    /// fn app() -> Blueprint {
82    ///     let mut bp = Blueprint::new();
83    ///     bp.constructor(DB_CONNECTION_POOL);
84    ///     bp.nest(home_bp());
85    ///     bp.nest(user_bp());
86    ///     bp
87    /// }
88    ///
89    /// /// All property-related routes and constructors.
90    /// fn home_bp() -> Blueprint {
91    ///     let mut bp = Blueprint::new();
92    ///     bp.import(from![crate::home]);
93    ///     bp.routes(from![crate::home]);
94    ///     bp
95    /// }
96    ///
97    /// /// All user-related routes and constructors.
98    /// fn user_bp() -> Blueprint {
99    ///     let mut bp = Blueprint::new();
100    ///     bp.import(from![crate::user]);
101    ///     bp.routes(from![crate::user]);
102    ///     bp
103    /// }
104    ///
105    /// # struct ConnectionPool;
106    /// #[pavex::singleton]
107    /// pub fn db_connection_pool() -> ConnectionPool {
108    ///     // [...]
109    ///     # todo!()
110    /// }
111    ///
112    /// pub mod home {
113    ///     // [...]
114    /// }
115    ///
116    /// pub mod user {
117    ///     # struct Session;
118    ///     pub fn get_session() -> Session {
119    ///         // [...]
120    ///         # todo!()
121    ///     }
122    ///     // [...]
123    /// }
124    /// ```
125    ///
126    /// In this example, we import two constructors:
127    /// - `crate::user::get_session`, for `Session`;
128    /// - `crate::db_connection_pool`, for `ConnectionPool`.
129    ///
130    /// The constructors defined in the `crate::user` module are only imported by the `user_bp` blueprint.
131    /// Since we are **nesting** the `user_bp` blueprint, those constructors will only be available
132    /// to the routes declared in the `user_bp` blueprint.
133    /// If a route declared in `home_bp` tries to inject a `Session`, Pavex will report an error
134    /// at compile-time, complaining that there is no registered constructor for `Session`.
135    /// In other words, all constructors imported in the `user_bp` blueprint are **private**
136    /// and **isolated** from the rest of the application.
137    ///
138    /// The `db_connection_pool` constructor, instead, is declared against the parent blueprint
139    /// and will therefore be available to all routes declared in `home_bp` and `user_bp`—i.e.
140    /// nested blueprints **inherit** all the constructors declared against their parent(s).
141    ///
142    /// ## Precedence
143    ///
144    /// If a constructor is declared against both the parent and one of its nested blueprints, the one
145    /// declared against the nested blueprint takes precedence.
146    ///
147    /// ```rust
148    /// use pavex::{blueprint::from, Blueprint};
149    ///
150    /// fn app() -> Blueprint {
151    ///     let mut bp = Blueprint::new();
152    ///     // These constructors are registered against the root blueprint and they're visible
153    ///     // to all nested blueprints.
154    ///     bp.import(from![crate::global]);
155    ///     bp.nest(user_bp());
156    ///     // [..]
157    ///     bp
158    /// }
159    ///
160    /// fn user_bp() -> Blueprint {
161    ///     let mut bp = Blueprint::new();
162    ///     // They can be overridden by a constructor for the same type registered
163    ///     // against a nested blueprint.
164    ///     // All routes in `user_bp` will use `user::get_session` instead of `global::get_session`.
165    ///     bp.import(from![crate::user]);
166    ///     // [...]
167    ///     bp
168    /// }
169    ///
170    /// pub mod global {
171    ///     # struct Session;
172    ///     pub fn get_session() -> Session {
173    ///         // [...]
174    ///         # todo!()
175    ///     }
176    /// }
177    ///
178    /// pub mod user {
179    ///     # struct Session;
180    ///     pub fn get_session() -> Session {
181    ///         // [...]
182    ///         # todo!()
183    ///     }
184    /// }
185    /// ```
186    ///
187    /// ## Singletons
188    ///
189    /// There is one exception to the precedence rule: [singletons][Lifecycle::Singleton].
190    /// Pavex guarantees that there will be only one instance of a singleton type for the entire
191    /// lifecycle of the application. What should happen if two different constructors are registered for
192    /// the same `Singleton` type by two nested blueprints that share the same parent?
193    /// We can't honor both constructors without ending up with two different instances of the same
194    /// type, which would violate the singleton contract.
195    ///
196    /// It goes one step further! Even if those two constructors are identical, what is the expected
197    /// behaviour? Does the user expect the same singleton instance to be injected in both blueprints?
198    /// Or does the user expect two different singleton instances to be injected in each nested blueprint?
199    ///
200    /// To avoid this ambiguity, Pavex takes a conservative approach: a singleton constructor
201    /// must be registered **exactly once** for each type.
202    /// If multiple nested blueprints need access to the singleton, the constructor must be
203    /// registered against a common parent blueprint—the root blueprint, if necessary.
204    ///
205    /// [Lifecycle::Singleton]: crate::blueprint::Lifecycle::Singleton
206    pub fn nest(self, bp: Blueprint) {
207        self.blueprint.components.push(
208            NestedBlueprint {
209                blueprint: bp.schema,
210                path_prefix: self.path_prefix,
211                nested_at: Location::caller(),
212                domain: self.domain,
213            }
214            .into(),
215        );
216    }
217
218    #[track_caller]
219    /// Register a group of routes.
220    ///
221    /// Their path will be prepended with a common prefix if one was provided via [`.prefix()`][`Self::prefix`].
222    /// They will be restricted to a specific domain if one was specified via [`.domain()`][`Self::domain`].
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// use pavex::{Blueprint, blueprint::from};
228    ///
229    /// let mut bp = Blueprint::new();
230    /// bp.prefix("/api").routes(from![crate::api]);
231    /// ```
232    pub fn routes(self, import: Import) {
233        let mut bp = Blueprint::new();
234        bp.routes(import);
235        self.nest(bp);
236    }
237}