Skip to main content

dynamic_config_loco/
lib.rs

1//! A request-scoped configuration snapshot for Loco.
2//!
3//! ```no_run
4//! use dynamic_config_loco::{sections, Config, DynamicConfig};
5//! use loco_rs::prelude::*;
6//! # use dynamic_config::dynamic_config;
7//! # use serde::Deserialize;
8//! # #[dynamic_config] #[derive(Deserialize)] struct Server { port: u16 }
9//! # #[dynamic_config] #[derive(Deserialize)] struct Features { cache: bool }
10//! # struct App;
11//!
12//! # impl App {
13//! async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
14//!     Ok(vec![Box::new(DynamicConfig::new(sections![Server, Features]))])
15//! }
16//! # }
17//! ```
18//!
19//! ```no_run
20//! # use dynamic_config_loco::Config;
21//! # use loco_rs::prelude::*;
22//! # struct Server { port: u16 }
23//! # struct Features { cache: bool }
24//! async fn index(
25//!     Config(server): Config<Server>,
26//!     Config(features): Config<Features>,
27//! ) -> Result<Response> {
28//!     // One reading, taken when the request began. `Sections::take`
29//!     // retries if a reload lands mid-read, so these two cannot be
30//!     // different generations.
31//!     format::text(&format!("{} {}", server.port, features.cache))
32//! }
33//! ```
34//!
35//! # What this crate is
36//!
37//! Loco is axum underneath, so the layer and the extractor are
38//! [`dynamic-config-axum`](https://docs.rs/dynamic-config-axum)'s, re-exported
39//! here unchanged. What Loco adds is a place to register one — the
40//! [`Initializer`] trait — and that registration is all this crate is.
41//!
42//! Writing it by hand is three lines in your own initializer, and doing so
43//! is fine. This crate exists so that it is one line in `initializers`, and
44//! so that there is somewhere to write down the two things below.
45//!
46//! # Loco's own configuration is a different thing
47//!
48//! Loco reads `config/development.yaml` into `ctx.config` at boot, and that
49//! is where the database URL, the worker mode and the server port live.
50//! None of it reloads, and none of it should: Loco binds its listener and
51//! builds its connection pool from those values once.
52//!
53//! This crate is for the *other* half — the settings an operator changes
54//! while the service runs. Keep them in their own file with their own
55//! `#[dynamic_config]` sections, and leave `ctx.config` to Loco.
56//!
57//! # What this crate does not do
58//!
59//! It does not load configuration, watch files, or own a [`WatchHandle`].
60//! Loco's `Hooks::boot` is where a service does that, before the router
61//! exists.
62//!
63//! [`WatchHandle`]: https://docs.rs/dynamic-config/latest/dynamic_config/watch/struct.WatchHandle.html
64
65#![forbid(unsafe_code)]
66#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
67#![cfg_attr(docsrs, feature(doc_cfg))]
68
69use async_trait::async_trait;
70
71use dynamic_config_web_core::Sections;
72use loco_rs::app::{AppContext, Initializer};
73use loco_rs::Result;
74
75// `axum::Router` itself: Loco's `AxumRouter` is a private alias for it
76// inside `loco_rs::app`, so the trait's signature is spelled here the way
77// the compiler sees it.
78use axum::Router as AxumRouter;
79
80pub use dynamic_config_axum::{snapshot, Config, SnapshotLayer, SnapshotMissing};
81pub use dynamic_config_web_core::{sections, NotInScope, Sections as ConfigSections, Snapshot};
82
83/// The initializer that puts one reading on every request.
84///
85/// ```no_run
86/// # use dynamic_config_loco::{DynamicConfig, ConfigSections};
87/// # use loco_rs::app::Initializer;
88/// let initializers: Vec<Box<dyn Initializer>> =
89///     vec![Box::new(DynamicConfig::new(ConfigSections::new()))];
90/// ```
91///
92/// Loco calls [`after_routes`](Initializer::after_routes) once, with the
93/// router it has built, and this adds the layer to it — so every route the
94/// application declares is covered, including the ones Loco adds itself.
95pub struct DynamicConfig {
96    layer: SnapshotLayer,
97}
98
99impl DynamicConfig {
100    /// Builds the initializer over the sections a request should read.
101    #[must_use]
102    pub fn new(sections: Sections) -> Self {
103        Self {
104            layer: SnapshotLayer::new(sections),
105        }
106    }
107
108    /// The same, boxed, for the `Vec<Box<dyn Initializer>>` Loco wants.
109    ///
110    /// ```no_run
111    /// # use dynamic_config_loco::{DynamicConfig, ConfigSections};
112    /// # use loco_rs::app::Initializer;
113    /// let initializers: Vec<Box<dyn Initializer>> =
114    ///     vec![DynamicConfig::boxed(ConfigSections::new())];
115    /// ```
116    #[must_use]
117    pub fn boxed(sections: Sections) -> Box<dyn Initializer> {
118        Box::new(Self::new(sections))
119    }
120
121    /// The layer this initializer installs.
122    ///
123    /// For an application that already has an initializer of its own and
124    /// would rather add one layer than one more `Box<dyn Initializer>`:
125    ///
126    /// ```no_run
127    /// # use dynamic_config_loco::{DynamicConfig, ConfigSections};
128    /// # use axum::Router;
129    /// # let router: Router = Router::new();
130    /// let configuration = DynamicConfig::new(ConfigSections::new());
131    ///
132    /// let router = router.layer(configuration.layer());
133    /// ```
134    #[must_use]
135    pub fn layer(&self) -> SnapshotLayer {
136        self.layer.clone()
137    }
138
139    /// The type names it will take, in order.
140    #[must_use]
141    pub fn names(&self) -> Vec<&'static str> {
142        self.layer.names()
143    }
144}
145
146impl std::fmt::Debug for DynamicConfig {
147    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        formatter
149            .debug_struct("DynamicConfig")
150            .field("sections", &self.layer.names())
151            .finish()
152    }
153}
154
155#[async_trait]
156impl Initializer for DynamicConfig {
157    /// What Loco prints when it lists what is installed.
158    fn name(&self) -> String {
159        "dynamic-config".to_string()
160    }
161
162    /// Adds the layer to the router Loco has built.
163    async fn after_routes(&self, router: AxumRouter, _context: &AppContext) -> Result<AxumRouter> {
164        // Cloned rather than moved: `after_routes` takes `&self`, and the
165        // layer is an `Arc` around the section list — so this is a refcount
166        // bump, not a second copy of anything.
167        Ok(router.layer(self.layer.clone()))
168    }
169}