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}