pavex_bp_schema/lib.rs
1//! The schema used by Pavex to serialize and deserialize blueprints.
2//!
3//! There are no guarantees that this schema will remain stable across Pavex versions:
4//! it is considered (for the time being) an internal implementation detail of Pavex's reflection system.
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::fmt::Formatter;
8
9#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
10/// The blueprint for a Pavex application.
11pub struct Blueprint {
12    /// The location where the `Blueprint` was created.
13    pub creation_location: Location,
14    /// All registered components, in the order they were registered.
15    pub components: Vec<Component>,
16}
17
18#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
19#[serde(rename_all = "snake_case")]
20pub enum Component {
21    Constructor(Constructor),
22    WrappingMiddleware(WrappingMiddleware),
23    PostProcessingMiddleware(PostProcessingMiddleware),
24    PreProcessingMiddleware(PreProcessingMiddleware),
25    Route(Route),
26    FallbackRequestHandler(Fallback),
27    NestedBlueprint(NestedBlueprint),
28    ErrorObserver(ErrorObserver),
29    ErrorHandler(ErrorHandler),
30    PrebuiltType(PrebuiltType),
31    ConfigType(ConfigType),
32    Import(Import),
33    RoutesImport(RoutesImport),
34}
35
36impl From<PrebuiltType> for Component {
37    fn from(i: PrebuiltType) -> Self {
38        Self::PrebuiltType(i)
39    }
40}
41
42impl From<ConfigType> for Component {
43    fn from(c: ConfigType) -> Self {
44        Self::ConfigType(c)
45    }
46}
47
48impl From<Constructor> for Component {
49    fn from(c: Constructor) -> Self {
50        Self::Constructor(c)
51    }
52}
53
54impl From<WrappingMiddleware> for Component {
55    fn from(m: WrappingMiddleware) -> Self {
56        Self::WrappingMiddleware(m)
57    }
58}
59
60impl From<PostProcessingMiddleware> for Component {
61    fn from(m: PostProcessingMiddleware) -> Self {
62        Self::PostProcessingMiddleware(m)
63    }
64}
65
66impl From<PreProcessingMiddleware> for Component {
67    fn from(m: PreProcessingMiddleware) -> Self {
68        Self::PreProcessingMiddleware(m)
69    }
70}
71
72impl From<Route> for Component {
73    fn from(r: Route) -> Self {
74        Self::Route(r)
75    }
76}
77
78impl From<Fallback> for Component {
79    fn from(f: Fallback) -> Self {
80        Self::FallbackRequestHandler(f)
81    }
82}
83
84impl From<NestedBlueprint> for Component {
85    fn from(b: NestedBlueprint) -> Self {
86        Self::NestedBlueprint(b)
87    }
88}
89
90impl From<ErrorObserver> for Component {
91    fn from(e: ErrorObserver) -> Self {
92        Self::ErrorObserver(e)
93    }
94}
95
96impl From<ErrorHandler> for Component {
97    fn from(e: ErrorHandler) -> Self {
98        Self::ErrorHandler(e)
99    }
100}
101
102impl From<Import> for Component {
103    fn from(i: Import) -> Self {
104        Self::Import(i)
105    }
106}
107
108impl From<RoutesImport> for Component {
109    fn from(i: RoutesImport) -> Self {
110        Self::RoutesImport(i)
111    }
112}
113
114#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
115pub struct RoutesImport {
116    pub sources: Sources,
117    /// The path of the module where this import was created (i.e. `from!` was invoked).
118    ///
119    /// It is used to resolve relative paths in the `from!` macro, i.e. paths starting
120    /// with `super` or `self`.
121    pub relative_to: String,
122    pub created_at: CreatedAt,
123    pub registered_at: Location,
124}
125
126#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
127pub struct Import {
128    pub sources: Sources,
129    /// The path of the module where this import was created (i.e. `from!` was invoked).
130    ///
131    /// It is used to resolve relative paths in the `from!` macro, i.e. paths starting
132    /// with `super` or `self`.
133    pub relative_to: String,
134    pub created_at: CreatedAt,
135    pub registered_at: Location,
136}
137
138#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
139/// A route registered against a `Blueprint` via `Blueprint::route`.
140pub struct Route {
141    pub coordinates: AnnotationCoordinates,
142    /// The location where the component was registered against the `Blueprint`.
143    pub registered_at: Location,
144    /// The callable in charge of processing errors returned by this route, if any.
145    pub error_handler: Option<ErrorHandler>,
146}
147
148#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
149/// A request handler registered against a `Blueprint` via `Blueprint::fallback` to
150/// process requests that don't match any of the registered routes.
151pub struct Fallback {
152    pub coordinates: AnnotationCoordinates,
153    /// The location where the component was registered against the `Blueprint`.
154    pub registered_at: Location,
155    /// The callable in charge of processing errors returned by this fallback, if any.
156    pub error_handler: Option<ErrorHandler>,
157}
158
159#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
160/// An error observer registered against a `Blueprint` via `Blueprint::error_observer` to
161/// intercept unhandled errors.
162pub struct ErrorObserver {
163    pub coordinates: AnnotationCoordinates,
164    /// The location where the component was registered against the `Blueprint`.
165    pub registered_at: Location,
166}
167
168#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
169/// An error handler registered against a `Blueprint` via `Blueprint::error_handler`.
170pub struct ErrorHandler {
171    pub coordinates: AnnotationCoordinates,
172    /// The location where the component was registered against the `Blueprint`.
173    pub registered_at: Location,
174}
175
176#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
177/// A type registered against a `Blueprint` via `Blueprint::prebuilt` to
178/// be added as an input parameter to `ApplicationState::new`.
179pub struct PrebuiltType {
180    /// The coordinates of the annotated type.
181    pub coordinates: AnnotationCoordinates,
182    /// The strategy dictating when the prebuilt type can be cloned.
183    pub cloning_policy: Option<CloningPolicy>,
184    /// The location where this prebuilt type was registered in the application code.
185    pub registered_at: Location,
186}
187
188#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
189/// A type registered against a `Blueprint` via `Blueprint::config` to
190/// become part of the overall configuration for the application.
191pub struct ConfigType {
192    /// The coordinates of the annotated type.
193    pub coordinates: AnnotationCoordinates,
194    /// The strategy dictating when the config type can be cloned.
195    pub cloning_policy: Option<CloningPolicy>,
196    /// Whether to use `Default::default` to generate default configuration
197    /// values if the user hasn't specified any.
198    pub default_if_missing: Option<bool>,
199    /// Whether to include the config type as a field in the generated
200    /// configuration struct even if it was never injected.
201    pub include_if_unused: Option<bool>,
202    /// The location where this configuration type was registered in the application code.
203    pub registered_at: Location,
204}
205
206#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
207/// A constructor registered against a `Blueprint` via `Blueprint::constructor`.
208pub struct Constructor {
209    /// The coordinates of the annotated constructor.
210    pub coordinates: AnnotationCoordinates,
211    /// The lifecycle of the constructed type.
212    pub lifecycle: Option<Lifecycle>,
213    /// The strategy dictating when the constructed type can be cloned.
214    pub cloning_policy: Option<CloningPolicy>,
215    /// The callable in charge of processing errors returned by this constructor, if any.
216    pub error_handler: Option<ErrorHandler>,
217    /// Lint settings for this constructor.
218    pub lints: BTreeMap<Lint, LintSetting>,
219    /// The location where this constructor was registered in the application code.
220    pub registered_at: Location,
221}
222
223#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
224/// A middleware registered against a `Blueprint` via `Blueprint::wrap`.
225pub struct WrappingMiddleware {
226    pub coordinates: AnnotationCoordinates,
227    /// The location where the component was registered against the `Blueprint`.
228    pub registered_at: Location,
229    /// The callable in charge of processing errors returned by this middleware, if any.
230    pub error_handler: Option<ErrorHandler>,
231}
232
233#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
234/// A middleware registered against a `Blueprint` via `Blueprint::post_process`.
235pub struct PostProcessingMiddleware {
236    pub coordinates: AnnotationCoordinates,
237    /// The location where the component was registered against the `Blueprint`.
238    pub registered_at: Location,
239    /// The callable in charge of processing errors returned by this middleware, if any.
240    pub error_handler: Option<ErrorHandler>,
241}
242
243#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
244/// A middleware registered against a `Blueprint` via `Blueprint::pre_process`.
245pub struct PreProcessingMiddleware {
246    pub coordinates: AnnotationCoordinates,
247    /// The location where the component was registered against the `Blueprint`.
248    pub registered_at: Location,
249    /// The callable in charge of processing errors returned by this middleware, if any.
250    pub error_handler: Option<ErrorHandler>,
251}
252
253#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
254/// A `Blueprint` that has been nested inside another `Blueprint` via `Blueprint::nest` or
255/// `Blueprint::nest_at`.
256pub struct NestedBlueprint {
257    /// The nested `Blueprint`.
258    pub blueprint: Blueprint,
259    /// The path prefix that will prepended to all routes registered against the nested
260    /// `Blueprint`.
261    /// If `None`, the routes coming from the nested `Blueprint` will be registered as-they-are.
262    pub path_prefix: Option<PathPrefix>,
263    /// If `Some`, only requests whose `Host` header matches this value will be forwarded to the
264    /// routes registered against this nested `Blueprint`.
265    pub domain: Option<Domain>,
266    /// The location where the `Blueprint` was nested under its parent `Blueprint`.
267    pub nested_at: Location,
268}
269
270#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
271/// A path modifier for a nested [`Blueprint`].
272pub struct PathPrefix {
273    /// The path prefix to prepend to all routes registered against the nested [`Blueprint`].
274    pub path_prefix: String,
275    /// The location where the path prefix was registered.
276    pub registered_at: Location,
277}
278
279/// A domain routing constraint.
280#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
281pub struct Domain {
282    /// The domain to match.
283    pub domain: String,
284    /// The location where the domain constraint was registered.
285    pub registered_at: Location,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum Lifecycle {
291    Singleton,
292    RequestScoped,
293    Transient,
294}
295
296impl fmt::Display for Lifecycle {
297    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
298        match self {
299            Lifecycle::Singleton => write!(f, "singleton"),
300            Lifecycle::RequestScoped => write!(f, "request-scoped"),
301            Lifecycle::Transient => write!(f, "transient"),
302        }
303    }
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
307#[serde(rename_all = "snake_case")]
308#[non_exhaustive]
309pub enum CloningPolicy {
310    /// Pavex will **never** try clone the output type returned by the constructor.
311    NeverClone,
312    /// Pavex will only clone the output type returned by this constructor if it's
313    /// necessary to generate code that satisfies Rust's borrow checker.
314    CloneIfNecessary,
315}
316
317#[derive(
318    Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
319)]
320#[serde(rename_all = "snake_case")]
321pub enum MethodGuard {
322    Any,
323    Some(BTreeSet<String>),
324}
325
326#[derive(
327    Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Serialize, serde::Deserialize,
328)]
329#[serde(rename_all = "snake_case")]
330#[non_exhaustive]
331/// Common mistakes and antipatterns that Pavex
332/// tries to catch when analysing your [`Blueprint`].
333pub enum Lint {
334    /// You registered a component that's never used in the generated
335    /// server SDK code.
336    Unused,
337    /// Allow Pavex to [invoke a fallback error handler if no specific error handler is provided][1].
338    ///
339    /// [1]: https://pavex.dev/docs/guide/errors/error_handlers/#fallback-error-handler
340    ErrorFallback,
341}
342
343#[derive(
344    Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize,
345)]
346#[serde(rename_all = "snake_case")]
347pub enum LintSetting {
348    Allow,
349    Warn,
350    Deny,
351}
352
353#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
354#[serde(rename_all = "snake_case")]
355/// A collection of modules to be scanned for components.
356pub enum Sources {
357    /// Use all valid sources: modules from the current crate and all its direct dependencies.
358    All,
359    /// Use only the specified modules as sources.
360    ///
361    /// Each module can be either from the current crate or from one of its direct dependencies.
362    Some(Vec<String>),
363}
364
365#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
366/// A set of coordinates to identify a precise spot in a source file.
367///
368/// # Implementation Notes
369///
370/// `Location` is an owned version of [`std::panic::Location`].
371/// You can build a `Location` instance starting from a [`std::panic::Location`]:
372///
373/// ```rust
374/// use pavex_bp_schema::Location;
375///
376/// let location: Location = std::panic::Location::caller().into();
377/// ```
378pub struct Location {
379    /// The line number.
380    ///
381    /// Lines are 1-indexed (i.e. the first line is numbered as 1, not 0).
382    pub line: u32,
383    /// The column number.
384    ///
385    /// Columns are 1-indexed (i.e. the first column is numbered as 1, not 0).
386    pub column: u32,
387    /// The name of the source file.
388    ///
389    /// Check out [`std::panic::Location::file`] for more details.
390    pub file: String,
391}
392
393impl<'a> From<&'a std::panic::Location<'a>> for Location {
394    fn from(l: &'a std::panic::Location<'a>) -> Self {
395        Self {
396            line: l.line(),
397            column: l.column(),
398            file: l.file().into(),
399        }
400    }
401}
402
403impl Location {
404    #[track_caller]
405    pub fn caller() -> Self {
406        std::panic::Location::caller().into()
407    }
408}
409
410#[derive(Debug, Hash, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
411/// The method used to create (and set the properties) for this component.
412pub enum CreatedBy {
413    /// The component was created via a macro annotation (e.g. `#[pavex::wrap]`)
414    /// on top of the target item (e.g. a function or a method).
415    Attribute { name: String },
416    /// The component was provided by the framework.
417    ///
418    /// For example, the default fallback handler if the user didn't specify one.
419    Framework,
420}
421
422impl CreatedBy {
423    /// Convert the name of the macro used to perform the registration into an instance of [`CreatedBy`].
424    pub fn macro_name(value: &str) -> Self {
425        match value {
426            "pre_process" | "post_process" | "wrap" | "request_scoped" | "transient"
427            | "singleton" | "config" | "error_handler" | "error_observer" | "fallback"
428            | "route" => CreatedBy::Attribute { name: value.into() },
429            _ => panic!(
430                "Pavex doesn't recognize `{value}` as one of its macros to register components"
431            ),
432        }
433    }
434}
435
436#[derive(Debug, Hash, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
437pub struct AnnotationCoordinates {
438    /// An opaque string that uniquely identifies this component within the package
439    /// where it was defined.
440    pub id: String,
441    /// Metadata required to pinpoint where the annotated component lives.
442    pub created_at: CreatedAt,
443    /// The name of the macro used to annotate the component.
444    pub macro_name: String,
445}
446
447#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
448/// Information on the crate/module where the component was created.
449///
450/// This location matches, for example, where the `from!` or the `f!` macro were invoked.
451/// For annotated items (e.g. via `#[pavex::config]`), this refers to the location of the annotation.
452///
453/// It may be different from the location where the component was registered
454/// with the blueprint—i.e. where a `Blueprint` method was invoked.
455pub struct CreatedAt {
456    /// The name of the crate that created the component, as it appears in the `package.name` field
457    /// of its `Cargo.toml`.
458    /// Obtained via [`CARGO_PKG_NAME`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates).
459    ///
460    /// In particular, the name has *not* been normalised—e.g. hyphens are not replaced with underscores.
461    ///
462    /// This information is needed to resolve the import path unambiguously.
463    ///
464    /// E.g. `my_crate::module_1::type_2`—which crate is `my_crate`?
465    /// This is not obvious due to the possibility of [renaming dependencies in `Cargo.toml`](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html?highlight=rename,depende#renaming-dependencies-in-cargotoml):
466    ///
467    /// ```toml
468    /// [package]
469    /// name = "mypackage"
470    /// version = "0.0.1"
471    ///
472    /// [dependencies]
473    /// my_crate = { version = "0.1", registry = "custom", package = "their_crate" }
474    /// ```
475    pub package_name: String,
476    /// The version of the crate that created the component, as it appears in the `package.version` field
477    /// of its `Cargo.toml`.
478    ///
479    /// Obtained via [`CARGO_PKG_VERSION`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates).
480    pub package_version: String,
481}