Skip to main content

stefans_utils/
php_safe_indexmap.rs

1use indexmap::IndexMap;
2use serde::{
3    Deserialize,
4    de::{self, Deserializer, MapAccess, Visitor},
5};
6use std::{fmt::Debug, hash::Hash, marker::PhantomData};
7
8pub struct PhpSafeIndexMap<K, V>(PhantomData<(K, V)>);
9
10impl<'de, K: Hash + Eq + Debug + Deserialize<'de>, V: Deserialize<'de>> PhpSafeIndexMap<K, V> {
11    pub fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<IndexMap<K, V>, D::Error> {
12        deserializer.deserialize_any(PhpSafeIndexMap::default())
13    }
14}
15
16impl<K, V> Default for PhpSafeIndexMap<K, V> {
17    fn default() -> Self {
18        Self(PhantomData)
19    }
20}
21
22impl<'de, K: Hash + Eq + Debug, V> Visitor<'de> for PhpSafeIndexMap<K, V>
23where
24    K: Deserialize<'de>,
25    V: Deserialize<'de>,
26{
27    type Value = IndexMap<K, V>;
28
29    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
30        formatter.write_str("A IndexMap<K, V> or a sequence of key-value tuples like Vec<(K, V)> where each key must be unique")
31    }
32
33    fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
34    where
35        M: MapAccess<'de>,
36    {
37        let mut map = IndexMap::with_capacity(access.size_hint().unwrap_or(0));
38
39        while let Some((key, value)) = access.next_entry()? {
40            map.insert(key, value);
41        }
42
43        Ok(map)
44    }
45
46    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
47    where
48        A: serde::de::SeqAccess<'de>,
49    {
50        let mut map = IndexMap::with_capacity(seq.size_hint().unwrap_or(0));
51
52        while let Some((k, v)) = seq.next_element::<(K, V)>()? {
53            if map.contains_key(&k) {
54                return Err(de::Error::custom(format_args!("duplicate field `{k:?}`")));
55            }
56
57            map.insert(k, v);
58        }
59
60        Ok(map)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use std::fmt::Display;
67
68    use super::*;
69    use serde::Serialize;
70    use serde_json::json;
71
72    #[derive(Debug, Serialize, Deserialize, PartialEq)]
73    #[serde(untagged)]
74    enum StringOrUsize {
75        String(String),
76        Usize(usize),
77    }
78
79    impl From<usize> for StringOrUsize {
80        fn from(value: usize) -> Self {
81            Self::Usize(value)
82        }
83    }
84
85    impl From<&'static str> for StringOrUsize {
86        fn from(value: &'static str) -> Self {
87            Self::String(value.to_string())
88        }
89    }
90
91    #[derive(Debug, Serialize, Deserialize, PartialEq)]
92    struct MyPhpSafeMap(
93        #[serde(deserialize_with = "PhpSafeIndexMap::deserialize")] IndexMap<String, StringOrUsize>,
94    );
95
96    impl MyPhpSafeMap {
97        fn from(key_values: impl IntoIterator<Item = (impl Display, StringOrUsize)>) -> Self {
98            let mut map = IndexMap::new();
99
100            for (key, value) in key_values {
101                map.insert(key.to_string(), value);
102            }
103
104            Self(map)
105        }
106    }
107
108    #[test]
109    fn should_work_with_an_actual_map() {
110        let json = json! (
111            {
112                "foo": 123,
113                "bar": "baz"
114            }
115        );
116
117        let map: MyPhpSafeMap = serde_json::from_value(json).unwrap();
118
119        assert_eq!(
120            map,
121            MyPhpSafeMap::from([("foo", 123.into()), ("bar", "baz".into())])
122        )
123    }
124
125    #[test]
126    fn should_work_with_an_associated_array() {
127        let json = json!([["foo", 123], ["bar", "baz"]]);
128        let map: MyPhpSafeMap = serde_json::from_value(json).unwrap();
129
130        assert_eq!(
131            map,
132            MyPhpSafeMap::from([("foo", 123.into()), ("bar", "baz".into())])
133        )
134    }
135
136    #[test]
137    fn should_error_on_duplicate_fields() {
138        let json = json!([["foo", 123], ["foo", "baz"]]);
139        let res: Result<MyPhpSafeMap, _> = serde_json::from_value(json);
140
141        assert!(res.is_err())
142    }
143}