Skip to main content

tocat_api/
forgiving.rs

1//! forgiving.rs: the deserializer that applies [`normalize`] to a plugin's own
2//! config, without the plugin having to know.
3//!
4//! [`normalize`]: crate::normalize::normalize
5//!
6//! The host cannot simply normalize the keys it was given. serde matches field
7//! names by exact comparison against a set fixed at compile time by
8//! `rename_all`, `rename` and `alias`, so rewriting `max-connections` to
9//! `maxconnections` would stop matching a `rename_all = "kebab-case"` struct,
10//! and `deny_unknown_fields` would turn the miss into a hard error. The host
11//! also cannot know a plugin's field names in advance, and for a WASM guest it
12//! never will.
13//!
14//! But serde hands them over at the moment of use.
15//! `Deserializer::deserialize_struct` receives the declared field list, and
16//! `deserialize_enum` the declared variants. So this deserializer normalizes to
17//! *match* and then rewrites the key to the plugin's *own* spelling before the
18//! derive ever sees it. The plugin's struct, its `rename_all` and its
19//! `deny_unknown_fields` behave exactly as written, aliases keep working, and
20//! an option nobody declared still reaches the plugin to be rejected there.
21//!
22//! Everything else delegates to `serde_json`, so a type with a hand-written
23//! `Deserialize` (rate's `Interval`, which asks for `deserialize_any` and
24//! parses `"500ms"` itself) is untouched.
25
26use serde::{
27    de::{self, DeserializeSeed, Deserializer, IntoDeserializer, MapAccess, SeqAccess, Visitor},
28    forward_to_deserialize_any,
29};
30use serde_json::{Map, Value};
31
32use crate::normalize::canonical;
33
34/// A `serde_json::Value` that matches identifiers the way the rest of tocat
35/// does.
36pub struct Forgiving(pub Value);
37
38/// Rewrite each key to the declared spelling it means.
39///
40/// Two spellings of one option are an error rather than a silent win for
41/// whichever the map happens to yield last: `Map` here is a `BTreeMap`, so
42/// "last" would mean alphabetically last, which is nobody's intent.
43fn rekey(
44    map: Map<String, Value>,
45    fields: &[&str],
46) -> Result<Map<String, Value>, serde_json::Error> {
47    let mut out = Map::new();
48
49    for (key, value) in map {
50        let key = match canonical(&key, fields) {
51            Some(field) => field.to_string(),
52            None => key,
53        };
54
55        if out.contains_key(&key) {
56            return Err(de::Error::custom(format!(
57                "`{key}` was given more than once, under more than one spelling"
58            )));
59        }
60
61        out.insert(key, value);
62    }
63
64    Ok(out)
65}
66
67impl<'de> Deserializer<'de> for Forgiving {
68    type Error = serde_json::Error;
69
70    fn deserialize_struct<V: Visitor<'de>>(
71        self,
72        name: &'static str,
73        fields: &'static [&'static str],
74        visitor: V,
75    ) -> Result<V::Value, Self::Error> {
76        match self.0 {
77            Value::Object(map) => visitor.visit_map(ForgivingMap::new(rekey(map, fields)?)),
78            other => other.deserialize_struct(name, fields, visitor),
79        }
80    }
81
82    /// A struct with `#[serde(flatten)]` arrives here instead, without a field
83    /// list, so its keys keep exact matching. Its values are still visited
84    /// through this deserializer.
85    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
86        match self.0 {
87            Value::Object(map) => visitor.visit_map(ForgivingMap::new(map)),
88            other => other.deserialize_map(visitor),
89        }
90    }
91
92    /// Unit variants only, which is all the `key=value` grammar can express.
93    /// A variant carrying data is left to `serde_json`.
94    fn deserialize_enum<V: Visitor<'de>>(
95        self,
96        name: &'static str,
97        variants: &'static [&'static str],
98        visitor: V,
99    ) -> Result<V::Value, Self::Error> {
100        let value = match self.0 {
101            Value::String(tag) => match canonical(&tag, variants) {
102                Some(variant) => Value::String(variant.to_string()),
103                None => Value::String(tag),
104            },
105            other => other,
106        };
107
108        value.deserialize_enum(name, variants, visitor)
109    }
110
111    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
112        match self.0 {
113            Value::Array(items) => visitor.visit_seq(ForgivingSeq(items.into_iter())),
114            other => other.deserialize_seq(visitor),
115        }
116    }
117
118    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
119        match self.0 {
120            Value::Null => visitor.visit_none(),
121            value => visitor.visit_some(Forgiving(value)),
122        }
123    }
124
125    fn deserialize_newtype_struct<V: Visitor<'de>>(
126        self,
127        _name: &'static str,
128        visitor: V,
129    ) -> Result<V::Value, Self::Error> {
130        visitor.visit_newtype_struct(Forgiving(self.0))
131    }
132
133    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
134        self.0.deserialize_any(visitor)
135    }
136
137    forward_to_deserialize_any! {
138        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
139        bytes byte_buf unit unit_struct tuple tuple_struct identifier ignored_any
140    }
141}
142
143struct ForgivingMap {
144    entries: serde_json::map::IntoIter,
145    value: Option<Value>,
146}
147
148impl ForgivingMap {
149    fn new(map: Map<String, Value>) -> Self {
150        Self {
151            entries: map.into_iter(),
152            value: None,
153        }
154    }
155}
156
157impl<'de> MapAccess<'de> for ForgivingMap {
158    type Error = serde_json::Error;
159
160    fn next_key_seed<K: DeserializeSeed<'de>>(
161        &mut self,
162        seed: K,
163    ) -> Result<Option<K::Value>, Self::Error> {
164        let Some((key, value)) = self.entries.next() else {
165            return Ok(None);
166        };
167
168        self.value = Some(value);
169        seed.deserialize(key.into_deserializer()).map(Some)
170    }
171
172    fn next_value_seed<V: DeserializeSeed<'de>>(
173        &mut self,
174        seed: V,
175    ) -> Result<V::Value, Self::Error> {
176        let value = self.value.take().unwrap_or(Value::Null);
177        seed.deserialize(Forgiving(value))
178    }
179
180    fn size_hint(&self) -> Option<usize> {
181        Some(self.entries.len())
182    }
183}
184
185struct ForgivingSeq(std::vec::IntoIter<Value>);
186
187impl<'de> SeqAccess<'de> for ForgivingSeq {
188    type Error = serde_json::Error;
189
190    fn next_element_seed<T: DeserializeSeed<'de>>(
191        &mut self,
192        seed: T,
193    ) -> Result<Option<T::Value>, Self::Error> {
194        match self.0.next() {
195            Some(value) => seed.deserialize(Forgiving(value)).map(Some),
196            None => Ok(None),
197        }
198    }
199
200    fn size_hint(&self) -> Option<usize> {
201        Some(self.0.len())
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use serde::Deserialize;
208    use serde_json::json;
209
210    use super::*;
211
212    #[derive(Debug, Deserialize, PartialEq)]
213    #[serde(rename_all = "kebab-case")]
214    enum Format {
215        Hex,
216        #[serde(alias = "raw")]
217        RawBinary,
218    }
219
220    #[derive(Debug, Deserialize, PartialEq)]
221    #[serde(rename_all = "kebab-case", deny_unknown_fields)]
222    struct Config {
223        format: Format,
224        #[serde(default)]
225        max_connections: Option<u32>,
226        #[serde(default)]
227        label: Option<String>,
228    }
229
230    fn parse(value: Value) -> Result<Config, serde_json::Error> {
231        Config::deserialize(Forgiving(value))
232    }
233
234    #[test]
235    fn keys_and_values_tolerate_any_spelling() {
236        let config = parse(json!({"Format": "Raw_Binary", "max_connections": 4})).unwrap();
237
238        assert_eq!(config.format, Format::RawBinary);
239        assert_eq!(config.max_connections, Some(4));
240    }
241
242    #[test]
243    fn declared_aliases_still_reach_serde() {
244        assert_eq!(
245            parse(json!({"format": "raw"})).unwrap().format,
246            Format::RawBinary
247        );
248    }
249
250    #[test]
251    fn values_are_not_touched() {
252        let config = parse(json!({"format": "hex", "label": "Wire_Tap"})).unwrap();
253
254        assert_eq!(config.label.as_deref(), Some("Wire_Tap"));
255    }
256
257    #[test]
258    fn unknown_options_are_still_rejected() {
259        assert!(parse(json!({"format": "hex", "nonsense": 1})).is_err());
260    }
261
262    #[test]
263    fn one_option_under_two_spellings_is_an_error() {
264        assert!(
265            parse(json!({"format": "hex", "max-connections": 1, "maxconnections": 2})).is_err()
266        );
267    }
268}