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//!
47//! # Long-lived connections
48//!
49//! A WebSocket upgrade begins as an HTTP request, so `Config<T>`
50//! extracted at upgrade time is correct *for the handshake* — and wrong
51//! as the connection's configuration for life. Do not move the `Arc`
52//! into the `on_upgrade` future as "the config"; inside the socket loop
53//! read `T::current()` per iteration or per message batch, and treat a
54//! change as an event if the protocol wants one. The same applies to SSE
55//! and long streaming bodies.
56
57#![forbid(unsafe_code)]
58#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
59#![cfg_attr(docsrs, feature(doc_cfg))]
60
61use std::any::Any;
62
63use axum::extract::FromRequestParts;
64use axum::http::request::Parts;
65use std::sync::Arc;
66
67use axum::http::StatusCode;
68use axum::response::{IntoResponse, Response};
69use dynamic_config_web_core::{NotInScope, Snapshot};
70
71pub use dynamic_config_web_core::{sections, NotInScope as OutOfScope, Sections as ConfigSections};
72
73/// One section of this request's configuration.
74///
75/// ```no_run
76/// # use dynamic_config_axum::Config;
77/// # struct Database { host: String }
78/// async fn handler(Config(db): Config<Database>) -> String {
79///     db.host.clone()
80/// }
81/// ```
82///
83/// Extracting the same type twice in one handler answers the same `Arc`.
84/// Extracting one the layer was not given is a wiring mistake and answers
85/// `500` — see [`SnapshotMissing`].
86pub struct Config<T>(pub Arc<T>);
87
88impl<T> Clone for Config<T> {
89    /// Hand-written: cloning an `Arc` never needs `T: Clone`, and a
90    /// derive would demand it of every section.
91    fn clone(&self) -> Self {
92        Self(Arc::clone(&self.0))
93    }
94}
95
96impl<T> std::fmt::Debug for Config<T> {
97    /// The type's name, never the section's contents.
98    ///
99    /// A configuration section holds credentials, and `?config` in a
100    /// `tracing` call is exactly how one reaches a log line. `Snapshot`
101    /// holds the same line; this is the extractor keeping it.
102    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        formatter
104            .debug_tuple("Config")
105            .field(&std::any::type_name::<T>())
106            .finish()
107    }
108}
109
110impl<T> std::ops::Deref for Config<T> {
111    type Target = T;
112
113    fn deref(&self) -> &Self::Target {
114        &self.0
115    }
116}
117
118impl<S, T> FromRequestParts<S> for Config<T>
119where
120    S: Send + Sync,
121    T: Any + Send + Sync,
122{
123    type Rejection = SnapshotMissing;
124
125    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
126        let snapshot = parts
127            .extensions
128            .get::<Snapshot>()
129            .ok_or(SnapshotMissing::NoLayer)?;
130
131        snapshot
132            .require::<T>()
133            .map(Config)
134            .map_err(SnapshotMissing::Section)
135    }
136}
137
138/// Why a [`Config`] extractor could not answer.
139///
140/// Every variant is a wiring mistake rather than anything a client did,
141/// which is why they are all `500`: a request that asks for a section the
142/// application never registered would be wrong however it was sent.
143#[derive(Debug, Clone, Copy)]
144pub enum SnapshotMissing {
145    /// No [`SnapshotLayer`] ran for this request.
146    NoLayer,
147    /// The layer ran, and this section was not in what it took.
148    Section(NotInScope),
149}
150
151impl std::fmt::Display for SnapshotMissing {
152    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            Self::NoLayer => formatter.write_str(
155                "no configuration snapshot on this request: add \
156                 `.layer(SnapshotLayer::new(sections![..]))` to the router",
157            ),
158            Self::Section(why) => write!(formatter, "{why}"),
159        }
160    }
161}
162
163impl std::error::Error for SnapshotMissing {}
164
165impl IntoResponse for SnapshotMissing {
166    fn into_response(self) -> Response {
167        // The detail names an internal type path, which is for whoever
168        // reads the logs rather than for whoever sent the request. The
169        // body says only that the server is misconfigured; `Display`
170        // carries the rest.
171        (
172            StatusCode::INTERNAL_SERVER_ERROR,
173            "configuration is not wired for this handler",
174        )
175            .into_response()
176    }
177}
178
179pub use dynamic_config_tower::{SnapshotLayer, SnapshotService};
180
181/// The snapshot this request began with, for code that has the parts in
182/// hand rather than an extractor — another middleware, or a handler that
183/// takes `Request` whole.
184///
185/// # Errors
186///
187/// [`SnapshotMissing::NoLayer`] when no [`SnapshotLayer`] ran.
188pub fn snapshot(parts: &Parts) -> Result<&Snapshot, SnapshotMissing> {
189    parts
190        .extensions
191        .get::<Snapshot>()
192        .ok_or(SnapshotMissing::NoLayer)
193}