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}