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