Skip to main content

dynamic_config/
dynamic.rs

1//! Instance-owned configuration: the engine without the `static`.
2//!
3//! `#[dynamic_config]` gives a *type* one configuration, stored in statics
4//! the macro generates. That is the right default and the wrong ceiling:
5//! multi-tenant programs want one configuration per tenant, tests want two
6//! side by side without type gymnastics, and a host language binding has no
7//! Rust type per user class at all. [`Dynamic<T>`] is the same engine with
8//! the storage owned by the value: its own cell, its own hooks, its own
9//! watcher identity — nothing shared with the type-level surface, and
10//! nothing global.
11//!
12//! ```no_run
13//! # #[cfg(feature = "json")] {
14//! use dynamic_config::{Builder, Dynamic};
15//! use serde::Deserialize;
16//!
17//! #[derive(Debug, Deserialize)]
18//! struct Tenant { name: String }
19//!
20//! let acme = Dynamic::new(Builder::new("tenant").file("acme.json"));
21//! let umbra = Dynamic::new(Builder::new("tenant").file("umbra.json"));
22//!
23//! acme.init()?;
24//! umbra.init()?;
25//!
26//! let a: std::sync::Arc<Tenant> = acme.current().expect("initialised above");
27//! let u: std::sync::Arc<Tenant> = umbra.current().expect("initialised above");
28//! # let _ = (a, u);
29//! # }
30//! # Ok::<(), dynamic_config::Error>(())
31//! ```
32
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::Arc;
35
36use serde::de::DeserializeOwned;
37
38use crate::builder::Builder;
39use crate::cell::ConfigCell;
40use crate::error::Error;
41
42/// One process-unique number per instance, for the watcher registry.
43///
44/// A type's watcher is keyed by `TypeId`; every `Dynamic<Value>` is the
45/// same type, so an instance carries a number instead. Starts at one so
46/// zero never names anything — the same "never ambiguous with nothing"
47/// convention the reload generation follows.
48static NEXT_INSTANCE: AtomicU64 = AtomicU64::new(1);
49
50/// A configuration owned by a value rather than a type.
51///
52/// Construct one from a [`Builder`] carrying the sources; everything the
53/// type-level surface does through generated statics happens here through
54/// the instance's own storage. Two instances of the same `T` are fully
55/// independent: separate snapshots, separate reload hooks, separate
56/// watchers, separate caches if configured.
57///
58/// Cloning is deliberately absent: a `Dynamic` is an *owner* — share one
59/// behind an `Arc` when several places read it, which is also what keeps
60/// "who stops the watcher" a question with one answer.
61pub struct Dynamic<T> {
62    cell: Arc<ConfigCell<T>>,
63    builder: Builder<T>,
64    id: u64,
65    /// The registry wants a `&'static str`; leaked once per instance, on
66    /// the first watch, and reused for every stop/start cycle after it.
67    #[cfg(feature = "watch")]
68    watch_name: std::sync::OnceLock<&'static str>,
69}
70
71impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
72    /// Wraps `builder` around storage this instance owns.
73    ///
74    /// The builder's sources, cache and validation hook all apply
75    /// unchanged; an installer the builder already carried (a generated
76    /// `builder()`'s static cell) is replaced by this instance's own.
77    #[must_use]
78    pub fn new(builder: Builder<T>) -> Self {
79        let cell = Arc::new(ConfigCell::new());
80
81        Self {
82            builder: builder.with_cell(Arc::clone(&cell)),
83            cell,
84            id: NEXT_INSTANCE.fetch_add(1, Ordering::Relaxed),
85            #[cfg(feature = "watch")]
86            watch_name: std::sync::OnceLock::new(),
87        }
88    }
89
90    /// Loads and installs as this instance's snapshot.
91    ///
92    /// The same lifecycle as a type's `init()`: validation runs before
93    /// anything installs, a configured cache is written after a clean
94    /// load and recovered from when the sources will not load.
95    ///
96    /// # Errors
97    ///
98    /// Whatever the load reports: a file that will not parse, a missing
99    /// required value, a validation refusal with no cache to fall back on.
100    pub fn init(&self) -> Result<(), Error> {
101        self.builder.init()
102    }
103
104    /// The installed snapshot, if [`init`](Self::init) has succeeded.
105    ///
106    /// One atomic load, no lock — cheap enough per request, but take it
107    /// once per request and reuse the `Arc`, or a reload landing
108    /// mid-request shows one request two configurations. `None` before the
109    /// first successful install: an instance has no place to panic with
110    /// the type's name in it, so absence is an answer rather than an
111    /// accident.
112    #[must_use]
113    pub fn current(&self) -> Option<Arc<T>> {
114        self.cell.load()
115    }
116
117    /// Reads the sources and deserializes, installing nothing.
118    ///
119    /// # Errors
120    ///
121    /// The same failures as [`init`](Self::init).
122    pub fn load(&self) -> Result<T, Error> {
123        self.builder.load()
124    }
125
126    /// One reload: load, validate, install, rewrite the cache.
127    ///
128    /// A failure installs nothing — the previous snapshot keeps serving.
129    ///
130    /// # Errors
131    ///
132    /// The same failures as [`load`](Self::load).
133    pub fn reload(&self) -> Result<(), Error> {
134        self.builder.reload()
135    }
136
137    /// Runs `hook` after every later install, for the instance's lifetime.
138    ///
139    /// The same contract as the type-level `on_reload`: called with the
140    /// outgoing and incoming snapshots, on whichever thread performed the
141    /// reload — compare, then signal the subsystem that owns the resource.
142    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
143        self.cell.on_reload(hook);
144    }
145
146    /// [`on_reload`](Self::on_reload), until the returned guard drops.
147    pub fn on_reload_scoped(
148        &self,
149        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
150    ) -> crate::HookGuard<T> {
151        ConfigCell::on_reload_scoped_shared(&self.cell, hook)
152    }
153
154    /// This instance's builder, for the diagnostics that answer without
155    /// installing: `source_of`, `is_set`, `check`, `explain`, `snapshot`.
156    ///
157    /// The instance does not re-wrap them — the builder's answers *are*
158    /// the instance's answers, because the builder is where its sources
159    /// live.
160    #[must_use]
161    pub fn builder(&self) -> &Builder<T> {
162        &self.builder
163    }
164
165    /// The section key this instance reads.
166    #[must_use]
167    pub fn key(&self) -> &str {
168        self.builder.key()
169    }
170}
171
172#[cfg(feature = "watch")]
173#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
174impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
175    /// Reloads on file changes until the returned handle is dropped.
176    ///
177    /// The same watcher as everything else — same debounce, same
178    /// directory-level watches — registered under this *instance* rather
179    /// than the type: two instances of one `T` watch side by side, and a
180    /// second watch on the *same* instance is `AlreadyExists`, exactly the
181    /// one-watcher-per-owner contract the type-level surface has.
182    ///
183    /// # Errors
184    ///
185    /// As the builder's `watch`: no watchable directory, a backend that
186    /// cannot start, or this instance already being watched.
187    pub fn watch(
188        &self,
189        debounce: core::time::Duration,
190    ) -> std::io::Result<crate::watch::WatchHandle> {
191        self.watch_with(debounce, crate::watch::WatchMode::Native)
192    }
193
194    /// [`watch`](Self::watch) with the detection strategy chosen
195    /// explicitly — polling is what network and overlay filesystems need.
196    ///
197    /// # Errors
198    ///
199    /// As [`watch`](Self::watch).
200    pub fn watch_with(
201        &self,
202        debounce: core::time::Duration,
203        mode: crate::watch::WatchMode,
204    ) -> std::io::Result<crate::watch::WatchHandle> {
205        let name = self.watch_name.get_or_init(|| {
206            Box::leak(format!("dynamic:{}#{}", self.builder.key(), self.id).into_boxed_str())
207        });
208
209        self.builder.watch_as(
210            crate::watch::WatchKey::Instance(self.id),
211            name,
212            debounce,
213            mode,
214        )
215    }
216}
217
218#[cfg(feature = "async")]
219#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
220impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
221    /// A handle woken by every later install of *this instance*.
222    ///
223    /// The same contract as the type-level `changes()`: the snapshot
224    /// current at this call counts as already seen, and a handle taken
225    /// before [`init`](Self::init) sees the first install as its first
226    /// change — "wake me when configuration exists". The handle keeps the
227    /// instance's storage alive, so it outliving the `Dynamic` is safe
228    /// rather than subtle.
229    #[must_use]
230    pub fn changes(&self) -> crate::Changes<T> {
231        crate::Changes::new_shared(Arc::clone(&self.cell))
232    }
233
234    /// [`load`](Self::load), off the async executor.
235    ///
236    /// # Errors
237    ///
238    /// The same failures as [`load`](Self::load).
239    pub async fn load_async(&self) -> Result<T, Error> {
240        self.builder.load_async().await
241    }
242
243    /// [`init`](Self::init), off the async executor.
244    ///
245    /// # Errors
246    ///
247    /// The same failures as [`init`](Self::init).
248    pub async fn init_async(&self) -> Result<(), Error> {
249        self.builder.init_async().await
250    }
251}
252
253impl<T> std::fmt::Debug for Dynamic<T> {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        f.debug_struct("Dynamic")
256            .field("id", &self.id)
257            .field("builder", &self.builder)
258            .finish_non_exhaustive()
259    }
260}