dynamic_config_actix/lib.rs
1//! A request-scoped configuration snapshot for Actix Web.
2//!
3//! ```no_run
4//! use actix_web::{get, App, HttpServer};
5//! use dynamic_config_actix::{Config, DynamicConfig};
6//! use dynamic_config_web_core::sections;
7//! # use dynamic_config::dynamic_config;
8//! # use serde::Deserialize;
9//! # #[dynamic_config] #[derive(Deserialize)] struct Server { port: u16 }
10//! # #[dynamic_config] #[derive(Deserialize)] struct Features { cache: bool }
11//!
12//! #[get("/")]
13//! async fn index(server: Config<Server>, features: Config<Features>) -> String {
14//! // Both came out of one snapshot, taken when the request began.
15//! // `Sections::take` retries if a reload lands mid-read, so these
16//! // two cannot be different generations.
17//! format!("{} {}", server.port, features.cache)
18//! }
19//!
20//! # fn build() {
21//! HttpServer::new(|| {
22//! App::new()
23//! .wrap(DynamicConfig::new(sections![Server, Features]))
24//! .service(index)
25//! });
26//! # }
27//! ```
28//!
29//! # What this is for
30//!
31//! Actix runs handlers across several worker threads, and `Server::current()`
32//! is an atomic load that every worker can make without a lock. That part is
33//! already right. What it does not give you is *two* sections that agree: a
34//! reload landing between two reads lets one response mix generations.
35//!
36//! [`DynamicConfig`] reads every listed section once, before the handler
37//! runs, and puts the result in the request's extensions. [`Config<T>`]
38//! reads it back out.
39//!
40//! Note what is **not** here: no `web::Data<ServerConfig>`. Handing a
41//! snapshot to the app factory freezes it at start-up, and each worker then
42//! serves the configuration that existed when it was built.
43//!
44//! # What this is not
45//!
46//! It does not load configuration, watch files, or own a [`WatchHandle`].
47//! That stays in the startup code that calls `init()` and holds the handles
48//! for the life of the process.
49//!
50//! [`WatchHandle`]: https://docs.rs/dynamic-config/latest/dynamic_config/watch/struct.WatchHandle.html
51
52#![forbid(unsafe_code)]
53#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
54#![cfg_attr(docsrs, feature(doc_cfg))]
55
56use std::any::Any;
57use std::future::{ready, Ready};
58use std::sync::Arc;
59
60use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
61use actix_web::http::StatusCode;
62use actix_web::{Error, FromRequest, HttpMessage, HttpRequest, ResponseError};
63use dynamic_config_web_core::{NotInScope, Sections, Snapshot};
64
65pub use dynamic_config_web_core::{sections, NotInScope as OutOfScope, Sections as ConfigSections};
66
67/// One section of this request's configuration.
68///
69/// ```no_run
70/// # use dynamic_config_actix::Config;
71/// # struct Database { host: String }
72/// async fn handler(db: Config<Database>) -> String {
73/// db.host.clone()
74/// }
75/// ```
76///
77/// Extracting the same type twice in one handler answers the same `Arc`.
78/// Extracting one the middleware was not given answers `500` — see
79/// [`SnapshotMissing`].
80pub struct Config<T>(pub Arc<T>);
81
82impl<T> Clone for Config<T> {
83 /// Hand-written: cloning an `Arc` never needs `T: Clone`.
84 fn clone(&self) -> Self {
85 Self(Arc::clone(&self.0))
86 }
87}
88
89impl<T> std::fmt::Debug for Config<T> {
90 /// The type's name, never the section's contents — a section holds
91 /// credentials, and `?config` is how one reaches a log line.
92 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 formatter
94 .debug_tuple("Config")
95 .field(&std::any::type_name::<T>())
96 .finish()
97 }
98}
99
100impl<T> std::ops::Deref for Config<T> {
101 type Target = T;
102
103 fn deref(&self) -> &Self::Target {
104 &self.0
105 }
106}
107
108impl<T> FromRequest for Config<T>
109where
110 T: Any + Send + Sync,
111{
112 type Error = Error;
113 type Future = Ready<Result<Self, Error>>;
114
115 fn from_request(request: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
116 ready(from_parts(request).map_err(Into::into))
117 }
118}
119
120fn from_parts<T: Any + Send + Sync>(request: &HttpRequest) -> Result<Config<T>, SnapshotMissing> {
121 let extensions = request.extensions();
122 let snapshot = extensions
123 .get::<Snapshot>()
124 .ok_or(SnapshotMissing::NoMiddleware)?;
125
126 snapshot
127 .require::<T>()
128 .map(Config)
129 .map_err(SnapshotMissing::Section)
130}
131
132/// Why a [`Config`] extractor could not answer.
133///
134/// Every variant is a wiring mistake rather than anything a client did,
135/// which is why they are all `500`.
136#[derive(Debug, Clone, Copy)]
137pub enum SnapshotMissing {
138 /// No [`DynamicConfig`] middleware ran for this request.
139 NoMiddleware,
140 /// It ran, and this section was not in what it took.
141 Section(NotInScope),
142}
143
144impl std::fmt::Display for SnapshotMissing {
145 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Self::NoMiddleware => formatter.write_str(
148 "no configuration snapshot on this request: add \
149 `.wrap(DynamicConfig::new(sections![..]))` to the app",
150 ),
151 Self::Section(why) => write!(formatter, "{why}"),
152 }
153 }
154}
155
156impl std::error::Error for SnapshotMissing {}
157
158impl ResponseError for SnapshotMissing {
159 fn status_code(&self) -> StatusCode {
160 StatusCode::INTERNAL_SERVER_ERROR
161 }
162
163 fn error_response(&self) -> actix_web::HttpResponse {
164 // Overridden, because the default renders `Display` into the body
165 // and `Display` names an internal type path. That detail is for
166 // whoever reads the logs; the client is told only that the server
167 // is misconfigured. The axum adapter answers the same way.
168 actix_web::HttpResponse::build(self.status_code())
169 .content_type("text/plain; charset=utf-8")
170 .body("configuration is not wired for this handler")
171 }
172}
173
174/// The snapshot this request began with, for code holding an
175/// [`HttpRequest`] rather than using the extractor.
176///
177/// Answers a clone, because actix hands out extensions behind a `RefCell`
178/// and a borrow could not outlive the call.
179///
180/// # Errors
181///
182/// [`SnapshotMissing::NoMiddleware`] when the middleware did not run.
183pub fn snapshot(request: &HttpRequest) -> Result<Snapshot, SnapshotMissing> {
184 request
185 .extensions()
186 .get::<Snapshot>()
187 .cloned()
188 .ok_or(SnapshotMissing::NoMiddleware)
189}
190
191/// Takes one snapshot per request and puts it in the request's extensions.
192///
193/// ```no_run
194/// # use actix_web::App;
195/// # use dynamic_config_actix::DynamicConfig;
196/// # use dynamic_config_web_core::Sections;
197/// # fn build() {
198/// # let sections = || Sections::new();
199/// App::new().wrap(DynamicConfig::new(sections()));
200/// # }
201/// ```
202///
203/// `HttpServer::new` takes a factory and calls it once per worker thread,
204/// so either build the [`Sections`] inside the closure — free, since it
205/// holds closures over process-wide configuration — or build one list and
206/// clone it in:
207///
208/// ```no_run
209/// # use actix_web::{App, HttpServer};
210/// # use dynamic_config_actix::DynamicConfig;
211/// # use dynamic_config_web_core::Sections;
212/// # fn build() {
213/// let configuration = DynamicConfig::new(Sections::new());
214///
215/// HttpServer::new(move || App::new().wrap(configuration.clone()));
216/// # }
217/// ```
218pub struct DynamicConfig {
219 sections: Arc<Sections>,
220}
221
222impl DynamicConfig {
223 /// Builds the middleware over the sections a request should read.
224 #[must_use]
225 pub fn new(sections: Sections) -> Self {
226 Self {
227 sections: Arc::new(sections),
228 }
229 }
230
231 /// The type names it will take, in order.
232 #[must_use]
233 pub fn names(&self) -> Vec<&'static str> {
234 self.sections.names()
235 }
236}
237
238impl std::fmt::Debug for DynamicConfig {
239 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 formatter
241 .debug_struct("DynamicConfig")
242 .field("sections", &self.sections.names())
243 .finish()
244 }
245}
246
247impl Clone for DynamicConfig {
248 fn clone(&self) -> Self {
249 Self {
250 sections: Arc::clone(&self.sections),
251 }
252 }
253}
254
255impl<S, B> Transform<S, ServiceRequest> for DynamicConfig
256where
257 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
258 S::Future: 'static,
259 B: 'static,
260{
261 type Response = ServiceResponse<B>;
262 type Error = Error;
263 type InitError = ();
264 type Transform = SnapshotMiddleware<S>;
265 type Future = Ready<Result<Self::Transform, Self::InitError>>;
266
267 fn new_transform(&self, service: S) -> Self::Future {
268 ready(Ok(SnapshotMiddleware {
269 service,
270 sections: Arc::clone(&self.sections),
271 }))
272 }
273}
274
275/// The service [`DynamicConfig`] wraps an application in.
276pub struct SnapshotMiddleware<S> {
277 service: S,
278 sections: Arc<Sections>,
279}
280
281impl<S> std::fmt::Debug for SnapshotMiddleware<S> {
282 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 formatter
284 .debug_struct("SnapshotMiddleware")
285 .field("sections", &self.sections.names())
286 .finish_non_exhaustive()
287 }
288}
289
290impl<S, B> Service<ServiceRequest> for SnapshotMiddleware<S>
291where
292 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
293 S::Future: 'static,
294 B: 'static,
295{
296 type Response = ServiceResponse<B>;
297 type Error = Error;
298 // The inner future, unboxed: nothing here runs after `call`, so there
299 // is nothing to wrap and no allocation to make per request.
300 type Future = S::Future;
301
302 forward_ready!(service);
303
304 fn call(&self, request: ServiceRequest) -> Self::Future {
305 // Taken *before* the extensions are borrowed: `take()` calls
306 // user-supplied readers, and actix hands out extensions behind a
307 // `RefCell` that panics on an overlapping borrow.
308 let taken = self.sections.take();
309
310 // Merged rather than inserted, because middleware nests: an
311 // `App::wrap` and a `scope().wrap()` may each carry a list, and a
312 // bare insert would erase the outer one.
313 let merged = match request.extensions_mut().remove::<Snapshot>() {
314 Some(outer) => outer.merged_with(taken),
315 None => taken,
316 };
317
318 request.extensions_mut().insert(merged);
319
320 self.service.call(request)
321 }
322}