Skip to content

Commit 76e4f85

Browse files
oguzkocerjkmassel
authored andcommitted
Add deserialize_empty_array_or_hashmap helper function
Provides a field-level serde helper to handle WordPress's inconsistent JSON responses where empty objects are sometimes returned as empty arrays. Changes: - Add `deserialize_empty_array_or_hashmap` function in `wp_serde_helper` - Use dedicated visitor pattern to avoid 'static lifetime constraints - Add comprehensive unit tests for the new helper - Simplify `WpApiDetailsAuthenticationMap` to use field annotation instead of custom Deserialize impl
1 parent b8cc918 commit 76e4f85

File tree

2 files changed

+82
-17
lines changed

2 files changed

+82
-17
lines changed

wp_api/src/login.rs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use serde::{Deserialize, Serialize};
55
use std::{collections::HashMap, str, sync::Arc};
66
use wp_localization::{MessageBundle, WpMessages, WpSupportsLocalization};
77
use wp_localization_macro::WpDeriveLocalizable;
8-
use wp_serde_helper::{deserialize_false_or_string, deserialize_offset};
8+
use wp_serde_helper::{
9+
deserialize_empty_array_or_hashmap, deserialize_false_or_string, deserialize_offset,
10+
};
911

1012
const KEY_APPLICATION_PASSWORDS: &str = "application-passwords";
1113

@@ -225,22 +227,11 @@ impl WpSupportsLocalization for OAuthResponseUrlError {
225227
}
226228
}
227229

228-
#[derive(Debug, Clone, Serialize, PartialEq)]
229-
pub struct WpApiDetailsAuthenticationMap(HashMap<String, WpRestApiAuthenticationScheme>);
230-
231-
// If the response is `[]`, default to an empty `HashMap`
232-
impl<'de> Deserialize<'de> for WpApiDetailsAuthenticationMap {
233-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
234-
where
235-
D: serde::Deserializer<'de>,
236-
{
237-
deserializer
238-
.deserialize_any(wp_serde_helper::DeserializeEmptyVecOrT::<
239-
HashMap<String, WpRestApiAuthenticationScheme>,
240-
>::new(Box::new(HashMap::new)))
241-
.map(Self)
242-
}
243-
}
230+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
231+
pub struct WpApiDetailsAuthenticationMap(
232+
#[serde(deserialize_with = "deserialize_empty_array_or_hashmap")]
233+
HashMap<String, WpRestApiAuthenticationScheme>,
234+
);
244235

245236
impl WpApiDetailsAuthenticationMap {
246237
pub fn has_application_passwords_authentication_url(&self) -> bool {

wp_serde_helper/src/lib.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,57 @@ where
253253
deserializer.deserialize_any(DeserializeFalseOrStringVisitor)
254254
}
255255

256+
struct DeserializeEmptyArrayOrHashMapVisitor<K, V>(PhantomData<(K, V)>);
257+
258+
impl<'de, K, V> de::Visitor<'de> for DeserializeEmptyArrayOrHashMapVisitor<K, V>
259+
where
260+
K: DeserializeOwned + std::hash::Hash + Eq,
261+
V: DeserializeOwned,
262+
{
263+
type Value = std::collections::HashMap<K, V>;
264+
265+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
266+
formatter.write_str("empty array or a HashMap")
267+
}
268+
269+
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
270+
where
271+
A: de::SeqAccess<'de>,
272+
{
273+
if seq.next_element::<Self::Value>()?.is_none() {
274+
// It's an empty array
275+
Ok(std::collections::HashMap::new())
276+
} else {
277+
// not an empty array
278+
Err(serde::de::Error::invalid_type(Unexpected::Seq, &self))
279+
}
280+
}
281+
282+
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
283+
where
284+
A: de::MapAccess<'de>,
285+
{
286+
std::collections::HashMap::deserialize(de::value::MapAccessDeserializer::new(map))
287+
}
288+
}
289+
290+
pub fn deserialize_empty_array_or_hashmap<'de, D, K, V>(
291+
deserializer: D,
292+
) -> Result<std::collections::HashMap<K, V>, D::Error>
293+
where
294+
D: Deserializer<'de>,
295+
K: DeserializeOwned + std::hash::Hash + Eq,
296+
V: DeserializeOwned,
297+
{
298+
deserializer.deserialize_any(DeserializeEmptyArrayOrHashMapVisitor::<K, V>(PhantomData))
299+
}
300+
256301
#[cfg(test)]
257302
mod tests {
258303
use super::*;
259304
use rstest::*;
260305
use serde::Deserialize;
306+
use std::collections::HashMap;
261307

262308
#[derive(Debug, Deserialize)]
263309
pub struct Foo {
@@ -314,4 +360,32 @@ mod tests {
314360
serde_json::from_str(test_case).expect("Test case should be a valid JSON");
315361
assert_eq!(expected_result, string_or_bool.value);
316362
}
363+
364+
#[derive(Debug, Deserialize)]
365+
pub struct HashMapWrapper {
366+
#[serde(deserialize_with = "deserialize_empty_array_or_hashmap")]
367+
pub map: HashMap<String, String>,
368+
}
369+
370+
#[rstest]
371+
#[case(r#"{"map": []}"#, HashMap::new())]
372+
#[case(r#"{"map": {"key": "value"}}"#, {
373+
let mut map = HashMap::new();
374+
map.insert("key".to_string(), "value".to_string());
375+
map
376+
})]
377+
#[case(r#"{"map": {"foo": "bar", "hello": "world"}}"#, {
378+
let mut map = HashMap::new();
379+
map.insert("foo".to_string(), "bar".to_string());
380+
map.insert("hello".to_string(), "world".to_string());
381+
map
382+
})]
383+
fn test_deserialize_empty_array_or_hashmap(
384+
#[case] test_case: &str,
385+
#[case] expected_result: HashMap<String, String>,
386+
) {
387+
let wrapper: HashMapWrapper =
388+
serde_json::from_str(test_case).expect("Test case should be a valid JSON");
389+
assert_eq!(expected_result, wrapper.map);
390+
}
317391
}

0 commit comments

Comments
 (0)