Skip to content

Commit 08a8185

Browse files
authored
fix: Serialize UUID as empty string if it is None (#97)
1 parent 1e0362d commit 08a8185

File tree

5 files changed

+123
-73
lines changed

5 files changed

+123
-73
lines changed

src/primitives/consumption_request.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::primitives::de_ext::deserialize_optional_uuid;
1+
use crate::primitives::serde_ext::{de_string_as_optional_uuid, ser_optional_uuid_as_string};
22
use crate::primitives::account_tenure::AccountTenure;
33
use crate::primitives::consumption_status::ConsumptionStatus;
44
use crate::primitives::delivery_status::DeliveryStatus;
@@ -46,8 +46,8 @@ pub struct ConsumptionRequest {
4646
///
4747
/// [appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken)
4848
#[serde(
49-
skip_serializing_if = "Option::is_none",
50-
deserialize_with = "deserialize_optional_uuid"
49+
deserialize_with = "de_string_as_optional_uuid",
50+
serialize_with = "ser_optional_uuid_as_string"
5151
)]
5252
pub app_account_token: Option<Uuid>,
5353

src/primitives/de_ext.rs

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/primitives/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@ pub mod advanced_commerce_transaction_info;
6363
pub mod advanced_commerce_renewal_info;
6464
pub mod advanced_commerce_renewal_item;
6565
pub mod retention_messaging;
66-
mod de_ext;
66+
mod serde_ext;

src/primitives/serde_ext.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use serde::{Deserialize, Deserializer, Serializer};
2+
use serde::de::Unexpected;
3+
use uuid::Uuid;
4+
5+
/// Custom deserializer for optional UUID that treats empty strings as None.
6+
pub fn de_string_as_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
7+
where
8+
D: Deserializer<'de>,
9+
{
10+
let s: Option<String> = Option::deserialize(deserializer)?;
11+
match s {
12+
None => Ok(None),
13+
Some(ref s) if s.is_empty() => Ok(None),
14+
Some(s) => s.parse::<Uuid>()
15+
.map(Some)
16+
.map_err(|_e| serde::de::Error::invalid_type(Unexpected::Str(&s), &"Valid Uuid")),
17+
}
18+
}
19+
20+
/// Custom serializer for optional UUID that serializes None as an empty string.
21+
pub fn ser_optional_uuid_as_string<S>(
22+
value: &Option<Uuid>,
23+
serializer: S,
24+
) -> Result<S::Ok, S::Error>
25+
where
26+
S: Serializer,
27+
{
28+
match value {
29+
Some(uuid) => serializer.serialize_str(&uuid.to_string()),
30+
None => serializer.serialize_str(""),
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod tests {
36+
use super::*;
37+
use serde::{Deserialize, Serialize};
38+
use serde_json::json;
39+
40+
#[derive(Debug, Serialize, Deserialize, PartialEq)]
41+
struct TestStruct {
42+
#[serde(
43+
deserialize_with = "de_string_as_optional_uuid",
44+
serialize_with = "ser_optional_uuid_as_string"
45+
)]
46+
id: Option<Uuid>,
47+
}
48+
49+
#[test]
50+
fn test_deserialize_null_uuid() {
51+
let json = json!({"id": null});
52+
let result: TestStruct = serde_json::from_value(json).unwrap();
53+
assert_eq!(result.id, None);
54+
}
55+
56+
#[test]
57+
fn test_deserialize_empty_string_uuid() {
58+
let json = json!({"id": ""});
59+
let result: TestStruct = serde_json::from_value(json).unwrap();
60+
assert_eq!(result.id, None);
61+
}
62+
63+
#[test]
64+
fn test_deserialize_valid_uuid() {
65+
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
66+
let json = json!({"id": uuid_str});
67+
let result: TestStruct = serde_json::from_value(json).unwrap();
68+
assert_eq!(result.id, Some(Uuid::parse_str(uuid_str).unwrap()));
69+
}
70+
71+
#[test]
72+
fn test_deserialize_invalid_uuid() {
73+
let json = json!({"id": "not-a-valid-uuid"});
74+
let result: Result<TestStruct, _> = serde_json::from_value(json);
75+
assert!(result.is_err());
76+
77+
// Verify the error message contains something about UUID parsing
78+
let err = result.unwrap_err();
79+
let err_msg = err.to_string().to_lowercase();
80+
assert!(err_msg.contains("uuid") || err_msg.contains("invalid"));
81+
}
82+
83+
#[test]
84+
fn test_serialize_none_as_empty_string() {
85+
let test_struct = TestStruct { id: None };
86+
let json = serde_json::to_value(&test_struct).unwrap();
87+
assert_eq!(json, json!({"id": ""}));
88+
}
89+
90+
#[test]
91+
fn test_serialize_some_uuid() {
92+
let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
93+
let test_struct = TestStruct { id: Some(uuid) };
94+
let json = serde_json::to_value(&test_struct).unwrap();
95+
assert_eq!(json, json!({"id": "550e8400-e29b-41d4-a716-446655440000"}));
96+
}
97+
98+
#[test]
99+
fn test_roundtrip_none() {
100+
let original = TestStruct { id: None };
101+
let json = serde_json::to_value(&original).unwrap();
102+
let deserialized: TestStruct = serde_json::from_value(json).unwrap();
103+
assert_eq!(original, deserialized);
104+
}
105+
106+
#[test]
107+
fn test_roundtrip_some() {
108+
let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
109+
let original = TestStruct { id: Some(uuid) };
110+
let json = serde_json::to_value(&original).unwrap();
111+
let deserialized: TestStruct = serde_json::from_value(json).unwrap();
112+
assert_eq!(original, deserialized);
113+
}
114+
}

tests/ass_api_client.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,10 +1229,11 @@ async fn test_send_consumption_data_with_null_app_account_token() {
12291229
.as_i64()
12301230
.unwrap()
12311231
);
1232-
// When app_account_token is None, it should not be included in the JSON at all
1233-
assert!(
1234-
!decoded_json.contains_key("appAccountToken"),
1235-
"appAccountToken field should be omitted when None"
1232+
assert_eq!(
1233+
"",
1234+
decoded_json["appAccountToken"]
1235+
.as_str()
1236+
.unwrap()
12361237
);
12371238
assert_eq!(
12381239
4,

0 commit comments

Comments
 (0)