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;
89
90/// How many tasks a [`ConfigCell`] can park by default.
91///
92/// Override it per cell with the second type parameter —
93/// `ConfigCell<Settings, 8>` parks eight. Size it to the number of tasks
94/// that genuinely await this configuration: beyond the limit, waiters evict
95/// and wake each other in a churn that keeps the executor from idling.
96pub const DEFAULT_WAITERS: usize = 4;
97pub use cell::ConfigCell;
98pub use error::{Error, ErrorKind};
99
100/// How a document is written.
101///
102/// One variant today. It is an enum rather than an assumption so that a second
103/// format — CBOR is the one that would earn its place on a device — does not
104/// change every signature when it arrives.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106#[non_exhaustive]
107pub enum Format {
108 /// JSON, via `serde-json-core`. Allocates nothing.
109 #[cfg(feature = "json")]
110 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
111 Json,
112}
113
114/// A configuration that can reject itself.
115///
116/// The same idea as the `validate` argument in the `std` crate: every field can
117/// be individually valid and the whole still wrong — a window that ends before
118/// it starts, a buffer larger than the RAM on the part.
119///
120/// Implemented for every `T` by default, accepting everything, so implementing
121/// it is opt-in:
122///
123/// ```
124/// # use dynamic_config_embedded::Validate;
125/// struct Settings {
126/// low_ms: u32,
127/// high_ms: u32,
128/// }
129///
130/// impl Validate for Settings {
131/// fn validate(&self) -> Result<(), &'static str> {
132/// if self.low_ms >= self.high_ms {
133/// return Err("low_ms must be below high_ms");
134/// }
135///
136/// Ok(())
137/// }
138/// }
139/// ```
140///
141/// The error is a `&'static str` rather than a `String`: there is no allocator,
142/// and a fixed message is what a device can log anyway.
143pub trait Validate {
144 /// Rejects a configuration that is not usable.
145 ///
146 /// # Errors
147 ///
148 /// A short, static description of what is wrong with it.
149 fn validate(&self) -> Result<(), &'static str> {
150 Ok(())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn validation_is_opt_in() {
160 struct Anything;
161
162 impl Validate for Anything {}
163
164 assert!(Anything.validate().is_ok());
165 }
166}