pavex/blueprint/blueprint.rs
1use super::Constructor;
2use super::ErrorHandler;
3use super::ErrorObserver;
4use super::Fallback;
5use super::Import;
6use super::PostProcessingMiddleware;
7use super::PreProcessingMiddleware;
8use super::Prebuilt;
9use super::RegisteredConstructor;
10use super::RegisteredErrorHandler;
11use super::RegisteredImport;
12use super::Route;
13use super::WrappingMiddleware;
14use super::conversions::{coordinates2coordinates, created_at2created_at, sources2sources};
15use super::nesting::RoutingModifiers;
16use super::{
17 Config, RegisteredConfig, RegisteredFallback, RegisteredPostProcessingMiddleware,
18 RegisteredPreProcessingMiddleware, RegisteredRoute, RegisteredRoutes,
19 RegisteredWrappingMiddleware,
20};
21use crate::blueprint::RegisteredErrorObserver;
22use crate::blueprint::RegisteredPrebuilt;
23use pavex_bp_schema::Blueprint as BlueprintSchema;
24use pavex_bp_schema::Location;
25
26/// The structure of your Pavex application.
27///
28/// # Guide
29///
30/// Check out the ["Project structure"](https://pavex.dev/docs/guide/project_structure) section of
31/// Pavex's guide for more details on the role of [`Blueprint`] in Pavex applications.
32///
33/// # Overview
34///
35/// A blueprint keeps track of:
36///
37/// - [Routes](https://pavex.dev/docs/guide/routing/), registered via [`.routes()`][`Blueprint::route`] and [`.route()`][`Blueprint::route`]
38/// - [Middlewares](https://pavex.dev/docs/guide/middleware/), registered via [`.pre_process()`][`Blueprint::pre_process`], [`.wrap()`][`Blueprint::wrap`] and
39/// [`.post_process()`][`Blueprint::post_process`]
40/// - [Error observers](https://pavex.dev/docs/guide/errors/error_observers/), registered via [`.error_observer()`][`Blueprint::error_observer`]
41/// - [Constructors](https://pavex.dev/docs/guide/dependency_injection/), imported via [`.import()`][`Blueprint::import`] or registered via [`.constructor()`][`Blueprint::constructor`]
42/// - [Configuration types](https://pavex.dev/docs/guide/configuration/), imported via [`.import()`][`Blueprint::import`] or registered via [`.config()`][`Blueprint::config`]
43/// - [Prebuilt types](https://pavex.dev/docs/guide/dependency_injection/prebuilt_types/), imported via [`.import()`][`Blueprint::import`] or registered via [`.prebuilt()`][`Blueprint::prebuilt`]
44/// - [Error handlers](https://pavex.dev/docs/guide/errors/error_handlers/), imported via [`.import()`][`Blueprint::import`] or registered via [`.error_handler()`][`Blueprint::error_handler`]
45/// - Fallback routes, registered via [`.fallback()`][`Blueprint::fallback`]
46///
47/// You can also decompose your application into smaller sub-components
48/// using [`.nest()`][`Blueprint::nest`], [`.prefix()`][`Blueprint::prefix`] and [`.domain()`][`Blueprint::domain`].
49///
50/// A blueprint can be serialized via [`.persist()`][`Blueprint::persist`] and forwarded to Pavex's CLI
51/// to (re)generate the [server SDK crate](https://pavex.dev/docs/guide/project_structure/server_sdk/).
52///
53/// # Example
54///
55/// ```rust
56/// use pavex::{Blueprint, blueprint::from};
57///
58/// # pub fn _blueprint(
59/// # LOGGER: pavex::blueprint::WrappingMiddleware,
60/// # ERROR_LOGGER: pavex::blueprint::ErrorObserver,
61/// # RESPONSE_LOGGER: pavex::blueprint::PostProcessingMiddleware) {
62/// let mut bp = Blueprint::new();
63/// // Bring into scope constructors, error handlers and configuration
64/// // types defined in the crates listed via `from!`.
65/// bp.import(from![
66/// // Local components, defined in this crate
67/// crate,
68/// // Components defined in the `pavex` crate,
69/// // by the framework itself.
70/// pavex,
71/// ]);
72///
73/// // Attach a `tracing` span to every incoming request.
74/// bp.wrap(LOGGER);
75/// // Log the status code of every response.
76/// bp.post_process(RESPONSE_LOGGER);
77/// // Capture the error message and source chain
78/// // of every unhandled error.
79/// bp.error_observer(ERROR_LOGGER);
80///
81/// // Register all routes defined in this crate,
82/// // prepending `/api` to their paths.
83/// bp.prefix("/api").routes(from![crate]);
84/// # }
85/// ```
86pub struct Blueprint {
87 pub(super) schema: BlueprintSchema,
88}
89
90impl Default for Blueprint {
91 #[track_caller]
92 fn default() -> Self {
93 Self {
94 schema: BlueprintSchema {
95 creation_location: Location::caller(),
96 components: Vec::new(),
97 },
98 }
99 }
100}
101
102impl Blueprint {
103 #[track_caller]
104 /// Create a new [`Blueprint`].
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 #[track_caller]
110 /// Import all constructors, error handlers, configuration and prebuilt types defined in the target modules.
111 ///
112 /// Components that have been annotated with Pavex's macros (e.g. `#[singleton]`) aren't automatically
113 /// considered when resolving the dependency graph for your application.\
114 /// They need to be explicitly imported using one or more invocations of this method.
115 ///
116 /// # Guide
117 ///
118 /// Check out the ["Dependency Injection"](https://pavex.dev/docs/guide/dependency_injection) section of Pavex's guide
119 /// for a thorough introduction to dependency injection in Pavex applications.
120 ///
121 /// # Wildcard import
122 ///
123 /// You can import all components defined in the current crate and its direct dependencies using the wildcard source, `*`:
124 ///
125 /// ```rust
126 /// use pavex::{blueprint::from, Blueprint};
127 ///
128 /// # fn main() {
129 /// let mut bp = Blueprint::new();
130 /// bp.import(from![*]);
131 /// # }
132 /// ```
133 ///
134 /// # All local components
135 ///
136 /// Use `crate` as source to import all components defined in the current crate:
137 ///
138 /// ```rust
139 /// use pavex::{blueprint::from, Blueprint};
140 ///
141 /// # fn main() {
142 /// let mut bp = Blueprint::new();
143 /// bp.import(from![crate]);
144 /// # }
145 /// ```
146 ///
147 /// # Specific modules
148 ///
149 /// You can restrict the import to modules:
150 ///
151 /// ```rust
152 /// use pavex::{blueprint::from, Blueprint};
153 ///
154 /// # fn main() {
155 /// let mut bp = Blueprint::new();
156 /// // It will only import components defined
157 /// // in the `crate::a` and `crate::b` modules.
158 /// bp.import(from![crate::a, crate::b]);
159 /// # }
160 /// ```
161 ///
162 /// # Dependencies
163 ///
164 /// You can import components from a dependency using the same mechanism:
165 ///
166 /// ```rust
167 /// use pavex::{blueprint::from, Blueprint};
168 ///
169 /// # fn main() {
170 /// let mut bp = Blueprint::new();
171 /// // Import components from the `pavex_session` and
172 /// // `pavex_session_sqlx` crates.
173 /// bp.import(from![pavex_session, pavex_session_sqlx]);
174 /// # }
175 /// ```
176 ///
177 /// The specified crates must be direct dependencies of the current crate.
178 pub fn import(&mut self, import: Import) -> RegisteredImport<'_> {
179 let import = pavex_bp_schema::Import {
180 sources: sources2sources(import.sources),
181 relative_to: import.relative_to.to_owned(),
182 created_at: created_at2created_at(import.created_at),
183 registered_at: Location::caller(),
184 };
185 let component_id = self.push_component(import);
186 RegisteredImport {
187 blueprint: &mut self.schema,
188 component_id,
189 }
190 }
191
192 #[track_caller]
193 /// Register all the routes defined in the target modules.
194 ///
195 /// Components that have been annotated with Pavex's macros (e.g. `#[pavex::get]`) aren't automatically
196 /// added to your application.\
197 /// They need to be explicitly imported using this method or [`.route()`](Blueprint::route).
198 ///
199 /// # Guide
200 ///
201 /// Check out the ["Routing"](https://pavex.dev/docs/guide/routing) section of Pavex's guide
202 /// for a thorough introduction to routing in Pavex applications.
203 ///
204 /// Check out [`.route()`](Blueprint::route)'s documentation to learn how routes are defined.
205 ///
206 /// # All local routes
207 ///
208 /// Use `crate` as source to register all the routes defined in the current crate:
209 ///
210 /// ```rust
211 /// use pavex::{blueprint::from, Blueprint};
212 ///
213 /// # fn main() {
214 /// let mut bp = Blueprint::new();
215 /// bp.routes(from![crate]);
216 /// # }
217 /// ```
218 ///
219 /// # Specific modules
220 ///
221 /// You can restrict the scope to specific modules:
222 ///
223 /// ```rust
224 /// use pavex::{blueprint::from, Blueprint};
225 ///
226 /// # fn main() {
227 /// let mut bp = Blueprint::new();
228 /// // It will only register routes defined
229 /// // in the `crate::routes::user` and `crate::routes::post` modules.
230 /// bp.routes(from![
231 /// crate::routes::user,
232 /// crate::routes::post
233 /// ]);
234 /// # }
235 /// ```
236 ///
237 /// # Dependencies
238 ///
239 /// You can register routes defined in one of your dependencies using the same mechanism:
240 ///
241 /// ```rust
242 /// use pavex::{blueprint::from, Blueprint};
243 ///
244 /// # fn main() {
245 /// let mut bp = Blueprint::new();
246 /// // Register request handlers from the `pavex_session` crate
247 /// bp.routes(from![pavex_session]);
248 /// # }
249 /// ```
250 ///
251 /// The specified crates must be direct dependencies of the current crate.
252 ///
253 /// # Wildcard import
254 ///
255 /// You can import all routes defined in the current crate and its direct dependencies using the wildcard source, `*`:
256 ///
257 /// ```rust
258 /// use pavex::{blueprint::from, Blueprint};
259 ///
260 /// # fn main() {
261 /// let mut bp = Blueprint::new();
262 /// bp.routes(from![*]);
263 /// # }
264 /// ```
265 ///
266 /// This is generally discouraged.
267 pub fn routes(&mut self, import: Import) -> RegisteredRoutes<'_> {
268 let import = pavex_bp_schema::RoutesImport {
269 sources: sources2sources(import.sources),
270 relative_to: import.relative_to.to_owned(),
271 created_at: created_at2created_at(import.created_at),
272 registered_at: Location::caller(),
273 };
274 let component_id = self.push_component(import);
275 RegisteredRoutes {
276 blueprint: &mut self.schema,
277 component_id,
278 }
279 }
280
281 #[track_caller]
282 /// Register a route to handle incoming requests.
283 ///
284 /// You can register at most one route for any given [path](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Path) and
285 /// [method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) pair.
286 ///
287 /// # Guide
288 ///
289 /// Check out the ["Routing"](https://pavex.dev/docs/guide/routing) section of Pavex's guide
290 /// for a thorough introduction to routing in Pavex applications.
291 ///
292 /// # Example: function route
293 ///
294 /// Add the [`get`](macro@crate::get) attribute to a function to create a route matching `GET` requests
295 /// to the given path:
296 ///
297 /// ```rust
298 /// use pavex::get;
299 /// use pavex::{request::RequestHead, Response};
300 ///
301 /// #[get(path = "/")]
302 /// pub fn get_root(request_head: &RequestHead) -> Response {
303 /// // [...]
304 /// # todo!()
305 /// }
306 /// ```
307 ///
308 /// The [`get`](macro@crate::get) attribute will define a new constant,
309 /// named `GET_ROOT`.\
310 /// Pass the constant to [`Blueprint::route`] to add the newly-defined route to your application:
311 ///
312 /// ```rust
313 /// # use pavex::Blueprint;
314 /// # fn blueprint(GET_ROOT: pavex::blueprint::Route) {
315 /// let mut bp = Blueprint::new();
316 /// bp.route(GET_ROOT);
317 /// # }
318 /// ```
319 ///
320 /// ## Method-specific attributes
321 ///
322 /// Pavex provides attributes for the most common HTTP methods: [`get`](macro@crate::get), [`post`](macro@crate::post), [`put`](macro@crate::put),
323 /// [`patch`](macro@crate::patch), [`delete`](macro@crate::delete), [`head`](macro@crate::head), and [`options`](macro@crate::options).
324 /// Use the [`route`](macro@crate::route) attribute, instead, to define routes that match multiple methods,
325 /// non-standard methods or arbitrary methods.
326 ///
327 /// # Example: method route
328 ///
329 /// You're not limited to free functions. Methods can be used as routes too:
330 ///
331 /// ```rust
332 /// use pavex::methods;
333 /// use pavex::request::RequestHead;
334 ///
335 /// pub struct LoginController(/* .. */);
336 ///
337 /// #[methods]
338 /// impl LoginController {
339 /// #[get(path = "/login")]
340 /// pub fn get(head: &RequestHead) -> Self {
341 /// // [...]
342 /// # todo!()
343 /// }
344 ///
345 /// #[post(path = "/login")]
346 /// pub fn post(head: &RequestHead) -> Self {
347 /// // [...]
348 /// # todo!()
349 /// }
350 /// }
351 /// ```
352 ///
353 /// For methods, you must add a `#[methods]` annotation on the `impl` block it belongs to,
354 /// in addition to the verb annotation on the method itself.\
355 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
356 ///
357 /// ```rust
358 /// # use pavex::Blueprint;
359 /// # fn blueprint(LOGIN_CONTROLLER_GET: pavex::blueprint::Route, LOGIN_CONTROLLER_POST: pavex::blueprint::Route) {
360 /// let mut bp = Blueprint::new();
361 /// bp.route(LOGIN_CONTROLLER_GET);
362 /// bp.route(LOGIN_CONTROLLER_POST);
363 /// # }
364 /// ```
365 ///
366 /// # Imports
367 ///
368 /// If you have defined multiple routes, you can invoke [`.routes()`][`Blueprint::routes`]
369 /// to register them in bulk:
370 ///
371 /// ```rust
372 /// use pavex::{Blueprint, blueprint::from};
373 ///
374 /// let mut bp = Blueprint::new();
375 /// // Import all the routes defined in the current crate.
376 /// // It's equivalent to invoking `bp.route` for every
377 /// // single route defined in the current crate.
378 /// bp.routes(from![crate]);
379 /// ```
380 ///
381 /// Check out the documentation for [`.routes()`][`Blueprint::routes`] for more information.
382 pub fn route(&mut self, route: Route) -> RegisteredRoute<'_> {
383 let registered = pavex_bp_schema::Route {
384 coordinates: coordinates2coordinates(route.coordinates),
385 registered_at: Location::caller(),
386 error_handler: None,
387 };
388 let component_id = self.push_component(registered);
389 RegisteredRoute {
390 blueprint: &mut self.schema,
391 component_id,
392 }
393 }
394
395 #[track_caller]
396 /// Add a new type to the application's configuration.
397 ///
398 /// # Required traits
399 ///
400 /// Configuration types *must* implement `Debug`, `Clone` and `serde::Deserialize`.
401 ///
402 /// # Guide
403 ///
404 /// Check out the ["Configuration"](https://pavex.dev/docs/guide/configuration)
405 /// section of Pavex's guide for a thorough introduction to Pavex's configuration system.
406 ///
407 /// # Example
408 ///
409 /// Add the [`config`](macro@crate::config) attribute to the type you want to include in
410 /// the configuration for your application:
411 ///
412 /// ```rust
413 /// use pavex::config;
414 ///
415 /// #[config(key = "pool")]
416 /// #[derive(serde::Deserialize, Debug, Clone)]
417 /// pub struct PoolConfig {
418 /// pub max_n_connections: u32,
419 /// pub min_n_connections: u32,
420 /// }
421 /// ```
422 ///
423 /// The [`config`](macro@crate::config) attribute will define a new constant, named `POOL_CONFIG`.\
424 /// Pass the constant to [`Blueprint::config`] to add the new configuration type to your application:
425 ///
426 /// ```rust
427 /// # use pavex::Blueprint;
428 /// # fn blueprint(POOL_CONFIG: pavex::blueprint::Config) {
429 /// let mut bp = Blueprint::new();
430 /// bp.config(POOL_CONFIG);
431 /// # }
432 /// ```
433 ///
434 /// A new field, named `pool` with type `PoolConfig`, will be added to the generated `ApplicationConfig` struct.
435 ///
436 /// # Imports
437 ///
438 /// If you have defined multiple configuration types, you can use an [import](`Blueprint::import`)
439 /// to register them in bulk:
440 ///
441 /// ```rust
442 /// use pavex::{Blueprint, blueprint::from};
443 ///
444 /// let mut bp = Blueprint::new();
445 /// // Import all the types from the current crate that
446 /// // have been annotated with `#[config]`.
447 /// // It's equivalent to calling `bp.config` for
448 /// // every single configuration type defined in the current crate.
449 /// bp.import(from![crate]);
450 /// ```
451 ///
452 /// Check out the documentation for [`Blueprint::import`] for more information.
453 pub fn config(&mut self, config: Config) -> RegisteredConfig<'_> {
454 let registered = pavex_bp_schema::ConfigType {
455 coordinates: coordinates2coordinates(config.coordinates),
456 cloning_policy: None,
457 default_if_missing: None,
458 include_if_unused: None,
459 registered_at: Location::caller(),
460 };
461 let component_id = self.push_component(registered);
462 RegisteredConfig {
463 blueprint: &mut self.schema,
464 component_id,
465 }
466 }
467
468 #[track_caller]
469 /// Register a constructor.
470 ///
471 /// If a constructor for the same type has already been registered, it will be overwritten.
472 ///
473 /// # Guide
474 ///
475 /// Check out the ["Dependency injection"](https://pavex.dev/docs/guide/dependency_injection)
476 /// section of Pavex's guide for a thorough introduction to dependency injection
477 /// in Pavex applications.
478 ///
479 /// # Example: function constructor
480 ///
481 /// Add the [`request_scoped`](macro@crate::request_scoped) attribute to a function to mark it as a
482 /// [request-scoped](crate::blueprint::Lifecycle) constructor:
483 ///
484 /// ```rust
485 /// use pavex::request_scoped;
486 /// use pavex::request::RequestHead;
487 ///
488 /// # struct LogLevel;
489 /// pub struct AuthorizationHeader(/* .. */);
490 ///
491 /// #[request_scoped]
492 /// pub fn extract_authorization(head: &RequestHead) -> AuthorizationHeader {
493 /// // [...]
494 /// # todo!()
495 /// }
496 /// ```
497 ///
498 /// The [`request_scoped`](macro@crate::request_scoped) attribute will define a new constant,
499 /// named `EXTRACT_AUTHORIZATION`.\
500 /// Pass the constant to [`Blueprint::constructor`] to allow other components to inject an instance
501 /// of the `AuthorizationHeader` type as an input parameter.
502 ///
503 /// ```rust
504 /// # use pavex::Blueprint;
505 /// # fn blueprint(EXTRACT_AUTHORIZATION: pavex::blueprint::Constructor) {
506 /// let mut bp = Blueprint::new();
507 /// bp.constructor(EXTRACT_AUTHORIZATION);
508 /// # }
509 /// ```
510 ///
511 /// ## Lifecycles
512 ///
513 /// You can also register constructors with [singleton](crate::blueprint::Lifecycle::Singleton) and
514 /// [transient](crate::blueprint::Lifecycle::Transient) lifecycles. Check out the respective
515 /// macros ([`singleton`](macro@crate::singleton) and [`transient`](macro@crate::transient)) for more
516 /// details.
517 ///
518 /// # Example: method constructor
519 ///
520 /// You're not limited to free functions. Methods can be used as constructors too:
521 ///
522 /// ```rust
523 /// use pavex::methods;
524 /// use pavex::request::RequestHead;
525 ///
526 /// # struct LogLevel;
527 /// pub struct AuthorizationHeader(/* .. */);
528 ///
529 /// #[methods]
530 /// impl AuthorizationHeader {
531 /// #[request_scoped]
532 /// pub fn new(head: &RequestHead) -> Self {
533 /// // [...]
534 /// # todo!()
535 /// }
536 /// }
537 /// ```
538 ///
539 /// For methods, you must add a `#[methods]` annotation on the `impl` block it belongs to,
540 /// in addition to the `#[request_scoped]` annotation on the method itself.\
541 ///
542 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
543 ///
544 /// ```rust
545 /// # use pavex::Blueprint;
546 /// # fn blueprint(AUTHORIZATION_HEADER_NEW: pavex::blueprint::Constructor) {
547 /// let mut bp = Blueprint::new();
548 /// bp.constructor(AUTHORIZATION_HEADER_NEW);
549 /// # }
550 /// ```
551 ///
552 /// # Imports
553 ///
554 /// If you have defined multiple constructors, you can use an [import](`Blueprint::import`)
555 /// to register them in bulk:
556 ///
557 /// ```rust
558 /// use pavex::{Blueprint, blueprint::from};
559 ///
560 /// let mut bp = Blueprint::new();
561 /// // Import all the types from the current crate that
562 /// // have been annotated with either `#[singleton]`,
563 /// // `#[request_scoped]`, `#[transient]` or `#[constructor]`.
564 /// // It's equivalent to invoking `bp.constructor` for every
565 /// // single constructor defined in the current crate.
566 /// bp.import(from![crate]);
567 /// ```
568 ///
569 /// Check out the documentation for [`Blueprint::import`] for more information.
570 pub fn constructor(&mut self, constructor: Constructor) -> RegisteredConstructor<'_> {
571 let registered_constructor = pavex_bp_schema::Constructor {
572 coordinates: coordinates2coordinates(constructor.coordinates),
573 lifecycle: None,
574 cloning_policy: None,
575 error_handler: None,
576 lints: Default::default(),
577 registered_at: Location::caller(),
578 };
579 let component_id = self.push_component(registered_constructor);
580 RegisteredConstructor {
581 component_id,
582 blueprint: &mut self.schema,
583 }
584 }
585
586 #[track_caller]
587 /// Register a wrapping middleware.
588 ///
589 /// # Guide
590 ///
591 /// Check out the ["Middleware"](https://pavex.dev/docs/guide/middleware)
592 /// section of Pavex's guide for a thorough introduction to middlewares
593 /// in Pavex applications.
594 ///
595 /// # Example: function wrapper
596 ///
597 /// Add the [`wrap`](macro@crate::wrap) attribute to a function to mark it as a
598 /// a wrapping middleware:
599 ///
600 /// ```rust
601 /// use pavex::{middleware::Next, Response, wrap};
602 /// use std::time::Duration;
603 /// use tokio::time::{timeout, error::Elapsed};
604 ///
605 /// #[wrap]
606 /// pub async fn timeout_wrapper<C>(next: Next<C>) -> Result<Response, Elapsed>
607 /// where
608 /// C: IntoFuture<Output = Response>
609 /// {
610 /// timeout(Duration::from_secs(2), next.into_future()).await
611 /// }
612 /// ```
613 ///
614 /// The [`wrap`](macro@crate::wrap) attribute will define a new constant,
615 /// named `TIMEOUT_WRAPPER`.\
616 /// Pass the constant to [`Blueprint::wrap`] to add the newly-defined middleware to
617 /// your application:
618 ///
619 /// ```rust
620 /// # use pavex::Blueprint;
621 /// # fn blueprint(TIMEOUT_WRAPPER: pavex::blueprint::WrappingMiddleware) {
622 /// let mut bp = Blueprint::new();
623 /// bp.wrap(TIMEOUT_WRAPPER);
624 /// # }
625 /// ```
626 ///
627 /// # Example: method middleware
628 ///
629 /// You're not limited to free functions. Methods can be used as middlewares too:
630 ///
631 /// ```rust
632 /// use pavex::{middleware::Next, Response, methods};
633 /// use std::time::Duration;
634 /// use tokio::time::{timeout, error::Elapsed};
635 ///
636 /// pub struct TimeoutMiddleware {
637 /// timeout: Duration,
638 /// }
639 ///
640 /// #[methods]
641 /// impl TimeoutMiddleware {
642 /// #[wrap]
643 /// pub async fn execute<C>(&self, next: Next<C>) -> Result<Response, Elapsed>
644 /// where
645 /// C: IntoFuture<Output = Response>
646 /// {
647 /// timeout(self.timeout, next.into_future()).await
648 /// }
649 /// }
650 /// ```
651 ///
652 /// For methods, you must add a `#[methods]` annotation on the `impl` block it belongs to,
653 /// in addition to the `#[wrap]` annotation on the method itself.\
654 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
655 ///
656 /// ```rust
657 /// # use pavex::Blueprint;
658 /// # fn blueprint(TIMEOUT_MIDDLEWARE_EXECUTE: pavex::blueprint::WrappingMiddleware) {
659 /// let mut bp = Blueprint::new();
660 /// bp.wrap(TIMEOUT_MIDDLEWARE_EXECUTE);
661 /// # }
662 /// ```
663 #[doc(alias = "middleware")]
664 pub fn wrap(&mut self, m: WrappingMiddleware) -> RegisteredWrappingMiddleware<'_> {
665 let registered = pavex_bp_schema::WrappingMiddleware {
666 coordinates: coordinates2coordinates(m.coordinates),
667 registered_at: Location::caller(),
668 error_handler: None,
669 };
670 let component_id = self.push_component(registered);
671 RegisteredWrappingMiddleware {
672 blueprint: &mut self.schema,
673 component_id,
674 }
675 }
676
677 #[track_caller]
678 /// Register a post-processing middleware.
679 ///
680 /// # Guide
681 ///
682 /// Check out the ["Middleware"](https://pavex.dev/docs/guide/middleware)
683 /// section of Pavex's guide for a thorough introduction to middlewares
684 /// in Pavex applications.
685 ///
686 /// # Example: function middleware
687 ///
688 /// Add the [`post_process`](macro@crate::post_process) attribute to a function to mark it as a
689 /// a post-processing middleware:
690 ///
691 /// ```rust
692 /// use pavex::{post_process, Response};
693 /// use pavex_tracing::{
694 /// RootSpan,
695 /// fields::{http_response_status_code, HTTP_RESPONSE_STATUS_CODE}
696 /// };
697 ///
698 /// #[post_process]
699 /// pub fn response_logger(response: Response, root_span: &RootSpan) -> Response
700 /// {
701 /// root_span.record(
702 /// HTTP_RESPONSE_STATUS_CODE,
703 /// http_response_status_code(&response),
704 /// );
705 /// response
706 /// }
707 /// ```
708 ///
709 /// The [`post_process`](macro@crate::post_process) attribute will define a new constant,
710 /// named `RESPONSE_LOGGER`.\
711 /// Pass the constant to [`Blueprint::post_process`] to add the newly-defined middleware to
712 /// your application:
713 ///
714 /// ```rust
715 /// # use pavex::Blueprint;
716 /// # fn blueprint(RESPONSE_LOGGER: pavex::blueprint::PostProcessingMiddleware) {
717 /// let mut bp = Blueprint::new();
718 /// bp.post_process(RESPONSE_LOGGER);
719 /// # }
720 /// ```
721 ///
722 /// # Example: method middleware
723 ///
724 /// You're not limited to free functions. Methods can be used as middlewares too:
725 ///
726 /// ```rust
727 /// use pavex::{methods, Response};
728 /// use pavex_tracing::{
729 /// RootSpan,
730 /// fields::{http_response_status_code, HTTP_RESPONSE_STATUS_CODE}
731 /// };
732 ///
733 /// pub struct ResponseLogger {
734 /// log_body_size: bool,
735 /// }
736 ///
737 /// #[methods]
738 /// impl ResponseLogger {
739 /// #[post_process]
740 /// pub fn log(&self, response: Response, root_span: &RootSpan) -> Response
741 /// {
742 /// if self.log_body_size {
743 /// // [...]
744 /// }
745 /// root_span.record(
746 /// HTTP_RESPONSE_STATUS_CODE,
747 /// http_response_status_code(&response),
748 /// );
749 /// response
750 /// }
751 /// }
752 /// ```
753 ///
754 /// For methods, you must add a [`#[methods]`][macro@crate::methods] annotation on the `impl` block it belongs to,
755 /// in addition to the [`#[post_process]`][macro@crate::post_process] annotation on the method itself.\
756 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
757 ///
758 /// ```rust
759 /// # use pavex::Blueprint;
760 /// # fn blueprint(RESPONSE_LOGGER_LOG: pavex::blueprint::PostProcessingMiddleware) {
761 /// let mut bp = Blueprint::new();
762 /// bp.post_process(RESPONSE_LOGGER_LOG);
763 /// # }
764 /// ```
765 #[doc(alias = "middleware")]
766 #[doc(alias = "postprocess")]
767 pub fn post_process(
768 &mut self,
769 m: PostProcessingMiddleware,
770 ) -> RegisteredPostProcessingMiddleware<'_> {
771 let registered = pavex_bp_schema::PostProcessingMiddleware {
772 coordinates: coordinates2coordinates(m.coordinates),
773 registered_at: Location::caller(),
774 error_handler: None,
775 };
776 let component_id = self.push_component(registered);
777 RegisteredPostProcessingMiddleware {
778 blueprint: &mut self.schema,
779 component_id,
780 }
781 }
782
783 #[track_caller]
784 /// Register a pre-processing middleware.
785 ///
786 /// # Guide
787 ///
788 /// Check out the ["Middleware"](https://pavex.dev/docs/guide/middleware)
789 /// section of Pavex's guide for a thorough introduction to middlewares
790 /// in Pavex applications.
791 ///
792 /// # Example: function middleware
793 ///
794 /// Add the [`pre_process`](macro@crate::pre_process) attribute to a function to mark it as a
795 /// a pre-processing middleware:
796 ///
797 /// ```rust
798 /// use pavex::{Blueprint, pre_process, Response};
799 /// use pavex::middleware::Processing;
800 /// use pavex::http::{HeaderValue, header::LOCATION};
801 /// use pavex::request::RequestHead;
802 ///
803 /// /// If the request path ends with a `/`,
804 /// /// redirect to the same path without the trailing `/`.
805 /// #[pre_process]
806 /// pub fn redirect_to_normalized(request_head: &RequestHead) -> Processing
807 /// {
808 /// let Some(normalized_path) = request_head.target.path().strip_suffix('/') else {
809 /// // No need to redirect, we continue processing the request.
810 /// return Processing::Continue;
811 /// };
812 /// let location = HeaderValue::from_str(normalized_path).unwrap();
813 /// let redirect = Response::temporary_redirect().insert_header(LOCATION, location);
814 /// // Short-circuit the request processing pipeline and return the redirect response
815 /// // to the client without invoking downstream middlewares and the request handler.
816 /// Processing::EarlyReturn(redirect)
817 /// }
818 /// ```
819 ///
820 /// The [`pre_process`](macro@crate::pre_process) attribute will define a new constant,
821 /// named `REDIRECT_TO_NORMALIZED`.\
822 /// Pass the constant to [`Blueprint::pre_process`] to add the newly-defined middleware to
823 /// your application:
824 ///
825 /// ```rust
826 /// # use pavex::Blueprint;
827 /// # fn blueprint(REDIRECT_TO_NORMALIZED: pavex::blueprint::PreProcessingMiddleware) {
828 /// let mut bp = Blueprint::new();
829 /// bp.pre_process(REDIRECT_TO_NORMALIZED);
830 /// # }
831 /// ```
832 ///
833 /// # Example: method middleware
834 ///
835 /// You're not limited to free functions. Methods can be used as middlewares too:
836 ///
837 /// ```rust
838 /// use pavex::{methods, Response};
839 /// use pavex::middleware::Processing;
840 /// use pavex::http::{HeaderValue, header::LOCATION};
841 /// use pavex::request::RequestHead;
842 ///
843 /// pub struct PathNormalizer {
844 /// // [...]
845 /// }
846 ///
847 /// #[methods]
848 /// impl PathNormalizer {
849 /// #[pre_process]
850 /// pub fn redirect(request_head: &RequestHead) -> Processing
851 /// {
852 /// // [...]
853 /// # todo!()
854 /// }
855 /// }
856 /// ```
857 ///
858 /// For methods, you must add a [`#[methods]`][macro@crate::methods] annotation on the `impl` block it belongs to,
859 /// in addition to the [`#[pre_process]`][macro@crate::pre_process] annotation on the method itself.\
860 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
861 ///
862 /// ```rust
863 /// # use pavex::Blueprint;
864 /// # fn blueprint(PATH_NORMALIZER_REDIRECT: pavex::blueprint::PreProcessingMiddleware) {
865 /// let mut bp = Blueprint::new();
866 /// bp.pre_process(PATH_NORMALIZER_REDIRECT);
867 /// # }
868 /// ```
869 #[doc(alias = "middleware")]
870 #[doc(alias = "preprocess")]
871 pub fn pre_process(
872 &mut self,
873 m: PreProcessingMiddleware,
874 ) -> RegisteredPreProcessingMiddleware<'_> {
875 let registered = pavex_bp_schema::PreProcessingMiddleware {
876 coordinates: coordinates2coordinates(m.coordinates),
877 registered_at: Location::caller(),
878 error_handler: None,
879 };
880 let component_id = self.push_component(registered);
881 RegisteredPreProcessingMiddleware {
882 blueprint: &mut self.schema,
883 component_id,
884 }
885 }
886
887 /// Nest a [`Blueprint`] under the current [`Blueprint`] (the parent), without adding a [common path prefix](Self::prefix)
888 /// nor a [domain restriction](Self::domain) to its routes.
889 ///
890 /// Check out [`RoutingModifiers::nest`](super::RoutingModifiers::nest) for more details on nesting.
891 #[track_caller]
892 #[doc(alias("scope"))]
893 pub fn nest(&mut self, blueprint: Blueprint) {
894 self.push_component(pavex_bp_schema::NestedBlueprint {
895 blueprint: blueprint.schema,
896 path_prefix: None,
897 domain: None,
898 nested_at: Location::caller(),
899 });
900 }
901
902 #[track_caller]
903 /// A common prefix will be prepended to the path of routes nested under this condition.
904 ///
905 /// ```rust
906 /// use pavex::Blueprint;
907 /// use pavex::get;
908 /// use pavex::Response;
909 ///
910 /// fn app() -> Blueprint {
911 /// let mut bp = Blueprint::new();
912 /// // Adding `/api` as common prefix here
913 /// bp.prefix("/api").nest(api_bp());
914 /// bp
915 /// }
916 ///
917 /// #[get(path = "/version")]
918 /// pub fn get_api_version() -> Response {
919 /// // [...]
920 /// # todo!()
921 /// }
922 ///
923 /// fn api_bp() -> Blueprint {
924 /// let mut bp = Blueprint::new();
925 /// // This will match `GET` requests to `/api/version`.
926 /// bp.route(GET_API_VERSION);
927 /// bp
928 /// }
929 /// # pub fn handler() {}
930 /// ```
931 ///
932 /// You can also add a (sub)domain constraint, in addition to the common prefix:
933 ///
934 /// ```rust
935 /// use pavex::Blueprint;
936 /// use pavex::get;
937 /// use pavex::Response;
938 ///
939 /// fn app() -> Blueprint {
940 /// let mut bp = Blueprint::new();
941 /// bp.prefix("/v1").domain("api.mybusiness.com").nest(api_bp());
942 /// bp
943 /// }
944 ///
945 /// #[get(path = "/about")]
946 /// pub fn get_about() -> Response {
947 /// // [...]
948 /// # todo!()
949 /// }
950 ///
951 /// fn api_bp() -> Blueprint {
952 /// let mut bp = Blueprint::new();
953 /// // This will match `GET` requests to `api.mybusiness.com/v1/about`.
954 /// bp.route(GET_ABOUT);
955 /// bp
956 /// }
957 /// ```
958 ///
959 /// Check out [`Blueprint::domain`] for more details on domain restrictions.
960 ///
961 /// ## Restrictions
962 ///
963 /// `prefix` must be non-empty and it must start with a `/`.
964 /// If you don't want to add a common prefix, check out [`Blueprint::nest`] or [`Blueprint::domain`].
965 ///
966 /// ## Trailing slashes
967 ///
968 /// `prefix` **can't** end with a trailing `/`.
969 /// This would result in routes with two consecutive `/` in their paths—e.g.
970 /// `/prefix//path`—which is rarely desirable.
971 /// If you actually need consecutive slashes in your route, you can add them explicitly to
972 /// the path of the route registered in the nested blueprint:
973 ///
974 /// ```rust
975 /// use pavex::Blueprint;
976 /// use pavex::get;
977 /// use pavex::Response;
978 ///
979 /// fn app() -> Blueprint {
980 /// let mut bp = Blueprint::new();
981 /// bp.prefix("/api").nest(api_bp());
982 /// bp
983 /// }
984 ///
985 /// #[get(path = "//version")]
986 /// pub fn get_api_version() -> Response {
987 /// // [...]
988 /// # todo!()
989 /// }
990 ///
991 /// fn api_bp() -> Blueprint {
992 /// let mut bp = Blueprint::new();
993 /// // This will match `GET` requests to `/api//version`.
994 /// bp.route(GET_API_VERSION);
995 /// bp
996 /// }
997 /// # pub fn handler() {}
998 /// ```
999 pub fn prefix(&mut self, prefix: &str) -> RoutingModifiers<'_> {
1000 RoutingModifiers::empty(&mut self.schema).prefix(prefix)
1001 }
1002
1003 #[track_caller]
1004 /// Only requests to the specified domain will be forwarded to routes nested under this condition.
1005 ///
1006 /// # Example
1007 ///
1008 /// ```rust
1009 /// use pavex::Blueprint;
1010 /// # fn api_routes() -> Blueprint { Blueprint::new() }
1011 /// # fn console_routes() -> Blueprint { Blueprint::new() }
1012 ///
1013 /// let mut bp = Blueprint::new();
1014 ///
1015 /// // We split UI and API routes into separate blueprints,
1016 /// // and we serve them using different subdomains.
1017 /// bp.domain("api.mybusiness.com")
1018 /// .nest(api_routes());
1019 /// bp.domain("console.mybusiness.com")
1020 /// .nest(console_routes());
1021 /// ```
1022 ///
1023 /// You can also prepend a common path prefix to all registered routes, in addition to the
1024 /// domain constraint:
1025 ///
1026 /// ```rust
1027 /// use pavex::Blueprint;
1028 /// use pavex::get;
1029 /// use pavex::Response;
1030 ///
1031 /// fn app() -> Blueprint {
1032 /// let mut bp = Blueprint::new();
1033 /// bp.prefix("/v1").domain("api.mybusiness.com").nest(api_bp());
1034 /// bp
1035 /// }
1036 ///
1037 /// #[get(path = "/about")]
1038 /// pub fn get_about() -> Response {
1039 /// // [...]
1040 /// # todo!()
1041 /// }
1042 ///
1043 /// fn api_bp() -> Blueprint {
1044 /// let mut bp = Blueprint::new();
1045 /// // This will match `GET` requests to `api.mybusiness.com/v1/about`.
1046 /// bp.route(GET_ABOUT);
1047 /// bp
1048 /// }
1049 /// ```
1050 ///
1051 /// Check out [`Blueprint::prefix`] for more details on path prefixes.
1052 ///
1053 /// # Domain detection
1054 ///
1055 /// Domain detection is based on the value of [`Host` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host).
1056 /// If the header is not present in the request, the condition will be considered as not met.
1057 ///
1058 /// Keep in mind that the [`Host` header can be easily spoofed by the client](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/17-Testing_for_Host_Header_Injection),
1059 /// so you should not rely on its value for auth or other security-sensitive operations.
1060 pub fn domain(&mut self, domain: &str) -> RoutingModifiers<'_> {
1061 RoutingModifiers::empty(&mut self.schema).domain(domain)
1062 }
1063
1064 #[track_caller]
1065 /// Register a fallback handler to be invoked when an incoming request does **not** match
1066 /// any of the routes you registered with [`Blueprint::route`].
1067 ///
1068 /// If you don't register a fallback handler, the
1069 /// [default framework fallback](crate::router::default_fallback) will be used instead.
1070 ///
1071 /// If a fallback handler has already been registered against this `Blueprint`,
1072 /// it will be overwritten.
1073 ///
1074 /// # Example
1075 ///
1076 /// ```rust
1077 /// use pavex::{get, fallback, Blueprint};
1078 /// use pavex::Response;
1079 ///
1080 /// #[get(path = "/path")]
1081 /// pub fn get_path() -> Response {
1082 /// // [...]
1083 /// # todo!()
1084 /// }
1085 /// #[fallback]
1086 /// pub fn fallback_handler() -> Response {
1087 /// // [...]
1088 /// # todo!()
1089 /// }
1090 ///
1091 /// # fn main() {
1092 /// let mut bp = Blueprint::new();
1093 /// bp.route(GET_PATH);
1094 /// // The fallback handler will be invoked for all the requests that don't match `/path`.
1095 /// // E.g. `GET /home`, `POST /home`, `GET /home/123`, etc.
1096 /// bp.fallback(FALLBACK_HANDLER);
1097 /// # }
1098 /// ```
1099 ///
1100 /// # Signature
1101 ///
1102 /// A fallback handler is a function (or a method) that returns a [`Response`], either directly
1103 /// (if infallible) or wrapped in a [`Result`] (if fallible).
1104 ///
1105 /// Fallback handlers can take advantage of dependency injection, like any
1106 /// other component.
1107 /// You list what you want to see injected as function parameters
1108 /// and Pavex will inject them for you in the generated code.
1109 ///
1110 /// ## Nesting
1111 ///
1112 /// You can register a single fallback handler for each blueprint.
1113 /// If your application takes advantage of [nesting](Blueprint::nest), you can register
1114 /// a fallback against each nested blueprint in your application as well as one for the
1115 /// top-level blueprint.
1116 ///
1117 /// Let's explore how nesting affects the invocation of fallback handlers.
1118 ///
1119 /// ### Nesting without prefix
1120 ///
1121 /// The fallback registered against a blueprint will be invoked for all the requests that match
1122 /// the path of a route that was **directly** registered against that blueprint, but don't satisfy
1123 /// their method guards.
1124 ///
1125 /// ```rust
1126 /// use pavex::{get, fallback, Blueprint};
1127 /// use pavex::Response;
1128 ///
1129 /// #[get(path = "/home")]
1130 /// pub fn get_home() -> Response {
1131 /// // [...]
1132 /// # todo!()
1133 /// }
1134 ///
1135 /// #[get(path = "/room")]
1136 /// pub fn get_room() -> Response {
1137 /// // [...]
1138 /// # todo!()
1139 /// }
1140 ///
1141 /// #[fallback]
1142 /// pub fn fallback_handler() -> Response {
1143 /// // [...]
1144 /// # todo!()
1145 /// }
1146 ///
1147 /// # fn main() {
1148 /// let mut bp = Blueprint::new();
1149 /// bp.route(GET_HOME);
1150 /// bp.nest({
1151 /// let mut bp = Blueprint::new();
1152 /// bp.route(GET_ROOM);
1153 /// bp.fallback(FALLBACK_HANDLER);
1154 /// bp
1155 /// });
1156 /// # }
1157 /// ```
1158 ///
1159 /// In the example above, `fallback_handler` will be invoked for incoming `POST /room`
1160 /// requests: the path matches the path of a route registered against the nested blueprint
1161 /// (`GET /room`), but the method guard doesn't (`POST` vs `GET`).
1162 /// If the incoming requests don't have `/room` as their path instead (e.g. `GET /street`
1163 /// or `GET /room/123`), they will be handled by the fallback registered against the **parent**
1164 /// blueprint—the top-level one in this case.
1165 /// Since no fallback has been explicitly registered against the top-level blueprint, the
1166 /// [default framework fallback](crate::router::default_fallback) will be used instead.
1167 ///
1168 /// ### Nesting with prefix
1169 ///
1170 /// If the nested blueprint includes a nesting prefix (e.g. `bp.nest_at("/api", api_bp)`),
1171 /// its fallback will **also** be invoked for all the requests that start with the prefix
1172 /// but don't match any of the route paths registered against the nested blueprint.
1173 ///
1174 /// ```rust
1175 /// use pavex::{get, fallback, Blueprint};
1176 /// use pavex::Response;
1177 ///
1178 /// #[get(path = "/home")]
1179 /// pub fn get_home() -> Response {
1180 /// // [...]
1181 /// # todo!()
1182 /// }
1183 ///
1184 /// #[get(path = "/")]
1185 /// pub fn list_rooms() -> Response {
1186 /// // [...]
1187 /// # todo!()
1188 /// }
1189 ///
1190 /// #[fallback]
1191 /// pub fn fallback_handler() -> Response {
1192 /// // [...]
1193 /// # todo!()
1194 /// }
1195 ///
1196 /// # fn main() {
1197 /// let mut bp = Blueprint::new();
1198 /// bp.route(GET_HOME);
1199 /// bp.prefix("/room").nest({
1200 /// let mut bp = Blueprint::new();
1201 /// bp.route(LIST_ROOMS);
1202 /// bp.fallback(FALLBACK_HANDLER);
1203 /// bp
1204 /// });
1205 /// # }
1206 /// ```
1207 ///
1208 /// In the example above, `fallback_handler` will be invoked for both `POST /room`
1209 /// **and** `POST /room/123` requests: the path of the latter doesn't match the path of the only
1210 /// route registered against the nested blueprint (`GET /room/`), but it starts with the
1211 /// prefix of the nested blueprint (`/room`).
1212 ///
1213 /// [`Response`]: crate::Response
1214 pub fn fallback(&mut self, fallback: Fallback) -> RegisteredFallback<'_> {
1215 let registered = pavex_bp_schema::Fallback {
1216 coordinates: coordinates2coordinates(fallback.coordinates),
1217 registered_at: Location::caller(),
1218 error_handler: None,
1219 };
1220 let component_id = self.push_component(registered);
1221 RegisteredFallback {
1222 blueprint: &mut self.schema,
1223 component_id,
1224 }
1225 }
1226
1227 #[track_caller]
1228 /// Register an error observer to intercept and report errors that occur during request handling.
1229 ///
1230 /// # Guide
1231 ///
1232 /// Check out the ["Error observers"](https://pavex.dev/docs/guide/errors/error_observers)
1233 /// section of Pavex's guide for a thorough introduction to error observers
1234 /// in Pavex applications.
1235 ///
1236 /// # Example: function observer
1237 ///
1238 /// ```rust
1239 /// use pavex::error_observer;
1240 /// use tracing_log_error::log_error;
1241 ///
1242 /// #[error_observer]
1243 /// pub fn error_logger(e: &pavex::Error) {
1244 /// log_error!(e, "An error occurred while handling a request");
1245 /// }
1246 /// ```
1247 ///
1248 /// The [`error_observer`](macro@crate::error_observer) attribute will define a new constant,
1249 /// named `ERROR_LOGGER`.\
1250 /// Pass the constant to [`.error_observer()`][`Blueprint::error_observer`] to register
1251 /// the newly defined error observer:
1252 ///
1253 /// ```rust
1254 /// # use pavex::Blueprint;
1255 /// # fn blueprint(ERROR_LOGGER: pavex::blueprint::ErrorObserver) {
1256 /// let mut bp = Blueprint::new();
1257 /// bp.error_observer(ERROR_LOGGER);
1258 /// # }
1259 /// ```
1260 ///
1261 /// # Example: method observer
1262 ///
1263 /// You're not limited to free functions. Methods can be used as error observers too:
1264 ///
1265 /// ```rust
1266 /// use pavex::methods;
1267 /// use tracing_log_error::log_error;
1268 ///
1269 /// pub struct ErrorLogger;
1270 ///
1271 /// #[methods]
1272 /// impl ErrorLogger {
1273 /// #[error_observer]
1274 /// pub fn log(e: &pavex::Error) {
1275 /// log_error!(e, "An error occurred while handling a request");
1276 /// }
1277 /// }
1278 /// ```
1279 ///
1280 /// For methods, you must add a [`#[methods]`](macro@crate::methods) annotation on the `impl` block it belongs to,
1281 /// in addition to the [`#[error_observer]`](macro@crate::error_observer) annotation on the method itself.\
1282 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
1283 ///
1284 /// ```rust
1285 /// # use pavex::Blueprint;
1286 /// # fn blueprint(ERROR_LOGGER_LOG: pavex::blueprint::ErrorObserver) {
1287 /// let mut bp = Blueprint::new();
1288 /// bp.error_observer(ERROR_LOGGER_LOG);
1289 /// # }
1290 /// ```
1291 pub fn error_observer(&mut self, error_observer: ErrorObserver) -> RegisteredErrorObserver<'_> {
1292 let registered = pavex_bp_schema::ErrorObserver {
1293 coordinates: coordinates2coordinates(error_observer.coordinates),
1294 registered_at: Location::caller(),
1295 };
1296 self.push_component(registered);
1297 RegisteredErrorObserver {
1298 blueprint: &mut self.schema,
1299 }
1300 }
1301
1302 #[track_caller]
1303 /// Register an error handler.
1304 ///
1305 /// # Guide
1306 ///
1307 /// Check out the ["Error handlers"](https://pavex.dev/docs/guide/errors/error_handlers)
1308 /// section of Pavex's guide for a thorough introduction to error handlers
1309 /// in Pavex applications.
1310 ///
1311 /// # Example: function handler
1312 ///
1313 /// Add the [`error_handler`](macro@crate::error_handler) attribute to a function to mark it as
1314 /// an error handler:
1315 ///
1316 /// ```rust
1317 /// use pavex::error_handler;
1318 /// use pavex::Response;
1319 ///
1320 /// pub enum LoginError {
1321 /// InvalidCredentials,
1322 /// DatabaseError,
1323 /// }
1324 ///
1325 /// #[error_handler]
1326 /// pub fn login_error_handler(e: &LoginError) -> Response {
1327 /// match e {
1328 /// LoginError::InvalidCredentials => Response::unauthorized(),
1329 /// LoginError::DatabaseError => Response::internal_server_error(),
1330 /// }
1331 /// }
1332 ///```
1333 ///
1334 /// The [`error_handler`](macro@crate::error_handler) attribute will define a new constant,
1335 /// named `LOGIN_ERROR_HANDLER`.\
1336 /// Pass the constant to [`.error_handler()`][`Blueprint::error_handler`] to register
1337 /// the newly defined error handler:
1338 ///
1339 /// ```rust
1340 /// # use pavex::Blueprint;
1341 /// # fn blueprint(LOGIN_ERROR_HANDLER: pavex::blueprint::ErrorHandler) {
1342 /// let mut bp = Blueprint::new();
1343 /// bp.error_handler(LOGIN_ERROR_HANDLER);
1344 /// # }
1345 /// ```
1346 ///
1347 /// # Example: method handler
1348 ///
1349 /// You're not limited to free functions. Methods can be used as error handlers too:
1350 ///
1351 /// ```rust
1352 /// use pavex::methods;
1353 /// use pavex::Response;
1354 ///
1355 /// pub enum LoginError {
1356 /// InvalidCredentials,
1357 /// DatabaseError,
1358 /// }
1359 ///
1360 /// #[methods]
1361 /// impl LoginError {
1362 /// #[error_handler]
1363 /// pub fn to_response(&self) -> Response {
1364 /// match self {
1365 /// LoginError::InvalidCredentials => Response::unauthorized(),
1366 /// LoginError::DatabaseError => Response::internal_server_error(),
1367 /// }
1368 /// }
1369 /// }
1370 /// ```
1371 ///
1372 /// For methods, you must add a [`#[methods]`](macro@crate::methods) annotation on the `impl` block it belongs to,
1373 /// in addition to the [`#[error_handler]`](macro@crate::error_handler) annotation on the method itself.\
1374 /// The generated constant is named `<type_name>_<method_name>`, in constant case:
1375 ///
1376 /// ```rust
1377 /// # use pavex::Blueprint;
1378 /// # fn blueprint(LOGIN_ERROR_TO_RESPONSE: pavex::blueprint::ErrorHandler) {
1379 /// let mut bp = Blueprint::new();
1380 /// bp.error_handler(LOGIN_ERROR_TO_RESPONSE);
1381 /// # }
1382 /// ```
1383 pub fn error_handler(&mut self, m: ErrorHandler) -> RegisteredErrorHandler<'_> {
1384 let registered = pavex_bp_schema::ErrorHandler {
1385 coordinates: coordinates2coordinates(m.coordinates),
1386 registered_at: Location::caller(),
1387 };
1388 self.push_component(registered);
1389 RegisteredErrorHandler {
1390 blueprint: &mut self.schema,
1391 }
1392 }
1393
1394 #[track_caller]
1395 /// Register a type to be used as input parameter to the (generated) `ApplicationState::new`
1396 /// method.
1397 ///
1398 /// # Guide
1399 ///
1400 /// Check out the ["Dependency injection"](https://pavex.dev/docs/guide/dependency_injection)
1401 /// section of Pavex's guide for a thorough introduction to dependency injection
1402 /// in Pavex applications.
1403 pub fn prebuilt(&mut self, prebuilt: Prebuilt) -> RegisteredPrebuilt<'_> {
1404 let registered = pavex_bp_schema::PrebuiltType {
1405 coordinates: coordinates2coordinates(prebuilt.coordinates),
1406 cloning_policy: None,
1407 registered_at: Location::caller(),
1408 };
1409 let component_id = self.push_component(registered);
1410 RegisteredPrebuilt {
1411 blueprint: &mut self.schema,
1412 component_id,
1413 }
1414 }
1415
1416 /// Register a component and return its id (i.e. its index in the `components` vector).
1417 fn push_component(&mut self, component: impl Into<pavex_bp_schema::Component>) -> usize {
1418 let id = self.schema.components.len();
1419 self.schema.components.push(component.into());
1420 id
1421 }
1422}
1423
1424/// Methods to serialize and deserialize a [`Blueprint`].
1425/// These are used to pass the blueprint data to Pavex's CLI.
1426impl Blueprint {
1427 /// Serialize the [`Blueprint`] to a file in RON format.
1428 ///
1429 /// The file is only written to disk if the content of the blueprint has changed.
1430 pub fn persist(&self, filepath: &std::path::Path) -> Result<(), anyhow::Error> {
1431 let config = ron::ser::PrettyConfig::new();
1432 let contents = ron::ser::to_string_pretty(&self.schema, config)?;
1433 persist_if_changed::persist_if_changed(filepath, contents.as_bytes())?;
1434 Ok(())
1435 }
1436
1437 /// Read a RON-encoded [`Blueprint`] from a file.
1438 pub fn load(filepath: &std::path::Path) -> Result<Self, anyhow::Error> {
1439 let file = fs_err::OpenOptions::new().read(true).open(filepath)?;
1440 let value: BlueprintSchema = ron::de::from_reader(&file)?;
1441 Ok(Self { schema: value })
1442 }
1443}