Skip to main content

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}