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
52impl<T: Clone, const WAITERS: usize> ConfigCell<T, WAITERS> {
53    /// Installs `value`, replacing whatever was there.
54    ///
55    /// For compiled-in defaults at start-up, and for anything that builds a
56    /// configuration without parsing one.
57    pub fn store(&self, value: T) {
58        critical_section::with(|token| {
59            self.inner.borrow(token).replace(Some(value));
60        });
61
62        #[cfg(feature = "async")]
63        self.notify.bump();
64    }
65
66    /// The current configuration, or `None` before anything is stored.
67    ///
68    /// Cloned out: there is no allocator, so there is no `Arc` to hand back.
69    /// Call it once and reuse the value — two calls could straddle a store and
70    /// let one piece of work observe two configurations.
71    #[must_use]
72    pub fn get(&self) -> Option<T> {
73        critical_section::with(|token| self.inner.borrow(token).borrow().clone())
74    }
75
76    /// Whether anything has been stored.
77    #[must_use]
78    pub fn is_set(&self) -> bool {
79        critical_section::with(|token| self.inner.borrow(token).borrow().is_some())
80    }
81
82    /// A handle that resolves each time the configuration is replaced.
83    #[cfg(feature = "async")]
84    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
85    #[must_use]
86    pub fn changes(&'static self) -> crate::Changes<T, WAITERS> {
87        crate::Changes::new(self)
88    }
89
90    #[cfg(feature = "async")]
91    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify<WAITERS> {
92        &self.notify
93    }
94}
95
96impl<T: Clone + DeserializeOwned + Validate, const WAITERS: usize> ConfigCell<T, WAITERS> {
97    /// Parses `document` and installs it, if it is usable.
98    ///
99    /// Everything that can fail happens before anything is installed: a
100    /// document that does not parse, does not fit, or does not validate leaves
101    /// the previous configuration serving. That is the whole reason this is one
102    /// call rather than parse-then-store.
103    ///
104    /// # Errors
105    ///
106    /// If the bytes are not valid in `format`, do not fit `T`, or `T` rejects
107    /// itself.
108    pub fn apply(&self, document: &[u8], format: Format) -> Result<(), Error> {
109        let value = parse::<T>(document, format)?;
110
111        value
112            .validate()
113            .map_err(|message| Error::new(ErrorKind::Invalid, message))?;
114
115        self.store(value);
116
117        Ok(())
118    }
119}
120
121/// Parses a document without allocating.
122fn parse<T: DeserializeOwned>(document: &[u8], format: Format) -> Result<T, Error> {
123    // With no format feature on, `Format` has no variants and the match below
124    // is empty — so nothing reads either argument.
125    #[cfg(not(feature = "json"))]
126    let _ = (document, format);
127
128    match format {
129        #[cfg(feature = "json")]
130        Format::Json => serde_json_core::from_slice::<T>(document)
131            .and_then(|(value, consumed)| {
132                // The document must be *all* of the buffer. On a device the
133                // bytes arrive over a link into a reused buffer, and a short
134                // write leaving the tail of a longer previous document — or
135                // two concatenated frames — parses cleanly as the first
136                // object. Installing a configuration nobody sent is exactly
137                // what "everything fallible happens before install" is for.
138                if consumed == document.len() {
139                    Ok(value)
140                } else {
141                    Err(serde_json_core::de::Error::TrailingCharacters)
142                }
143            })
144            .map_err(|error| {
145                // `serde-json-core` distinguishes a malformed document from one
146                // that does not fit, and the difference is what a person
147                // debugging this needs first.
148                let kind = match error {
149                    // A missing field or a value of the wrong shape reaches
150                    // serde as a custom error; malformed bytes do not get that
151                    // far.
152                    serde_json_core::de::Error::InvalidType
153                    | serde_json_core::de::Error::CustomError => ErrorKind::Type,
154                    _ => ErrorKind::Parse,
155                };
156
157                Error::new(kind, "the document is not a configuration of this shape")
158            }),
159
160        #[allow(unreachable_patterns)]
161        _ => Err(Error::new(
162            ErrorKind::Unsupported,
163            "the format's feature is not enabled in this build",
164        )),
165    }
166}
167
168impl<T, const WAITERS: usize> Default for ConfigCell<T, WAITERS> {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl<T> core::fmt::Debug for ConfigCell<T> {
175    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176        // Never the value: a configuration holds whatever a device was told,
177        // which on a device is as likely to be a key as anything else.
178        f.debug_struct("ConfigCell").finish_non_exhaustive()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use serde::Deserialize;
185
186    use super::*;
187
188    #[derive(Debug, Clone, PartialEq, Deserialize)]
189    struct Settings {
190        interval_ms: u32,
191        verbose: bool,
192    }
193
194    impl Validate for Settings {
195        fn validate(&self) -> Result<(), &'static str> {
196            if self.interval_ms == 0 {
197                return Err("interval_ms of zero would spin");
198            }
199
200            Ok(())
201        }
202    }
203
204    #[test]
205    fn an_empty_cell_answers_nothing() {
206        let cell: ConfigCell<Settings> = ConfigCell::new();
207
208        assert!(!cell.is_set());
209        assert!(cell.get().is_none());
210    }
211
212    #[test]
213    fn a_stored_value_comes_back() {
214        let cell: ConfigCell<Settings> = ConfigCell::new();
215
216        cell.store(Settings {
217            interval_ms: 1000,
218            verbose: false,
219        });
220
221        assert_eq!(cell.get().unwrap().interval_ms, 1000);
222    }
223
224    #[cfg(feature = "json")]
225    #[test]
226    fn a_document_replaces_the_configuration() {
227        let cell: ConfigCell<Settings> = ConfigCell::new();
228
229        cell.store(Settings {
230            interval_ms: 1000,
231            verbose: false,
232        });
233
234        cell.apply(br#"{"interval_ms": 250, "verbose": true}"#, Format::Json)
235            .expect("the document fits");
236
237        assert_eq!(
238            cell.get().unwrap(),
239            Settings {
240                interval_ms: 250,
241                verbose: true,
242            }
243        );
244    }
245
246    #[cfg(feature = "json")]
247    #[test]
248    fn a_document_that_does_not_parse_leaves_the_previous_one_serving() {
249        let cell: ConfigCell<Settings> = ConfigCell::new();
250
251        cell.store(Settings {
252            interval_ms: 1000,
253            verbose: false,
254        });
255
256        let error = cell
257            .apply(b"{not json", Format::Json)
258            .expect_err("that is not a document");
259
260        assert_eq!(error.kind(), ErrorKind::Parse);
261        assert_eq!(
262            cell.get().unwrap().interval_ms,
263            1000,
264            "a bad document must not take the device's configuration with it"
265        );
266    }
267
268    #[cfg(feature = "json")]
269    #[test]
270    fn a_document_that_fails_validation_is_refused_whole() {
271        let cell: ConfigCell<Settings> = ConfigCell::new();
272
273        cell.store(Settings {
274            interval_ms: 1000,
275            verbose: false,
276        });
277
278        let error = cell
279            .apply(br#"{"interval_ms": 0, "verbose": true}"#, Format::Json)
280            .expect_err("zero would spin");
281
282        assert_eq!(error.kind(), ErrorKind::Invalid);
283        assert_eq!(error.message(), "interval_ms of zero would spin");
284        assert!(
285            !cell.get().unwrap().verbose,
286            "not even the fields that were fine"
287        );
288    }
289
290    #[cfg(feature = "json")]
291    #[test]
292    fn a_document_missing_a_field_is_a_type_error_not_a_parse_error() {
293        let cell: ConfigCell<Settings> = ConfigCell::new();
294
295        let error = cell
296            .apply(br#"{"interval_ms": 250}"#, Format::Json)
297            .expect_err("`verbose` is missing");
298
299        // The positive claim, not `assert_ne`: "anything but Invalid" would
300        // also accept `Parse`, which is exactly the misclassification the
301        // test's name promises against.
302        assert_eq!(error.kind(), ErrorKind::Type);
303    }
304
305    /// `Debug` must not print the value, and checking that needs a formatter —
306    /// which needs an allocator, which is why this one is host-only.
307    #[cfg(feature = "std")]
308    #[test]
309    fn debug_never_prints_the_configuration() {
310        extern crate std;
311
312        let cell: ConfigCell<Settings> = ConfigCell::new();
313
314        cell.store(Settings {
315            interval_ms: 1234,
316            verbose: true,
317        });
318
319        assert!(!std::format!("{cell:?}").contains("1234"));
320    }
321}