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> {
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,
38}
39
40impl<T> ConfigCell<T> {
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> ConfigCell<T> {
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> {
87        crate::Changes::new(self)
88    }
89
90    #[cfg(feature = "async")]
91    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
92        &self.notify
93    }
94}
95
96impl<T: Clone + DeserializeOwned + Validate> ConfigCell<T> {
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            .map(|(value, _consumed)| value)
132            .map_err(|error| {
133                // `serde-json-core` distinguishes a malformed document from one
134                // that does not fit, and the difference is what a person
135                // debugging this needs first.
136                let kind = match error {
137                    // A missing field or a value of the wrong shape reaches
138                    // serde as a custom error; malformed bytes do not get that
139                    // far.
140                    serde_json_core::de::Error::InvalidType
141                    | serde_json_core::de::Error::CustomError => ErrorKind::Type,
142                    _ => ErrorKind::Parse,
143                };
144
145                Error::new(kind, "the document is not a configuration of this shape")
146            }),
147
148        #[allow(unreachable_patterns)]
149        _ => Err(Error::new(
150            ErrorKind::Unsupported,
151            "the format's feature is not enabled in this build",
152        )),
153    }
154}
155
156impl<T> Default for ConfigCell<T> {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl<T> core::fmt::Debug for ConfigCell<T> {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        // Never the value: a configuration holds whatever a device was told,
165        // which on a device is as likely to be a key as anything else.
166        f.debug_struct("ConfigCell").finish_non_exhaustive()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use serde::Deserialize;
173
174    use super::*;
175
176    #[derive(Debug, Clone, PartialEq, Deserialize)]
177    struct Settings {
178        interval_ms: u32,
179        verbose: bool,
180    }
181
182    impl Validate for Settings {
183        fn validate(&self) -> Result<(), &'static str> {
184            if self.interval_ms == 0 {
185                return Err("interval_ms of zero would spin");
186            }
187
188            Ok(())
189        }
190    }
191
192    #[test]
193    fn an_empty_cell_answers_nothing() {
194        let cell: ConfigCell<Settings> = ConfigCell::new();
195
196        assert!(!cell.is_set());
197        assert!(cell.get().is_none());
198    }
199
200    #[test]
201    fn a_stored_value_comes_back() {
202        let cell = ConfigCell::new();
203
204        cell.store(Settings {
205            interval_ms: 1000,
206            verbose: false,
207        });
208
209        assert_eq!(cell.get().unwrap().interval_ms, 1000);
210    }
211
212    #[cfg(feature = "json")]
213    #[test]
214    fn a_document_replaces_the_configuration() {
215        let cell = ConfigCell::new();
216
217        cell.store(Settings {
218            interval_ms: 1000,
219            verbose: false,
220        });
221
222        cell.apply(br#"{"interval_ms": 250, "verbose": true}"#, Format::Json)
223            .expect("the document fits");
224
225        assert_eq!(
226            cell.get().unwrap(),
227            Settings {
228                interval_ms: 250,
229                verbose: true,
230            }
231        );
232    }
233
234    #[cfg(feature = "json")]
235    #[test]
236    fn a_document_that_does_not_parse_leaves_the_previous_one_serving() {
237        let cell = ConfigCell::new();
238
239        cell.store(Settings {
240            interval_ms: 1000,
241            verbose: false,
242        });
243
244        let error = cell
245            .apply(b"{not json", Format::Json)
246            .expect_err("that is not a document");
247
248        assert_eq!(error.kind(), ErrorKind::Parse);
249        assert_eq!(
250            cell.get().unwrap().interval_ms,
251            1000,
252            "a bad document must not take the device's configuration with it"
253        );
254    }
255
256    #[cfg(feature = "json")]
257    #[test]
258    fn a_document_that_fails_validation_is_refused_whole() {
259        let cell = ConfigCell::new();
260
261        cell.store(Settings {
262            interval_ms: 1000,
263            verbose: false,
264        });
265
266        let error = cell
267            .apply(br#"{"interval_ms": 0, "verbose": true}"#, Format::Json)
268            .expect_err("zero would spin");
269
270        assert_eq!(error.kind(), ErrorKind::Invalid);
271        assert_eq!(error.message(), "interval_ms of zero would spin");
272        assert!(
273            !cell.get().unwrap().verbose,
274            "not even the fields that were fine"
275        );
276    }
277
278    #[cfg(feature = "json")]
279    #[test]
280    fn a_document_missing_a_field_is_a_type_error_not_a_parse_error() {
281        let cell: ConfigCell<Settings> = ConfigCell::new();
282
283        let error = cell
284            .apply(br#"{"interval_ms": 250}"#, Format::Json)
285            .expect_err("`verbose` is missing");
286
287        // The positive claim, not `assert_ne`: "anything but Invalid" would
288        // also accept `Parse`, which is exactly the misclassification the
289        // test's name promises against.
290        assert_eq!(error.kind(), ErrorKind::Type);
291    }
292
293    /// `Debug` must not print the value, and checking that needs a formatter —
294    /// which needs an allocator, which is why this one is host-only.
295    #[cfg(feature = "std")]
296    #[test]
297    fn debug_never_prints_the_configuration() {
298        extern crate std;
299
300        let cell = ConfigCell::new();
301
302        cell.store(Settings {
303            interval_ms: 1234,
304            verbose: true,
305        });
306
307        assert!(!std::format!("{cell:?}").contains("1234"));
308    }
309}