Skip to main content

plugx_input/
serde.rs

1//! [`serde`] support for [`Input`] (`serde` Cargo feature).
2//!
3//! Implements [`Serialize`] and [`Deserialize`] with path-aware errors via
4//! [`crate::error`] and [`crate::position`].
5
6use crate::{
7    Input,
8    error::{DeserializeError, EXPECTED_INPUT_TYPES, i128_to_isize, invalid_type},
9    position::InputPath,
10};
11use serde::{
12    Deserializer, Serializer,
13    de::{self, Deserialize, DeserializeSeed, MapAccess, SeqAccess, Visitor},
14    ser::{Serialize, SerializeMap, SerializeSeq},
15};
16use std::{collections::HashMap, fmt};
17
18impl Input {
19    /// Serialize with any [`Serializer`]; infallible for supported output types.
20    pub fn serialize<S: Serializer>(&self, serializer: S) -> S::Ok {
21        serialize_infallible(self, serializer)
22    }
23}
24
25impl Serialize for Input {
26    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
27        Ok(serialize_infallible(self, serializer))
28    }
29}
30
31fn serialize_infallible<S: Serializer>(input: &Input, serializer: S) -> S::Ok {
32    match input {
33        Input::Bool(value) => serializer.serialize_bool(*value).unwrap_infallible(),
34        Input::Int(value) => serializer.serialize_i64(*value as i64).unwrap_infallible(),
35        Input::Float(value) => serializer.serialize_f64(*value).unwrap_infallible(),
36        Input::Str(value) => serializer.serialize_str(value).unwrap_infallible(),
37        Input::List(values) => {
38            let mut seq = serializer
39                .serialize_seq(Some(values.len()))
40                .unwrap_infallible();
41            for value in values {
42                seq.serialize_element(value).unwrap_infallible();
43            }
44            seq.end().unwrap_infallible()
45        }
46        Input::Map(values) => {
47            let mut map = serializer
48                .serialize_map(Some(values.len()))
49                .unwrap_infallible();
50            for (key, value) in values {
51                map.serialize_key(key).unwrap_infallible();
52                map.serialize_value(value).unwrap_infallible();
53            }
54            map.end().unwrap_infallible()
55        }
56    }
57}
58
59trait UnwrapInfallible<T> {
60    fn unwrap_infallible(self) -> T;
61}
62
63impl<T, E: fmt::Debug> UnwrapInfallible<T> for Result<T, E> {
64    fn unwrap_infallible(self) -> T {
65        self.unwrap_or_else(|error| panic!("Input serialization cannot fail: {error:?}"))
66    }
67}
68
69impl<'de> Deserialize<'de> for Input {
70    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
71        deserializer.deserialize_any(InputSeed {
72            path: InputPath::root(),
73        })
74    }
75}
76
77struct InputSeed {
78    path: InputPath,
79}
80
81impl Clone for InputSeed {
82    fn clone(&self) -> Self {
83        Self {
84            path: self.path.clone(),
85        }
86    }
87}
88
89impl<'de> DeserializeSeed<'de> for InputSeed {
90    type Value = Input;
91
92    fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
93        deserializer.deserialize_any(self)
94    }
95}
96
97impl InputSeed {
98    fn invalid_type<E: de::Error>(&self, found: impl Into<String>) -> E {
99        E::custom(invalid_type(self.path.clone(), found).to_string())
100    }
101
102    fn integer_out_of_range<E: de::Error>(&self, value: i128) -> E {
103        E::custom(
104            DeserializeError::IntegerOutOfRange {
105                path: self.path.clone(),
106                value,
107            }
108            .to_string(),
109        )
110    }
111
112    fn int_from_i128<E: de::Error>(&self, value: i128) -> Result<Input, E> {
113        match i128_to_isize(value) {
114            Some(value) => Ok(Input::Int(value)),
115            None => Err(self.integer_out_of_range(value)),
116        }
117    }
118}
119
120impl<'de> Visitor<'de> for InputSeed {
121    type Value = Input;
122
123    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(formatter, "a {EXPECTED_INPUT_TYPES}")
125    }
126
127    fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
128        Ok(Input::Bool(value))
129    }
130
131    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
132        self.int_from_i128(value as i128)
133    }
134
135    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
136        self.int_from_i128(value as i128)
137    }
138
139    fn visit_i128<E: de::Error>(self, value: i128) -> Result<Self::Value, E> {
140        self.int_from_i128(value)
141    }
142
143    fn visit_u128<E: de::Error>(self, value: u128) -> Result<Self::Value, E> {
144        self.int_from_i128(value as i128)
145    }
146
147    fn visit_f32<E: de::Error>(self, value: f32) -> Result<Self::Value, E> {
148        Ok(Input::Float(value as f64))
149    }
150
151    fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
152        Ok(Input::Float(value))
153    }
154
155    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
156        Ok(Input::Str(value.to_owned()))
157    }
158
159    fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
160        Ok(Input::Str(value))
161    }
162
163    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
164        let mut values = Vec::new();
165        let mut index = 0;
166        while let Some(value) = seq.next_element_seed(InputSeed {
167            path: self.path.clone().with_index(index),
168        })? {
169            values.push(value);
170            index += 1;
171        }
172        Ok(Input::List(values))
173    }
174
175    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
176        let mut values = HashMap::new();
177        while let Some(key) = map.next_key::<String>()? {
178            let value = map.next_value_seed(InputSeed {
179                path: self.path.clone().with_key(&key),
180            })?;
181            values.insert(key, value);
182        }
183        Ok(Input::Map(values))
184    }
185
186    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
187        Err(self.invalid_type("null"))
188    }
189
190    fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
191        Err(self.invalid_type("null"))
192    }
193
194    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
195        deserializer.deserialize_any(self)
196    }
197
198    fn visit_newtype_struct<D: Deserializer<'de>>(
199        self,
200        deserializer: D,
201    ) -> Result<Self::Value, D::Error> {
202        deserializer.deserialize_any(self)
203    }
204
205    fn visit_enum<A: de::EnumAccess<'de>>(self, _data: A) -> Result<Self::Value, A::Error> {
206        Err(self.invalid_type("enum"))
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::error::InputSerializeError;
214    use serde_json::{Value, json};
215
216    #[test]
217    fn serialize_is_infallible() {
218        let input = Input::Map(HashMap::from([
219            ("list".to_string(), Input::from([1, 2, 3])),
220            ("flag".to_string(), Input::from(true)),
221        ]));
222        let _: Result<Value, InputSerializeError> =
223            Ok(input.serialize(serde_json::value::Serializer));
224    }
225
226    #[test]
227    fn rejects_null_with_clean_error() {
228        let error = serde_json::from_str::<Input>("null").unwrap_err();
229        assert!(
230            error
231                .to_string()
232                .starts_with("expected boolean, integer, float, string, list, or map, found null")
233        );
234    }
235
236    #[test]
237    fn rejects_invalid_type_at_path() {
238        let error = serde_json::from_str::<Input>(r#"{"foo": null}"#).unwrap_err();
239        assert!(error.to_string().starts_with(
240            "[foo]: expected boolean, integer, float, string, list, or map, found null"
241        ));
242    }
243
244    #[test]
245    fn rejects_out_of_range_integer_at_path() {
246        let value = if cfg!(target_pointer_width = "64") {
247            json!(i64::MAX as i128 + 1)
248        } else {
249            json!(i64::MAX)
250        };
251        let json = format!(r#"{{"count": {value}}}"#);
252        let error = serde_json::from_str::<Input>(&json).unwrap_err();
253        assert!(error.to_string().starts_with(&format!(
254            "[count]: integer {value} is out of range for isize"
255        )));
256    }
257
258    #[test]
259    fn roundtrip() {
260        let input = Input::from(HashMap::from([
261            ("name".to_string(), Input::from("plugx")),
262            ("enabled".to_string(), Input::from(true)),
263            ("count".to_string(), Input::from(3)),
264            ("ratio".to_string(), Input::from(0.5)),
265        ]));
266        let json = serde_json::to_string(&input).unwrap();
267        let decoded: Input = serde_json::from_str(&json).unwrap();
268        assert_eq!(input, decoded);
269    }
270
271    #[test]
272    fn deep_json_roundtrip() {
273        let json = r#"{
274            "app": {
275                "server": {
276                    "host": "localhost",
277                    "port": 8080,
278                    "routes": [
279                        {
280                            "path": "/api",
281                            "handler": {
282                                "type": "proxy",
283                                "target": {
284                                    "url": "http://example.com",
285                                    "headers": ["accept", "authorization"]
286                                }
287                            }
288                        }
289                    ]
290                },
291                "logging": {
292                    "level": "debug",
293                    "filters": {
294                        "modules": {
295                            "plugx": {
296                                "input": {
297                                    "serde": true
298                                }
299                            }
300                        }
301                    }
302                }
303            }
304        }"#;
305
306        let decoded: Input = serde_json::from_str(json).unwrap();
307        let encoded = serde_json::to_string(&decoded).unwrap();
308        let roundtripped: Input = serde_json::from_str(&encoded).unwrap();
309        assert_eq!(decoded, roundtripped);
310
311        assert_eq!(
312            decoded
313                .as_map()
314                .unwrap()
315                .get("app")
316                .unwrap()
317                .as_map()
318                .unwrap()
319                .get("logging")
320                .unwrap()
321                .as_map()
322                .unwrap()
323                .get("filters")
324                .unwrap()
325                .as_map()
326                .unwrap()
327                .get("modules")
328                .unwrap()
329                .as_map()
330                .unwrap()
331                .get("plugx")
332                .unwrap()
333                .as_map()
334                .unwrap()
335                .get("input")
336                .unwrap()
337                .as_map()
338                .unwrap()
339                .get("serde")
340                .unwrap(),
341            &Input::Bool(true)
342        );
343    }
344
345    #[test]
346    fn rejects_invalid_type_at_deep_path() {
347        let error = serde_json::from_str::<Input>(
348            r#"{"app": {"logging": {"filters": {"modules": {"plugx": null}}}}}"#,
349        )
350        .unwrap_err();
351        assert!(
352            error.to_string().starts_with(
353                "[app][logging][filters][modules][plugx]: expected boolean, integer, float, string, list, or map, found null"
354            )
355        );
356    }
357}