dynamic_config_embedded/cell.rs
1//! Storage for one configuration, in a `static`, with no allocator.
2
3use core::cell::RefCell;
4
5use critical_section::Mutex;
6use serde::de::DeserializeOwned;
7
8use crate::error::{Error, ErrorKind};
9use crate::{Format, Validate};
10
11/// Process-wide storage for one configuration type.
12///
13/// Lives in a `static`, which is the only place a device has to put anything
14/// that outlives a function:
15///
16/// ```
17/// # use dynamic_config_embedded::ConfigCell;
18/// # use serde::Deserialize;
19/// # #[derive(Clone, Deserialize)] struct Settings { interval_ms: u32 }
20/// static SETTINGS: ConfigCell<Settings> = ConfigCell::new();
21/// ```
22///
23/// # Why a critical section
24///
25/// A reader has to see either the old configuration or the new one, never a
26/// mixture. On a host that is an `ArcSwap`; on a device without an allocator it
27/// is a few instructions with interrupts masked, which is what
28/// [`critical_section`] provides and what every embedded HAL implements.
29///
30/// The section is held for a clone of the value and nothing else. Keep the
31/// configuration struct small — which a device's configuration is — and that is
32/// a memcpy with interrupts off, measured in microseconds.
33pub struct ConfigCell<T, const WAITERS: usize = { crate::DEFAULT_WAITERS }> {
34 inner: Mutex<RefCell<Option<T>>>,
35 /// Bumped on every store. Zero means nothing has been stored yet.
36 #[cfg(feature = "async")]
37 notify: crate::asynchronous::Notify<WAITERS>,
38}
39
40impl<T, const WAITERS: usize> ConfigCell<T, WAITERS> {
41 /// An empty cell.
42 #[must_use]
43 pub const fn new() -> Self {
44 Self {
45 inner: Mutex::new(RefCell::new(None)),
46 #[cfg(feature = "async")]
47 notify: crate::asynchronous::Notify::new(),
48 }
49 }
50
51 /// How many times a waiter has had to displace another one, saturating.
52 ///
53 /// A [`ConfigCell`] parks `WAITERS` tasks and no more. Past that, a
54 /// registration evicts an existing waiter and wakes it — no wake-up is
55 /// lost, but the two tasks then wake each other for as long as both are
56 /// waiting, and a device that is doing that is not asleep. There is no
57 /// fifth slot to find: a fixed array cannot park what does not fit, and
58 /// the alternatives (drop the waker, refuse the registration) are both a
59 /// task that nobody polls again.
60 ///
61 /// So this is the report. **Non-zero means `WAITERS` is too small for this
62 /// firmware** — raise the second type parameter to the number of tasks
63 /// that genuinely await this configuration. Zero on a device that has run
64 /// its real workload means the budget fits, which is the only proof of
65 /// that worth having.
66 ///
67 /// ```
68 /// # use dynamic_config_embedded::ConfigCell;
69 /// # use serde::Deserialize;
70 /// # #[derive(Clone, Deserialize)] struct Settings { interval_ms: u32 }
71 /// static SETTINGS: ConfigCell<Settings, 4> = ConfigCell::new();
72 ///
73 /// // On a bench, after the firmware has run everything it does:
74 /// assert_eq!(SETTINGS.waiter_evictions(), 0, "raise WAITERS");
75 /// ```
76 #[cfg(feature = "async")]
77 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
78 #[must_use]
79 pub fn waiter_evictions(&self) -> u32 {
80 self.notify.evictions()
81 }
82}
83
84impl<T: Clone, const WAITERS: usize> ConfigCell<T, WAITERS> {
85 /// Installs `value`, replacing whatever was there.
86 ///
87 /// For compiled-in defaults at start-up, and for anything that builds a
88 /// configuration without parsing one.
89 pub fn store(&self, value: T) {
90 critical_section::with(|token| {
91 self.inner.borrow(token).replace(Some(value));
92 });
93
94 #[cfg(feature = "async")]
95 self.notify.bump();
96 }
97
98 /// The current configuration, or `None` before anything is stored.
99 ///
100 /// Cloned out: there is no allocator, so there is no `Arc` to hand back.
101 /// Call it once and reuse the value — two calls could straddle a store and
102 /// let one piece of work observe two configurations.
103 #[must_use]
104 pub fn get(&self) -> Option<T> {
105 critical_section::with(|token| self.inner.borrow(token).borrow().clone())
106 }
107
108 /// Whether anything has been stored.
109 #[must_use]
110 pub fn is_set(&self) -> bool {
111 critical_section::with(|token| self.inner.borrow(token).borrow().is_some())
112 }
113
114 /// A handle that resolves each time the configuration is replaced.
115 #[cfg(feature = "async")]
116 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
117 #[must_use]
118 pub fn changes(&'static self) -> crate::Changes<T, WAITERS> {
119 crate::Changes::new(self)
120 }
121
122 #[cfg(feature = "async")]
123 pub(crate) fn notify(&self) -> &crate::asynchronous::Notify<WAITERS> {
124 &self.notify
125 }
126}
127
128impl<T: Clone + DeserializeOwned + Validate, const WAITERS: usize> ConfigCell<T, WAITERS> {
129 /// Parses `document` and installs it, if it is usable.
130 ///
131 /// Everything that can fail happens before anything is installed: a
132 /// document that does not parse, does not fit, or does not validate leaves
133 /// the previous configuration serving. That is the whole reason this is one
134 /// call rather than parse-then-store.
135 ///
136 /// # Errors
137 ///
138 /// If the bytes are not valid in `format`, do not fit `T`, or `T` rejects
139 /// itself.
140 pub fn apply(&self, document: &[u8], format: Format) -> Result<(), Error> {
141 let value = parse::<T>(document, format)?;
142
143 value
144 .validate()
145 .map_err(|message| Error::new(ErrorKind::Invalid, message))?;
146
147 self.store(value);
148
149 Ok(())
150 }
151}
152
153/// Parses a document without allocating.
154fn parse<T: DeserializeOwned>(document: &[u8], format: Format) -> Result<T, Error> {
155 // With no format feature on, `Format` has no variants and the match below
156 // is empty — so nothing reads either argument.
157 #[cfg(not(feature = "json"))]
158 let _ = (document, format);
159
160 match format {
161 #[cfg(feature = "json")]
162 Format::Json => serde_json_core::from_slice::<T>(document)
163 .and_then(|(value, consumed)| {
164 // The document must be *all* of the buffer. On a device the
165 // bytes arrive over a link into a reused buffer, and a short
166 // write leaving the tail of a longer previous document — or
167 // two concatenated frames — parses cleanly as the first
168 // object. Installing a configuration nobody sent is exactly
169 // what "everything fallible happens before install" is for.
170 if consumed == document.len() {
171 Ok(value)
172 } else {
173 Err(serde_json_core::de::Error::TrailingCharacters)
174 }
175 })
176 .map_err(|error| {
177 // `serde-json-core` distinguishes a malformed document from one
178 // that does not fit, and the difference is what a person
179 // debugging this needs first.
180 let kind = match error {
181 // A missing field or a value of the wrong shape reaches
182 // serde as a custom error; malformed bytes do not get that
183 // far.
184 serde_json_core::de::Error::InvalidType
185 | serde_json_core::de::Error::CustomError => ErrorKind::Type,
186 _ => ErrorKind::Parse,
187 };
188
189 Error::new(kind, "the document is not a configuration of this shape")
190 }),
191
192 #[allow(unreachable_patterns)]
193 _ => Err(Error::new(
194 ErrorKind::Unsupported,
195 "the format's feature is not enabled in this build",
196 )),
197 }
198}
199
200impl<T, const WAITERS: usize> Default for ConfigCell<T, WAITERS> {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206impl<T> core::fmt::Debug for ConfigCell<T> {
207 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208 // Never the value: a configuration holds whatever a device was told,
209 // which on a device is as likely to be a key as anything else.
210 f.debug_struct("ConfigCell").finish_non_exhaustive()
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use serde::Deserialize;
217
218 use super::*;
219
220 #[derive(Debug, Clone, PartialEq, Deserialize)]
221 struct Settings {
222 interval_ms: u32,
223 verbose: bool,
224 }
225
226 impl Validate for Settings {
227 fn validate(&self) -> Result<(), &'static str> {
228 if self.interval_ms == 0 {
229 return Err("interval_ms of zero would spin");
230 }
231
232 Ok(())
233 }
234 }
235
236 #[test]
237 fn an_empty_cell_answers_nothing() {
238 let cell: ConfigCell<Settings> = ConfigCell::new();
239
240 assert!(!cell.is_set());
241 assert!(cell.get().is_none());
242 }
243
244 #[test]
245 fn a_stored_value_comes_back() {
246 let cell: ConfigCell<Settings> = ConfigCell::new();
247
248 cell.store(Settings {
249 interval_ms: 1000,
250 verbose: false,
251 });
252
253 assert_eq!(cell.get().unwrap().interval_ms, 1000);
254 }
255
256 #[cfg(feature = "json")]
257 #[test]
258 fn a_document_replaces_the_configuration() {
259 let cell: ConfigCell<Settings> = ConfigCell::new();
260
261 cell.store(Settings {
262 interval_ms: 1000,
263 verbose: false,
264 });
265
266 cell.apply(br#"{"interval_ms": 250, "verbose": true}"#, Format::Json)
267 .expect("the document fits");
268
269 assert_eq!(
270 cell.get().unwrap(),
271 Settings {
272 interval_ms: 250,
273 verbose: true,
274 }
275 );
276 }
277
278 #[cfg(feature = "json")]
279 #[test]
280 fn a_document_that_does_not_parse_leaves_the_previous_one_serving() {
281 let cell: ConfigCell<Settings> = ConfigCell::new();
282
283 cell.store(Settings {
284 interval_ms: 1000,
285 verbose: false,
286 });
287
288 let error = cell
289 .apply(b"{not json", Format::Json)
290 .expect_err("that is not a document");
291
292 assert_eq!(error.kind(), ErrorKind::Parse);
293 assert_eq!(
294 cell.get().unwrap().interval_ms,
295 1000,
296 "a bad document must not take the device's configuration with it"
297 );
298 }
299
300 #[cfg(feature = "json")]
301 #[test]
302 fn a_document_that_fails_validation_is_refused_whole() {
303 let cell: ConfigCell<Settings> = ConfigCell::new();
304
305 cell.store(Settings {
306 interval_ms: 1000,
307 verbose: false,
308 });
309
310 let error = cell
311 .apply(br#"{"interval_ms": 0, "verbose": true}"#, Format::Json)
312 .expect_err("zero would spin");
313
314 assert_eq!(error.kind(), ErrorKind::Invalid);
315 assert_eq!(error.message(), "interval_ms of zero would spin");
316 assert!(
317 !cell.get().unwrap().verbose,
318 "not even the fields that were fine"
319 );
320 }
321
322 #[cfg(feature = "json")]
323 #[test]
324 fn a_document_missing_a_field_is_a_type_error_not_a_parse_error() {
325 let cell: ConfigCell<Settings> = ConfigCell::new();
326
327 let error = cell
328 .apply(br#"{"interval_ms": 250}"#, Format::Json)
329 .expect_err("`verbose` is missing");
330
331 // The positive claim, not `assert_ne`: "anything but Invalid" would
332 // also accept `Parse`, which is exactly the misclassification the
333 // test's name promises against.
334 assert_eq!(error.kind(), ErrorKind::Type);
335 }
336
337 /// `Debug` must not print the value, and checking that needs a formatter —
338 /// which needs an allocator, which is why this one is host-only.
339 #[cfg(feature = "std")]
340 #[test]
341 fn debug_never_prints_the_configuration() {
342 extern crate std;
343
344 let cell: ConfigCell<Settings> = ConfigCell::new();
345
346 cell.store(Settings {
347 interval_ms: 1234,
348 verbose: true,
349 });
350
351 assert!(!std::format!("{cell:?}").contains("1234"));
352 }
353}