dynamic_config/lib.rs
1//! Hot-reloadable, lock-free application configuration, built on
2//! [figment](https://docs.rs/figment).
3//!
4//! Annotate a struct, call `init()` once, and read it from anywhere:
5//!
6//! ```
7//! # #[cfg(feature = "json")] {
8//! use dynamic_config::dynamic_config;
9//! use serde::Deserialize;
10//!
11//! #[dynamic_config(files = ["config.json"], key = "server", env = "APP_")]
12//! #[derive(Debug, Deserialize)]
13//! struct ServerConfig {
14//! #[serde(default = "default_host")]
15//! host: String,
16//! #[serde(default = "default_port")]
17//! port: u16,
18//! }
19//!
20//! # fn default_host() -> String { "0.0.0.0".into() }
21//! # fn default_port() -> u16 { 8080 }
22//! // `config.json` does not exist here, so every field falls back to its
23//! // default — a missing file is skipped, not an error.
24//! ServerConfig::init().expect("defaults cover every field");
25//!
26//! let config = ServerConfig::current();
27//! println!("{}:{}", config.host, config.port);
28//! # }
29//! ```
30//!
31//! # What the attribute generates
32//!
33//! | Method | Description |
34//! |---|---|
35//! | `load() -> Result<Self, Error>` | Read the sources and deserialize. Does not touch the snapshot. |
36//! | `init() -> Result<(), Error>` | `load()` plus install as the initial snapshot. Call once at startup. |
37//! | `replace(Self)` | Atomically swap in a new snapshot. |
38//! | `current() -> Arc<Self>` | The current snapshot. Panics before `init()`. |
39//! | `try_current() -> Option<Arc<Self>>` | The current snapshot, or `None` before `init()`. |
40//! | `start_watch() -> io::Result<()>` | With `watch`: reload on file changes. Idempotent. |
41//! | `load_async()` / `init_async()` | With `async`: the same, off the async executor. |
42//! | `changes()` | With `async`: a handle woken by every later reload. |
43//! | `set_default(path, value)` | A fallback used only when nothing else supplies the key. |
44//! | `set_override(path, value)` | A value that wins over every file and variable. |
45//! | `clear_defaults()` / `clear_overrides()` | Drop them again. |
46//!
47//! # Precedence
48//!
49//! ```text
50//! set_default < config.toml < secrets.json < APP_DB_* < set_override
51//! (runtime) (first) (last file) (environment) (runtime)
52//! ```
53//!
54//! Files merge left to right and tables merge key by key, so a small
55//! `secrets.json` can override two fields of a large `config.toml` without
56//! restating it.
57//!
58//! The two runtime layers bracket the rest. Defaults cover a fallback the
59//! program can compute but a file need not state; overrides are what make a
60//! test or a `--set key=value` flag authoritative without touching disk. Both
61//! take effect on the next `load()`.
62//!
63//! # Reading configuration is lock-free
64//!
65//! `current()` hands out an `Arc` cloned from an `ArcSwap`, so a reload never
66//! blocks a request handler. A reader that already holds an `Arc` keeps its own
67//! generation — call `current()` once per request and reuse it, or a reload
68//! landing mid-request will show you two different configurations.
69//!
70//! # Reloading cannot take the process down
71//!
72//! A reload re-runs `load()`. If the new configuration is invalid, or a file is
73//! caught half-written, the error is reported and the previous snapshot stays
74//! in place. A bad edit degrades to "no change".
75//!
76//! # Environment variables
77//!
78//! `env = "APP_"` with `key = "db"` reads `APP_DB_*`. A single underscore is
79//! part of a field name; a doubled one introduces nesting:
80//!
81//! | Variable | Sets |
82//! |---|---|
83//! | `APP_DB_HOST` | `host` |
84//! | `APP_DB_MAX_SIZE` | `max_size` |
85//! | `APP_DB_POOL__MAX_SIZE` | `pool.max_size` |
86//!
87//! Values are interpreted by figment, which reads them loosely: `8080` reaches
88//! a `u16`, `true` reaches a `bool`, and `[a, b, c]` reaches a `Vec<String>`.
89//! A value that cannot become the field's type is an error naming the field.
90//!
91//! # Units
92//!
93//! `timeout = 30` is ambiguous and `max_body = 67108864` is unreadable, so both
94//! are usually written with a unit — which no stock `Deserialize` accepts:
95//!
96//! ```
97//! use std::time::Duration;
98//! use serde::Deserialize;
99//!
100//! #[derive(Deserialize)]
101//! struct Limits {
102//! #[serde(with = "dynamic_config::duration")]
103//! timeout: Duration, // "30s", "1h30m", "500ms", or a number of seconds
104//! #[serde(with = "dynamic_config::bytes")]
105//! max_body: u64, // "64MiB", "1GB", or a number of bytes
106//! }
107//! ```
108//!
109//! # Async
110//!
111//! With the `async` feature and the `async` argument, configuration loads
112//! without blocking the executor, and tasks can await reloads instead of
113//! polling. No runtime is named anywhere: `changes()` is a `Future`, so any
114//! executor drives it.
115//!
116//! ```ignore
117//! #[dynamic_config(files = ["config.json"], key = "db", watch, async)]
118//! #[derive(Debug, Deserialize)]
119//! struct DbConfig { pool_size: u32 }
120//!
121//! DbConfig::init_async().await?;
122//! DbConfig::start_watch()?;
123//!
124//! let mut reloads = DbConfig::changes();
125//!
126//! spawn(async move {
127//! loop {
128//! let config = reloads.changed().await;
129//! pool.resize(config.pool_size);
130//! }
131//! });
132//! ```
133//!
134//! The watcher itself stays on a plain thread. `notify`'s channel is
135//! synchronous, and keeping it off the runtime means file watching works
136//! whether or not a runtime is running.
137//!
138//! # Features
139//!
140//! | Feature | Default | Effect |
141//! |---|---|---|
142//! | `json` | yes | `.json` sources |
143//! | `toml` | no | `.toml` sources |
144//! | `yaml` | no | `.yaml` / `.yml` sources |
145//! | `watch` | no | `start_watch()` and the file watcher |
146//! | `async` | no | `load_async`, `init_async`, `changes` — no runtime dependency |
147//! | `tokio` | no | `async`, plus tokio's blocking pool instead of a thread per load |
148//! | `clap` | no | `augment_command` / `from_matches`: flags as the top layer |
149//! | `schema` | no | `schema()`: a JSON Schema for the resolved configuration |
150//! | `decrypt` | no | the [`Decryptor`]/[`Encryptor`] traits and `.age`-suffix handling |
151//! | `age` | no | `decrypt`, plus the `age` module's implementation of it |
152//! | `figment` | no | foreign figment providers as sources, via `Source::provider` |
153//! | `dotenv` | no | `env_files = [".env"]`: `.env` files as the environment layer |
154//! | `tracing` | no | Watcher diagnostics via `tracing` instead of stderr |
155//! | `full` | no | all of the above |
156//!
157//! Using a format, `watch` or `async` whose feature is disabled is a compile
158//! error naming the feature to add.
159//!
160//! # Without the macro
161//!
162//! [`load`], [`ConfigCell`] and [`LoadSpec`] are the whole engine and are
163//! usable on their own:
164//!
165//! ```
166//! # #[cfg(feature = "json")] {
167//! use dynamic_config::{load, Format, LoadSpec, Source};
168//! use serde::Deserialize;
169//!
170//! #[derive(Deserialize)]
171//! struct Db { host: String }
172//!
173//! let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
174//! let db: Db = load(&LoadSpec::new("db", &sources))
175//! .expect("the inline document is well formed");
176//!
177//! assert_eq!(db.host, "localhost");
178//! # }
179//! ```
180
181#![forbid(unsafe_code)]
182#![deny(missing_docs)]
183// A getter whose result is discarded is a mistake in a library like this one —
184// `is_set`, `contains`, `document`, `describe` all answer a question and change
185// nothing. Warned about rather than left to review, and CI denies warnings.
186#![warn(clippy::must_use_candidate)]
187#![cfg_attr(docsrs, feature(doc_cfg))]
188
189#[cfg(feature = "age")]
190#[cfg_attr(docsrs, doc(cfg(feature = "age")))]
191pub mod age;
192mod aliases;
193#[cfg(feature = "async")]
194mod asynchronous;
195mod bindings;
196mod cache;
197mod cell;
198mod check;
199#[cfg(feature = "decrypt")]
200mod decrypt;
201mod discovery;
202#[cfg(feature = "dotenv")]
203mod dotenv;
204mod error;
205mod group;
206mod layer;
207mod loader;
208mod log;
209mod registry;
210mod remote;
211#[cfg(feature = "schema")]
212#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
213pub mod schema;
214mod snapshot;
215mod source;
216mod units;
217mod write;
218
219#[cfg(feature = "watch")]
220#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
221pub mod watch;
222
223#[cfg(feature = "async")]
224pub use asynchronous::{set_blocking_executor, BlockingExecutor, Changes};
225/// figment itself, re-exported.
226///
227/// So that writing a [`Source::provider`] needs no direct dependency, and no
228/// second version of figment in the graph. This is the one place figment
229/// appears in this crate's API, which is why it is behind a feature.
230#[cfg(feature = "figment")]
231#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
232pub use figment;
233
234pub use aliases::Aliases;
235pub use bindings::EnvBindings;
236pub use cache::{CacheMode, Recovery};
237pub use cell::ConfigCell;
238pub use check::{check, Report, Resolved, UnknownKey};
239#[cfg(feature = "decrypt")]
240#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
241pub use decrypt::{has_decryptor, set_decryptor, Decryptor, Encryptor};
242pub use discovery::Search;
243pub use error::{Error, ErrorKind, Origin};
244pub use group::{Commit, ReloadGroup, Reloadable};
245pub use layer::Layer;
246pub use registry::Registry;
247#[cfg(feature = "async")]
248pub use remote::AsyncRemoteSource;
249pub use remote::{Fetched, Remote, RemoteSource, RemoteWatch, Watching};
250pub use snapshot::{Change, ChangeKind, Snapshot};
251pub use source::{Format, LoadSpec, Source, DEFAULT_NEST};
252pub use units::{bytes, duration};
253#[cfg(feature = "decrypt")]
254#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
255pub use write::save_encrypted;
256pub use write::{save, save_new};
257
258/// Turns a struct into a hot-reloadable configuration snapshot.
259///
260/// See the [crate documentation](crate) for the full guide.
261///
262/// # Arguments
263///
264/// | Argument | Form | Requires | Default |
265/// |---|---|---|---|
266/// | `files` | `files = ["a.toml"]` | one of `files` / `name`+`paths` | — |
267/// | `name` | `name = "config"` | `paths` | — |
268/// | `paths` | `paths = ["/etc/app", "."]` | `name` | — |
269/// | `key` | `key = "db"` | always | — |
270/// | `env` | `env = "APP_"` | | no environment layer |
271/// | `nest` | `nest = "__"` | `env` | `"__"` |
272/// | `allow_empty_env` | flag | `env` | off — `FOO=` is unset |
273/// | `profile_env` | `profile_env = "APP_ENV"` | | no profile overlay |
274/// | `watch` | flag | `watch` feature | off |
275/// | `debounce` | `debounce = 250` | `watch` | 250 ms |
276/// | `poll` / `poll_interval` | flag / `= 2000` | `watch` | native backend |
277/// | `diff` | flag | `watch` | off |
278/// | `validate` | flag | a `validate()` on the type | off |
279/// | `save` | flag | `Self: Serialize` | off |
280/// | `async` | flag | `async` feature | off |
281///
282/// One field attribute: `#[config(secret)]` generates a `Debug` that prints
283/// `***` for the marked fields, and forbids `#[derive(Debug)]` alongside it.
284///
285/// The README carries a section per argument, with an example and the reasoning
286/// behind each default.
287///
288/// # Requirements
289///
290/// The annotated struct must implement `serde::Deserialize` and be
291/// `Send + Sync + 'static`. Type and const parameters are supported — those go
292/// through a `TypeId` registry rather than a `static`, at a measured cost of
293/// roughly 10 ns per read. A **lifetime** parameter is rejected at compile
294/// time: the snapshot outlives every borrow that could name one.
295pub use dynamic_config_macros::dynamic_config;
296
297use serde::de::DeserializeOwned;
298
299/// Reads and deserializes a configuration section.
300///
301/// This is what the generated `load()` calls. Missing files are skipped;
302/// everything else — a parse failure, a missing required field, a value that
303/// cannot become the requested type — is an [`Error`] naming the key path and
304/// the source it came from.
305///
306/// # Errors
307///
308/// See [`ErrorKind`] for the categories.
309///
310/// # Example
311///
312/// ```
313/// # #[cfg(feature = "json")] {
314/// use dynamic_config::{load, Format, LoadSpec, Source};
315/// use serde::Deserialize;
316///
317/// #[derive(Deserialize)]
318/// struct Server { port: u16 }
319///
320/// let sources = [Source::inline(r#"{"server": {"port": 8080}}"#, Format::Json)];
321/// let server: Server = load(&LoadSpec::new("server", &sources).with_env("APP_"))
322/// .expect("the inline document is well formed");
323///
324/// assert_eq!(server.port, 8080);
325/// # }
326/// ```
327pub fn load<T: DeserializeOwned>(spec: &LoadSpec<'_>) -> Result<T, Error> {
328 loader::load(spec)
329}
330
331/// Resolves the section without deserializing it.
332///
333/// Two snapshots can be compared with [`Snapshot::diff`], which is how a reload
334/// reports *which* keys changed rather than only that something did.
335///
336/// # Errors
337///
338/// If a source cannot be read or parsed — the same failures as [`load`].
339pub fn snapshot(spec: &LoadSpec<'_>) -> Result<Snapshot, Error> {
340 loader::snapshot(spec)
341}
342
343/// Where the value at `path` would come from, if anything supplies it.
344///
345/// This is the answer to the question every configuration bug starts with:
346/// *which layer set this?* It re-reads the sources, so it reports what the
347/// **next** load would see rather than what the current snapshot holds.
348///
349/// `path` is dotted and relative to the section, as in `"pool.max_size"`.
350///
351/// # Errors
352///
353/// If a source cannot be read or parsed — the same failures as [`load`].
354///
355/// # Example
356///
357/// ```
358/// # #[cfg(feature = "json")] {
359/// use dynamic_config::{source_of, Format, LoadSpec, Origin, Source};
360///
361/// let sources = [Source::inline(r#"{"db": {"host": "localhost"}}"#, Format::Json)];
362/// let spec = LoadSpec::new("db", &sources);
363///
364/// assert_eq!(source_of(&spec, "host").unwrap(), Some(Origin::Inline));
365/// assert_eq!(source_of(&spec, "port").unwrap(), None);
366/// # }
367/// ```
368pub fn source_of(spec: &LoadSpec<'_>, path: &str) -> Result<Option<Origin>, Error> {
369 loader::source_of(spec, path)
370}
371
372/// Whether anything supplies `path`.
373///
374/// Distinguishes "absent" from "present but falsy", which
375/// `#[serde(default)]` cannot.
376///
377/// # Errors
378///
379/// If a source cannot be read or parsed — the same failures as [`load`].
380pub fn is_set(spec: &LoadSpec<'_>, path: &str) -> Result<bool, Error> {
381 loader::is_set(spec, path)
382}
383
384/// [`load`], moved off the async executor.
385///
386/// Reading configuration touches the filesystem, which would block the worker
387/// it runs on. Where the work actually goes depends on what is available:
388/// tokio's blocking pool with the `tokio` feature, an executor installed by
389/// [`set_blocking_executor`], or a freshly spawned thread. A configuration load
390/// happens at startup and on reload, so a thread per call is a real answer
391/// rather than a placeholder.
392///
393/// `LoadSpec<'static>` is taken by value because the work outlives the call;
394/// the spec the macro emits satisfies that for free.
395///
396/// # Errors
397///
398/// Same as [`load`], plus an [`ErrorKind::Backend`] error if the work never
399/// produced a result — a panic inside it, or a runtime shutting down.
400#[cfg(feature = "async")]
401#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
402pub async fn load_async<T>(spec: LoadSpec<'static>) -> Result<T, Error>
403where
404 T: DeserializeOwned + Send + 'static,
405{
406 off_thread(move || load(&spec)).await
407}
408
409/// Runs blocking configuration work without blocking the caller's executor.
410///
411/// See [`load_async`] for where the work goes.
412///
413/// # Errors
414///
415/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
416/// result.
417#[cfg(feature = "async")]
418#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
419pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
420where
421 F: FnOnce() -> Result<T, Error> + Send + 'static,
422 T: Send + 'static,
423{
424 asynchronous::off_thread(work).await
425}
426
427// ---------------------------------------------------------------------------
428// Redirects used by the generated code.
429//
430// A proc-macro cannot see which features this crate was built with, so it emits
431// a call to one of these instead of naming `Format::Toml` directly. When the
432// feature is off the redirect expands to a `compile_error!` that says exactly
433// what to add — rather than "no variant named `Toml`", or worse, a runtime
434// failure on a machine that only runs the code path in production.
435// ---------------------------------------------------------------------------
436
437/// Not public API. Lets the generated code name `serde` without the caller
438/// having to depend on it under that exact name.
439#[doc(hidden)]
440pub mod __private {
441 #[cfg(feature = "clap")]
442 pub use clap;
443 #[cfg(feature = "schema")]
444 pub use schemars;
445 pub use serde;
446 #[cfg(feature = "schema")]
447 pub use serde_json;
448}
449
450/// Not public API.
451///
452/// Expands to `save_encrypted` when the `decrypt` feature is on, and to
453/// nothing when it is not — nothing rather than a compile error, because
454/// `save` alone is fully legitimate without decryption, and the method's very
455/// signature names [`Encryptor`], which only exists with the feature.
456///
457/// It lives here rather than in the proc-macro because a `#[cfg]` emitted into
458/// generated code is evaluated against the *user's* crate features; this macro
459/// is expanded inside dynamic-config, where `decrypt` actually lives.
460#[cfg(feature = "decrypt")]
461#[macro_export]
462#[doc(hidden)]
463macro_rules! __save_encrypted_method {
464 ($key:expr) => {
465 /// Writes this configuration to `path`, encrypted.
466 ///
467 /// The counterpart to reading a `secrets.json.age`. The format comes
468 /// from the extension *under* the `.age` suffix, so
469 /// `secrets.json.age` is JSON.
470 ///
471 /// The encryptor is passed here rather than installed process-wide,
472 /// because *who may read this file* is a decision about this write.
473 ///
474 /// # Errors
475 ///
476 /// If the name resolves to no supported format, the value cannot be
477 /// serialized, encryption fails, or the file cannot be written.
478 pub fn save_encrypted(
479 &self,
480 path: impl ::core::convert::AsRef<::std::path::Path>,
481 encryptor: &dyn $crate::Encryptor,
482 ) -> ::core::result::Result<(), $crate::Error> {
483 let path = path.as_ref();
484 let format = $crate::Format::from_path(path)?;
485
486 $crate::save_encrypted(self, path, format, $key, encryptor)
487 }
488 };
489}
490
491/// Not public API.
492#[cfg(not(feature = "decrypt"))]
493#[macro_export]
494#[doc(hidden)]
495macro_rules! __save_encrypted_method {
496 ($key:expr) => {};
497}
498
499/// Not public API.
500///
501/// Nothing when this build can read `.env` files, and a compile error naming
502/// the feature when it cannot.
503#[cfg(feature = "dotenv")]
504#[macro_export]
505#[doc(hidden)]
506macro_rules! __require_dotenv {
507 () => {};
508}
509
510/// Not public API.
511#[cfg(not(feature = "dotenv"))]
512#[macro_export]
513#[doc(hidden)]
514macro_rules! __require_dotenv {
515 () => {
516 ::core::compile_error!(
517 "dynamic-config: `env_files` in #[dynamic_config(..)] requires the `dotenv` \
518 feature; add features = [\"dotenv\"] to your dynamic-config dependency"
519 );
520 };
521}
522
523/// Not public API.
524///
525/// An encrypted source when this build can decrypt, and a compile error naming
526/// the feature when it cannot — the same treatment a `.toml` file gets without
527/// the `toml` feature, for the same reason: a silent runtime failure on the one
528/// machine that has an encrypted file is worse than a build that will not start.
529#[cfg(feature = "decrypt")]
530#[macro_export]
531#[doc(hidden)]
532macro_rules! __source_encrypted {
533 ($path:expr, $format:expr) => {
534 $crate::Source::encrypted($path, $format)
535 };
536}
537
538/// Not public API.
539#[cfg(not(feature = "decrypt"))]
540#[macro_export]
541#[doc(hidden)]
542macro_rules! __source_encrypted {
543 ($path:expr, $format:expr) => {{
544 ::core::compile_error!(
545 "dynamic-config: a `.age` config file needs decryption support; \
546 add features = [\"age\"] to your dynamic-config dependency"
547 );
548
549 $crate::Source::file($path, $format)
550 }};
551}
552
553/// Not public API.
554#[cfg(feature = "json")]
555#[macro_export]
556#[doc(hidden)]
557macro_rules! __format_json {
558 () => {
559 $crate::Format::Json
560 };
561}
562
563/// Not public API.
564#[cfg(not(feature = "json"))]
565#[macro_export]
566#[doc(hidden)]
567macro_rules! __format_json {
568 () => {
569 ::core::compile_error!(
570 "dynamic-config: `.json` files require the `json` feature; \
571 add features = [\"json\"] to your dynamic-config dependency"
572 )
573 };
574}
575
576/// Not public API.
577#[cfg(feature = "toml")]
578#[macro_export]
579#[doc(hidden)]
580macro_rules! __format_toml {
581 () => {
582 $crate::Format::Toml
583 };
584}
585
586/// Not public API.
587#[cfg(not(feature = "toml"))]
588#[macro_export]
589#[doc(hidden)]
590macro_rules! __format_toml {
591 () => {
592 ::core::compile_error!(
593 "dynamic-config: `.toml` files require the `toml` feature; \
594 add features = [\"toml\"] to your dynamic-config dependency"
595 )
596 };
597}
598
599/// Not public API.
600#[cfg(feature = "yaml")]
601#[macro_export]
602#[doc(hidden)]
603macro_rules! __format_yaml {
604 () => {
605 $crate::Format::Yaml
606 };
607}
608
609/// Not public API.
610#[cfg(not(feature = "yaml"))]
611#[macro_export]
612#[doc(hidden)]
613macro_rules! __format_yaml {
614 () => {
615 ::core::compile_error!(
616 "dynamic-config: `.yaml` and `.yml` files require the `yaml` feature; \
617 add features = [\"yaml\"] to your dynamic-config dependency"
618 )
619 };
620}
621
622/// Not public API.
623///
624/// Writes the last configuration that worked, if one is configured. A failure
625/// here is reported and swallowed: a cache that cannot be written is a worse
626/// tomorrow, not a broken today.
627#[doc(hidden)]
628pub fn __write_cache(
629 snapshot: &Snapshot,
630 cache: Option<(&'static str, &'static str, &'static [&'static str])>,
631) {
632 let Some((path, mode, secrets)) = cache else {
633 return;
634 };
635
636 let mode = CacheMode::parse(mode).unwrap_or_default();
637
638 if let Err(error) = cache::write(snapshot, std::path::Path::new(path), mode, secrets) {
639 crate::log::warning!("could not write the configuration cache to {path}: {error}");
640 }
641}
642
643/// Not public API.
644///
645/// The last configuration that worked, when a cold start could not read the
646/// real one.
647///
648/// # Errors
649///
650/// If the cache exists but cannot be read. A missing cache is `Ok(None)`.
651#[doc(hidden)]
652pub fn recover<T: DeserializeOwned>(
653 name: &str,
654 spec: &LoadSpec<'_>,
655 cache: Option<(&'static str, &'static str, &'static [&'static str])>,
656 failure: &Error,
657) -> Result<Option<T>, Error> {
658 let Some((path, mode, _)) = cache else {
659 return Ok(None);
660 };
661
662 let mode = CacheMode::parse(mode).unwrap_or_default();
663 let path = std::path::Path::new(path);
664
665 // What the sources resolve to *now*, if they resolve at all — the drift
666 // report needs it, and a parse failure means there is nothing to compare.
667 let current = loader::snapshot(spec).ok();
668
669 match cache::read(path, current.as_ref())? {
670 Recovery::Absent => Ok(None),
671
672 Recovery::Drift(moved) => {
673 report(
674 name,
675 &format!(
676 "cannot start: {failure}. Since the last good configuration: {}",
677 if moved.is_empty() {
678 "the same keys, with different values".to_owned()
679 } else {
680 moved.join(", ")
681 },
682 ),
683 );
684
685 Ok(None)
686 }
687
688 Recovery::Usable(cached) if mode.recovers() => {
689 let config = loader::recover::<T>(spec, &cached).map_err(|error| {
690 Error::new(
691 ErrorKind::Backend,
692 format!("the cached configuration did not load either: {error}"),
693 )
694 })?;
695
696 report(
697 name,
698 &format!("starting from the last configuration that worked, because: {failure}"),
699 );
700
701 Ok(Some(config))
702 }
703
704 Recovery::Usable(_) => Ok(None),
705 }
706}
707
708fn report(name: &str, message: &str) {
709 crate::log::warning!("{name}: {message}");
710}
711
712/// Not public API.
713///
714/// A reload a remote watch caused. Worded to name the trigger, because a
715/// program watching both files and a store wants its log to say which one
716/// moved.
717#[doc(hidden)]
718pub fn __log_remote_reload(name: &str, summary: Option<&str>) {
719 match summary {
720 Some(summary) => crate::log::info!("{name}: reloaded from the remote store, {summary}"),
721 None => crate::log::info!("{name}: reloaded from the remote store"),
722 }
723}
724
725/// Not public API.
726///
727/// A document the store pushed that this program cannot use. Logged as well as
728/// returned: the loop that called this has nobody to hand an error to either,
729/// and a store quietly serving a configuration nothing accepts is worth a line.
730#[doc(hidden)]
731pub fn __log_remote_failure(name: &str, error: &Error) {
732 crate::log::warning!(
733 "{name}: the remote store's document did not apply, keeping the previous \
734 snapshot: {error}"
735 );
736}
737
738/// Not public API.
739///
740/// Renders the keys a reload changed, for the watcher to log. Returning a
741/// string rather than logging keeps this out of the generated code's way and
742/// leaves one log line per reload instead of two.
743///
744/// Always a string — "nothing to say" is not this function's case. The
745/// caller's `Option` means "there was no previous snapshot to compare", and
746/// that decision is made where the previous snapshot lives.
747#[doc(hidden)]
748#[must_use]
749pub fn __summarize_changes(previous: &Snapshot, current: &Snapshot) -> String {
750 let changes = previous.diff(current);
751
752 if changes.is_empty() {
753 return "no keys changed".to_owned();
754 }
755
756 changes
757 .iter()
758 .map(ToString::to_string)
759 .collect::<Vec<_>>()
760 .join(", ")
761}
762
763/// Not public API.
764#[cfg(feature = "watch")]
765#[macro_export]
766#[doc(hidden)]
767macro_rules! __spawn_watch {
768 ($($argument:tt)*) => {
769 $crate::watch::spawn_with($($argument)*)
770 };
771}
772
773/// Not public API.
774#[cfg(not(feature = "watch"))]
775#[macro_export]
776#[doc(hidden)]
777macro_rules! __spawn_watch {
778 ($($argument:tt)*) => {
779 ::core::compile_error!(
780 "dynamic-config: `watch` in #[dynamic_config(..)] requires the `watch` feature; \
781 add features = [\"watch\"] to your dynamic-config dependency"
782 )
783 };
784}
785
786/// Not public API.
787///
788/// Expands to `bind_clap` when the `clap` feature is on, and to nothing when it
789/// is not. An item-level macro rather than an expression-level redirect,
790/// because the signature names a clap type.
791#[cfg(feature = "clap")]
792#[macro_export]
793#[doc(hidden)]
794macro_rules! __clap_methods {
795 () => {
796 /// Copies clap arguments into the flags layer, by
797 /// `(argument id, key path)`.
798 ///
799 /// Only arguments that came from the command line are taken: clap's own
800 /// `default_value` is indistinguishable from a typed flag in
801 /// `ArgMatches`, and letting one outrank a configuration file would
802 /// invert the precedence order.
803 ///
804 /// # Errors
805 ///
806 /// If a key path is unusable, or an argument is not valid UTF-8.
807 pub fn bind_clap(
808 matches: &$crate::__private::clap::ArgMatches,
809 bindings: &[(&str, &str)],
810 ) -> ::core::result::Result<(), $crate::Error> {
811 Self::dynamic_config_flags().bind_clap(matches, bindings)
812 }
813 };
814}
815
816/// Not public API.
817#[cfg(not(feature = "clap"))]
818#[macro_export]
819#[doc(hidden)]
820macro_rules! __clap_methods {
821 () => {};
822}
823
824/// Not public API.
825///
826/// Expands to `schema` when the `schema` feature is on, and to nothing when it
827/// is not. An item-level macro rather than an expression-level redirect,
828/// because the `where` clause names a schemars trait.
829#[cfg(feature = "schema")]
830#[macro_export]
831#[doc(hidden)]
832macro_rules! __schema_methods {
833 ($key:expr, $secrets:expr) => {
834 /// A JSON Schema for the *file* this section lives in.
835 ///
836 /// Not the struct's schema: the struct is one section, and a config
837 /// file is a map of them, so this is the struct's schema wrapped under
838 /// its key. Fields marked `#[config(secret)]` carry `writeOnly`, which
839 /// is how JSON Schema says *not for reading back*.
840 ///
841 /// Combine several with `dynamic_config::schema::merge` when more than
842 /// one config type shares a file. See that module for what the schema
843 /// deliberately leaves out, and for how each format wires one up.
844 pub fn schema() -> ::dynamic_config::__private::serde_json::Value
845 where
846 Self: ::dynamic_config::__private::schemars::JsonSchema,
847 {
848 let generated = ::dynamic_config::__private::schemars::schema_for!(Self);
849
850 ::dynamic_config::schema::section(
851 $key,
852 ::core::convert::Into::into(generated),
853 $secrets,
854 )
855 }
856 };
857}
858
859/// Not public API.
860#[cfg(not(feature = "schema"))]
861#[macro_export]
862#[doc(hidden)]
863macro_rules! __schema_methods {
864 ($key:expr, $secrets:expr) => {
865 ::core::compile_error!(
866 "dynamic-config: `schema` in #[dynamic_config(..)] requires the `schema` feature; \
867 add features = [\"schema\"] to your dynamic-config dependency"
868 );
869 };
870}
871
872/// Not public API.
873///
874/// Expands to the async half of the generated `impl`. It lives here rather than
875/// in the proc-macro because these method *signatures* mention tokio types, and
876/// a signature cannot be hidden behind an expression-level `compile_error!`.
877#[cfg(feature = "async")]
878#[macro_export]
879#[doc(hidden)]
880macro_rules! __async_methods {
881 ($name:ident) => {
882 /// Reads the configuration without blocking the async executor.
883 ///
884 /// # Errors
885 ///
886 /// Same as `load`, plus if the blocking task is cancelled.
887 pub async fn load_async() -> ::core::result::Result<Self, $crate::Error> {
888 $crate::load_async(Self::dynamic_config_spec()).await
889 }
890
891 /// Loads the configuration and installs it as the initial snapshot,
892 /// without blocking the async executor.
893 ///
894 /// # Errors
895 ///
896 /// Same as `load_async`.
897 pub async fn init_async() -> ::core::result::Result<(), $crate::Error> {
898 $crate::off_thread(Self::dynamic_config_apply).await?;
899
900 ::core::result::Result::Ok(())
901 }
902
903 /// A handle woken by every later reload.
904 ///
905 /// The snapshot current at this call counts as already seen, so the
906 /// first `changed().await` waits for the *next* reload. Read the value
907 /// you start from with `current()`.
908 ///
909 /// Runtime-agnostic: it is a `Future`, so tokio, async-std, smol and a
910 /// hand-written executor all drive it the same way. Unlike `current()`
911 /// it never panics — a handle taken before `init()` simply waits for
912 /// the first snapshot.
913 pub fn changes() -> $crate::Changes<Self> {
914 Self::dynamic_config_cell().changes()
915 }
916 };
917}
918
919/// Not public API.
920///
921/// The async half of the remote API. Its own redirect rather than part of
922/// `__async_methods!`, because a program can want an async *store* without
923/// wanting the async *loading* surface — they are different axes.
924#[cfg(feature = "async")]
925#[macro_export]
926#[doc(hidden)]
927macro_rules! __async_remote_methods {
928 () => {
929 /// Installs an async remote store to read configuration from.
930 ///
931 /// Nothing is fetched here; call
932 /// [`refresh_remote_async`](Self::refresh_remote_async) for that.
933 /// Installing a source drops whatever the previous one had fetched.
934 pub fn set_remote_async(source: impl $crate::AsyncRemoteSource) {
935 Self::dynamic_config_remote().set_async(source);
936 }
937
938 /// Reads the remote store, and keeps what came back.
939 ///
940 /// Works with a blocking source too, so swapping one implementation for
941 /// the other is not a breaking change for the caller.
942 ///
943 /// # Errors
944 ///
945 /// If no source is installed, or the fetch fails.
946 pub async fn refresh_remote_async() -> ::core::result::Result<(), $crate::Error> {
947 Self::dynamic_config_remote().refresh_async().await
948 }
949 };
950}
951
952/// Not public API.
953#[cfg(not(feature = "async"))]
954#[macro_export]
955#[doc(hidden)]
956macro_rules! __async_remote_methods {
957 () => {};
958}
959
960/// Not public API.
961#[cfg(not(feature = "async"))]
962#[macro_export]
963#[doc(hidden)]
964macro_rules! __async_methods {
965 ($name:ident) => {
966 ::core::compile_error!(
967 "dynamic-config: `async` in #[dynamic_config(..)] requires the `async` feature \
968 (or `tokio`, which implies it); add features = [\"async\"] to your \
969 dynamic-config dependency"
970 );
971 };
972}