stefans_utils/
php_safe_indexmap.rs1use 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}