Skip to main content

dynamic_config_axum/
lib.rs

1//! A request-scoped configuration snapshot for axum.
2//!
3//! ```no_run
4//! use axum::{routing::get, Router};
5//! use dynamic_config_axum::{Config, SnapshotLayer};
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//! async fn index(
13//!     Config(server): Config<Server>,
14//!     Config(features): Config<Features>,
15//! ) -> String {
16//!     // Both came out of one snapshot, taken when the request began.
17//!     // `Sections::take` retries if a reload lands mid-read, so these
18//!     // two cannot be different generations.
19//!     format!("{} {}", server.port, features.cache)
20//! }
21//!
22//! let app: Router = Router::new()
23//!     .route("/", get(index))
24//!     .layer(SnapshotLayer::new(sections![Server, Features]));
25//! ```
26//!
27//! # What this is for
28//!
29//! `Server::current()` is an atomic load, and calling it in a handler is
30//! correct. Calling it *twice*, or calling it for two sections, is where a
31//! reload landing mid-request lets one response mix generations.
32//!
33//! [`SnapshotLayer`] reads every listed section once, before the handler
34//! runs, and stores the result in the request's extensions. [`Config<T>`]
35//! reads it back out. Request extensions are axum's request scope, so
36//! nothing here is thread-local and nothing has to be undone afterwards.
37//!
38//! # What this is not
39//!
40//! It does not load configuration, watch files, or own a [`WatchHandle`].
41//! That stays where it already is — in the startup code that calls
42//! `init()` and holds the handles for the life of the process.
43//!
44//! [`WatchHandle`]: https://docs.rs/dynamic-config/latest/dynamic_config/watch/struct.WatchHandle.html
45
46#![forbid(unsafe_code)]
47#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
48#![cfg_attr(docsrs, feature(doc_cfg))]
49
50use std::any::Any;
51use std::sync::Arc;
52use std::task::{Context, Poll};
53
54use axum::extract::FromRequestParts;
55use axum::http::request::Parts;
56use axum::http::{Request, StatusCode};
57use axum::response::{IntoResponse, Response};
58use dynamic_config_web_core::{NotInScope, Sections, Snapshot};
59use tower::{Layer, Service};
60
61pub use dynamic_config_web_core::{sections, NotInScope as OutOfScope, Sections as ConfigSections};
62
63/// One section of this request's configuration.
64///
65/// ```no_run
66/// # use dynamic_config_axum::Config;
67/// # struct Database { host: String }
68/// async fn handler(Config(db): Config<Database>) -> String {
69///     db.host.clone()
70/// }
71/// ```
72///
73/// Extracting the same type twice in one handler answers the same `Arc`.
74/// Extracting one the layer was not given is a wiring mistake and answers
75/// `500` — see [`SnapshotMissing`].
76pub struct Config<T>(pub Arc<T>);
77
78impl<T> Clone for Config<T> {
79    /// Hand-written: cloning an `Arc` never needs `T: Clone`, and a
80    /// derive would demand it of every section.
81    fn clone(&self) -> Self {
82        Self(Arc::clone(&self.0))
83    }
84}
85
86impl<T> std::fmt::Debug for Config<T> {
87    /// The type's name, never the section's contents.
88    ///
89    /// A configuration section holds credentials, and `?config` in a
90    /// `tracing` call is exactly how one reaches a log line. `Snapshot`
91    /// holds the same line; this is the extractor keeping it.
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<S, T> FromRequestParts<S> for Config<T>
109where
110    S: Send + Sync,
111    T: Any + Send + Sync,
112{
113    type Rejection = SnapshotMissing;
114
115    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
116        let snapshot = parts
117            .extensions
118            .get::<Snapshot>()
119            .ok_or(SnapshotMissing::NoLayer)?;
120
121        snapshot
122            .require::<T>()
123            .map(Config)
124            .map_err(SnapshotMissing::Section)
125    }
126}
127
128/// Why a [`Config`] extractor could not answer.
129///
130/// Every variant is a wiring mistake rather than anything a client did,
131/// which is why they are all `500`: a request that asks for a section the
132/// application never registered would be wrong however it was sent.
133#[derive(Debug, Clone, Copy)]
134pub enum SnapshotMissing {
135    /// No [`SnapshotLayer`] ran for this request.
136    NoLayer,
137    /// The layer ran, and this section was not in what it took.
138    Section(NotInScope),
139}
140
141impl std::fmt::Display for SnapshotMissing {
142    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::NoLayer => formatter.write_str(
145                "no configuration snapshot on this request: add \
146                 `.layer(SnapshotLayer::new(sections![..]))` to the router",
147            ),
148            Self::Section(why) => write!(formatter, "{why}"),
149        }
150    }
151}
152
153impl std::error::Error for SnapshotMissing {}
154
155impl IntoResponse for SnapshotMissing {
156    fn into_response(self) -> Response {
157        // The detail names an internal type path, which is for whoever
158        // reads the logs rather than for whoever sent the request. The
159        // body says only that the server is misconfigured; `Display`
160        // carries the rest.
161        (
162            StatusCode::INTERNAL_SERVER_ERROR,
163            "configuration is not wired for this handler",
164        )
165            .into_response()
166    }
167}
168
169/// Takes one snapshot per request and puts it in the request's extensions.
170///
171/// ```no_run
172/// # use axum::Router;
173/// # use dynamic_config_axum::SnapshotLayer;
174/// # use dynamic_config_web_core::Sections;
175/// # let sections = Sections::new();
176/// let app: Router = Router::new().layer(SnapshotLayer::new(sections));
177/// ```
178///
179/// `.layer()` wraps the routes a `Router` holds **when it is called**, so
180/// it goes last:
181///
182/// ```ignore
183/// Router::new().route("/", get(handler)).layer(layer)   // handler is wrapped
184/// Router::new().layer(layer).route("/", get(handler))   // it is NOT
185/// ```
186///
187/// The second form compiles and answers `500` on every request.
188#[derive(Clone)]
189pub struct SnapshotLayer {
190    sections: Arc<Sections>,
191}
192
193impl SnapshotLayer {
194    /// Builds the layer over the sections a request should read.
195    #[must_use]
196    pub fn new(sections: Sections) -> Self {
197        Self {
198            sections: Arc::new(sections),
199        }
200    }
201
202    /// The type names it will take, in order.
203    #[must_use]
204    pub fn names(&self) -> Vec<&'static str> {
205        self.sections.names()
206    }
207}
208
209impl std::fmt::Debug for SnapshotLayer {
210    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        formatter
212            .debug_struct("SnapshotLayer")
213            .field("sections", &self.sections.names())
214            .finish()
215    }
216}
217
218impl<S> Layer<S> for SnapshotLayer {
219    type Service = SnapshotService<S>;
220
221    fn layer(&self, inner: S) -> Self::Service {
222        SnapshotService {
223            inner,
224            sections: Arc::clone(&self.sections),
225        }
226    }
227}
228
229/// The service [`SnapshotLayer`] wraps a router in.
230#[derive(Clone)]
231pub struct SnapshotService<S> {
232    inner: S,
233    sections: Arc<Sections>,
234}
235
236impl<S> std::fmt::Debug for SnapshotService<S> {
237    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        formatter
239            .debug_struct("SnapshotService")
240            .field("sections", &self.sections.names())
241            .finish_non_exhaustive()
242    }
243}
244
245impl<S, B> Service<Request<B>> for SnapshotService<S>
246where
247    S: Service<Request<B>>,
248{
249    type Response = S::Response;
250    type Error = S::Error;
251    type Future = S::Future;
252
253    fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
254        self.inner.poll_ready(context)
255    }
256
257    fn call(&mut self, mut request: Request<B>) -> Self::Future {
258        // Once, here, before anything downstream runs. Every read in the
259        // handler comes out of this one value.
260        let taken = self.sections.take();
261
262        // Merged rather than inserted, because layers nest: an outer
263        // `Router` and a `nest`ed one may each carry a layer, the outer
264        // runs first, and a bare `insert` here would erase what it took.
265        // A handler under both then sees only the inner list, and asks for
266        // a section whose 500 says to add what is already there.
267        let merged = match request.extensions_mut().remove::<Snapshot>() {
268            Some(outer) => outer.merged_with(taken),
269            None => taken,
270        };
271
272        request.extensions_mut().insert(merged);
273
274        self.inner.call(request)
275    }
276}
277
278/// The snapshot this request began with, for code that has the parts in
279/// hand rather than an extractor — another middleware, or a handler that
280/// takes `Request` whole.
281///
282/// # Errors
283///
284/// [`SnapshotMissing::NoLayer`] when no [`SnapshotLayer`] ran.
285pub fn snapshot(parts: &Parts) -> Result<&Snapshot, SnapshotMissing> {
286    parts
287        .extensions
288        .get::<Snapshot>()
289        .ok_or(SnapshotMissing::NoLayer)
290}