dynamic_config_embedded/lib.rs
1//! Hot-reloadable configuration for `no_std` targets.
2//!
3//! [`dynamic-config`] reads files, searches directories and merges layers with
4//! [figment]. A microcontroller has no files, no directories and no allocator,
5//! and figment is `std` — so this is not that crate with a feature switched
6//! off. It is the same *shape*, built from what a device actually has.
7//!
8//! ```rust
9//! use dynamic_config_embedded::{ConfigCell, Format, Validate};
10//! use serde::Deserialize;
11//!
12//! #[derive(Debug, Deserialize, Clone)]
13//! struct Settings {
14//! interval_ms: u32,
15//! verbose: bool,
16//! }
17//!
18//! // Accepting everything is the default; implement it to reject a
19//! // configuration whose fields are individually fine and jointly wrong.
20//! impl Validate for Settings {}
21//!
22//! static SETTINGS: ConfigCell<Settings> = ConfigCell::new();
23//!
24//! // Compiled-in defaults, so the device is configured before anything arrives.
25//! SETTINGS.store(Settings { interval_ms: 1000, verbose: false });
26//!
27//! // A document from wherever this device gets one: a serial link, an MQTT
28//! // message, a page of flash.
29//! # #[cfg(feature = "json")]
30//! SETTINGS.apply(br#"{"interval_ms": 250, "verbose": true}"#, Format::Json)?;
31//!
32//! # #[cfg(feature = "json")]
33//! assert_eq!(SETTINGS.get().unwrap().interval_ms, 250);
34//! # Ok::<(), dynamic_config_embedded::Error>(())
35//! ```
36//!
37//! # What it keeps from the big crate
38//!
39//! - **A snapshot in a `static`**, replaced whole. A reader never sees a
40//! half-applied configuration.
41//! - **A bad document cannot take the process down.** Parsing and validation
42//! happen before anything is installed; a failure leaves the previous
43//! configuration serving.
44//! - **`changes()`**, a `Future` that resolves on the next configuration. The
45//! same generation-counter-and-wakers design as the `std` crate, which is why
46//! it drives on Embassy, RTIC, or a hand-written executor.
47//! - **Validation**, through the same [`Validate`] shape.
48//!
49//! # What it cannot keep, and why
50//!
51//! | | |
52//! |---|---|
53//! | Files, directory search, profiles | there is no filesystem |
54//! | Environment variables | there is no environment |
55//! | Layered merging | figment is `std`, and merging needs a value tree that allocates |
56//! | `Arc` snapshots | no allocator; readers get a `Copy` or a clone instead |
57//! | Provenance (`source_of`) | there is one source, so the question does not arise |
58//!
59//! A device gets *one* document at a time and replaces the whole
60//! configuration. That is not a reduced version of layering — it is what
61//! configuring a device looks like.
62//!
63//! # No allocator, and no lock a reader can block on
64//!
65//! Storage is a `critical-section` around a plain slot: a handful of
66//! instructions with interrupts masked, which is the primitive every embedded
67//! HAL provides and the only one this crate needs. Readers clone the value out;
68//! there is no `Arc` to hand back because there is no allocator to make one.
69//!
70//! That makes `T: Clone` the price of admission. For a configuration struct of
71//! scalars — which is what a device's configuration is — the clone is a memcpy.
72//!
73//! [`dynamic-config`]: https://docs.rs/dynamic-config
74//! [figment]: https://docs.rs/figment
75
76#![no_std]
77#![forbid(unsafe_code)]
78#![deny(missing_docs)]
79#![cfg_attr(docsrs, feature(doc_cfg))]
80
81#[cfg(feature = "async")]
82mod asynchronous;
83mod cell;
84mod error;
85
86#[cfg(feature = "async")]
87#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
88pub use asynchronous::{Changes, WAITERS};
89pub use cell::ConfigCell;
90pub use error::{Error, ErrorKind};
91
92/// How a document is written.
93///
94/// One variant today. It is an enum rather than an assumption so that a second
95/// format — CBOR is the one that would earn its place on a device — does not
96/// change every signature when it arrives.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98#[non_exhaustive]
99pub enum Format {
100 /// JSON, via `serde-json-core`. Allocates nothing.
101 #[cfg(feature = "json")]
102 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
103 Json,
104}
105
106/// A configuration that can reject itself.
107///
108/// The same idea as the `validate` argument in the `std` crate: every field can
109/// be individually valid and the whole still wrong — a window that ends before
110/// it starts, a buffer larger than the RAM on the part.
111///
112/// Implemented for every `T` by default, accepting everything, so implementing
113/// it is opt-in:
114///
115/// ```
116/// # use dynamic_config_embedded::Validate;
117/// struct Settings {
118/// low_ms: u32,
119/// high_ms: u32,
120/// }
121///
122/// impl Validate for Settings {
123/// fn validate(&self) -> Result<(), &'static str> {
124/// if self.low_ms >= self.high_ms {
125/// return Err("low_ms must be below high_ms");
126/// }
127///
128/// Ok(())
129/// }
130/// }
131/// ```
132///
133/// The error is a `&'static str` rather than a `String`: there is no allocator,
134/// and a fixed message is what a device can log anyway.
135pub trait Validate {
136 /// Rejects a configuration that is not usable.
137 ///
138 /// # Errors
139 ///
140 /// A short, static description of what is wrong with it.
141 fn validate(&self) -> Result<(), &'static str> {
142 Ok(())
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn validation_is_opt_in() {
152 struct Anything;
153
154 impl Validate for Anything {}
155
156 assert!(Anything.validate().is_ok());
157 }
158}