Skip to content

Commit e53322a

Browse files
committed
fix: Handle empty string as None
1 parent 4132edb commit e53322a

File tree

5 files changed

+72
-46
lines changed

5 files changed

+72
-46
lines changed

src/primitives/consumption_request.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::primitives::de_ext::deserialize_optional_uuid;
12
use crate::primitives::account_tenure::AccountTenure;
23
use crate::primitives::consumption_status::ConsumptionStatus;
34
use crate::primitives::delivery_status::DeliveryStatus;
@@ -44,7 +45,10 @@ pub struct ConsumptionRequest {
4445
/// The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction.
4546
///
4647
/// [appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken)
47-
#[serde(skip_serializing_if = "Option::is_none")]
48+
#[serde(
49+
skip_serializing_if = "Option::is_none",
50+
deserialize_with = "deserialize_optional_uuid"
51+
)]
4852
pub app_account_token: Option<Uuid>,
4953

5054
/// The age of the customer’s account.

src/primitives/de_ext.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use serde::{Deserialize, Deserializer};
2+
use uuid::Uuid;
3+
4+
/// Custom deserializer for optional UUID that treats empty strings as None.
5+
pub fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
6+
where
7+
D: Deserializer<'de>,
8+
{
9+
let s: Option<String> = Option::deserialize(deserializer)?;
10+
match s {
11+
None => Ok(None),
12+
Some(ref s) if s.is_empty() => Ok(None),
13+
Some(s) => s.parse::<Uuid>().map(Some).map_err(serde::de::Error::custom),
14+
}
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::*;
20+
use serde::Deserialize;
21+
use serde_json::json;
22+
23+
#[derive(Debug, Deserialize, PartialEq)]
24+
struct TestStruct {
25+
#[serde(deserialize_with = "deserialize_optional_uuid")]
26+
id: Option<Uuid>,
27+
}
28+
29+
#[test]
30+
fn test_deserialize_null_uuid() {
31+
let json = json!({"id": null});
32+
let result: TestStruct = serde_json::from_value(json).unwrap();
33+
assert_eq!(result.id, None);
34+
}
35+
36+
#[test]
37+
fn test_deserialize_empty_string_uuid() {
38+
let json = json!({"id": ""});
39+
let result: TestStruct = serde_json::from_value(json).unwrap();
40+
assert_eq!(result.id, None);
41+
}
42+
43+
#[test]
44+
fn test_deserialize_valid_uuid() {
45+
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
46+
let json = json!({"id": uuid_str});
47+
let result: TestStruct = serde_json::from_value(json).unwrap();
48+
assert_eq!(result.id, Some(Uuid::parse_str(uuid_str).unwrap()));
49+
}
50+
51+
#[test]
52+
fn test_deserialize_invalid_uuid() {
53+
let json = json!({"id": "not-a-valid-uuid"});
54+
let result: Result<TestStruct, _> = serde_json::from_value(json);
55+
assert!(result.is_err());
56+
57+
// Verify the error message contains something about UUID parsing
58+
let err = result.unwrap_err();
59+
let err_msg = err.to_string().to_lowercase();
60+
assert!(err_msg.contains("uuid") || err_msg.contains("invalid"));
61+
}
62+
}

src/primitives/jws_renewal_info_decoded_payload.rs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,92 +16,79 @@ use crate::primitives::advanced_commerce_renewal_info::AdvancedCommerceRenewalIn
1616
/// [JWSRenewalInfoDecodedPayload](https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload)
1717
#[serde_with::serde_as]
1818
#[derive(Debug, Clone, Deserialize, Serialize, Hash)]
19+
#[serde(rename_all = "camelCase")]
1920
pub struct JWSRenewalInfoDecodedPayload {
2021
/// The reason the subscription expired.
2122
///
2223
/// [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)
23-
#[serde(rename = "expirationIntent")]
2424
pub expiration_intent: Option<ExpirationIntent>,
2525

2626
/// The original transaction identifier of a purchase.
2727
///
2828
/// [originalTransactionId](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid)
29-
#[serde(rename = "originalTransactionId")]
3029
pub original_transaction_id: Option<String>,
3130

3231
/// The product identifier of the product that will renew at the next billing period.
3332
///
3433
/// [autoRenewProductId](https://developer.apple.com/documentation/appstoreserverapi/autorenewproductid)
35-
#[serde(rename = "autoRenewProductId")]
3634
pub auto_renew_product_id: Option<String>,
3735

3836
/// The unique identifier for the product, that you create in App Store Connect.
3937
///
4038
/// [productId](https://developer.apple.com/documentation/appstoreserverapi/productid)
41-
#[serde(rename = "productId")]
4239
pub product_id: Option<String>,
4340

4441
/// The renewal status of the auto-renewable subscription.
4542
///
4643
/// [autoRenewStatus](https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus)
47-
#[serde(rename = "autoRenewStatus")]
4844
pub auto_renew_status: Option<AutoRenewStatus>,
4945

5046
/// A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription.
5147
///
5248
/// [isInBillingRetryPeriod](https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod)
53-
#[serde(rename = "isInBillingRetryPeriod")]
5449
pub is_in_billing_retry_period: Option<bool>,
5550

5651
/// The status that indicates whether the auto-renewable subscription is subject to a price increase.
5752
///
5853
/// [priceIncreaseStatus](https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus)
59-
#[serde(rename = "priceIncreaseStatus")]
6054
pub price_increase_status: Option<PriceIncreaseStatus>,
6155

6256
/// The time when the billing grace period for subscription renewals expires.
6357
///
6458
/// [gracePeriodExpiresDate](https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate)
65-
#[serde(rename = "gracePeriodExpiresDate")]
6659
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
6760
pub grace_period_expires_date: Option<DateTime<Utc>>,
6861

6962
/// The type of subscription offer.
7063
///
7164
/// [offerType](https://developer.apple.com/documentation/appstoreserverapi/offertype)
72-
#[serde(rename = "offerType")]
7365
pub offer_type: Option<OfferType>,
7466

7567
/// The offer code or the promotional offer identifier.
7668
///
7769
/// [offerIdentifier](https://developer.apple.com/documentation/appstoreserverapi/offeridentifier)
78-
#[serde(rename = "offerIdentifier")]
7970
pub offer_identifier: Option<String>,
8071

8172
/// The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data.
8273
///
8374
/// [signedDate](https://developer.apple.com/documentation/appstoreserverapi/signeddate)
84-
#[serde(rename = "signedDate")]
8575
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
8676
pub signed_date: Option<DateTime<Utc>>,
8777

8878
/// The server environment, either sandbox or production.
8979
///
9080
/// [environment](https://developer.apple.com/documentation/appstoreserverapi/environment)
91-
#[serde(rename = "environment")]
9281
pub environment: Option<Environment>,
9382

9483
/// The earliest start date of a subscription in a series of auto-renewable subscription purchases that ignores all lapses of paid service shorter than 60 days.
9584
///
9685
/// [recentSubscriptionStartDate](https://developer.apple.com/documentation/appstoreserverapi/recentsubscriptionstartdate)
97-
#[serde(rename = "recentSubscriptionStartDate")]
9886
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
9987
pub recent_subscription_start_date: Option<DateTime<Utc>>,
10088

10189
/// The UNIX time, in milliseconds, when the most recent auto-renewable subscription purchase expires.
10290
///
10391
/// [renewalDate](https://developer.apple.com/documentation/appstoreserverapi/renewaldate)
104-
#[serde(rename = "renewalDate")]
10592
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
10693
pub renewal_date: Option<DateTime<Utc>>,
10794

@@ -113,37 +100,32 @@ pub struct JWSRenewalInfoDecodedPayload {
113100
///The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period.
114101
///
115102
///[renewalPrice](https://developer.apple.com/documentation/appstoreserverapi/renewalprice)
116-
#[serde(rename = "renewalPrice")]
117103
pub renewal_price: Option<i64>,
118104

119105
///The payment mode you configure for the offer.
120106
///
121107
///[offerDiscountType](https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype)
122-
#[serde(rename = "offerDiscountType")]
123108
pub offer_discount_type: Option<OfferDiscountType>,
124109

125110
///An array of win-back offer identifiers that a customer is eligible to redeem, which sorts the identifiers to present the better offers first.
126111
///
127112
///[eligibleWinBackOfferIds](https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids)
128-
#[serde(rename = "eligibleWinBackOfferIds")]
129113
pub eligible_win_back_offer_ids: Option<Vec<String>>,
130114

131115
/// The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction.
132116
///
133117
/// [appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appAccountToken)
134-
#[serde(rename = "appAccountToken")]
118+
#[serde(skip_serializing_if = "Option::is_none")]
135119
pub app_account_token: Option<Uuid>,
136120

137121
/// The unique identifier of the app download transaction.
138122
///
139123
/// [appTransactionId](https://developer.apple.com/documentation/appstoreserverapi/appTransactionId)
140-
#[serde(rename = "appTransactionId")]
141124
pub app_transaction_id: Option<String>,
142125

143126
/// The duration of the offer.
144127
///
145128
/// [offerPeriod](https://developer.apple.com/documentation/appstoreserverapi/offerPeriod)
146-
#[serde(rename = "offerPeriod")]
147129
pub offer_period: Option<String>,
148130

149131
/// Renewal information that is present only for Advanced Commerce SKUs.

src/primitives/jws_transaction_decoded_payload.rs

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,124 +16,107 @@ use crate::primitives::advanced_commerce_transaction_info::AdvancedCommerceTrans
1616
/// [JWSTransactionDecodedPayload](https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload)
1717
#[serde_with::serde_as]
1818
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Hash)]
19+
#[serde(rename_all = "camelCase")]
1920
pub struct JWSTransactionDecodedPayload {
2021
/// The original transaction identifier of a purchase.
2122
///
2223
/// [originalTransactionId](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid)
23-
#[serde(rename = "originalTransactionId")]
2424
pub original_transaction_id: Option<String>,
2525

2626
/// The unique identifier for a transaction such as an in-app purchase, restored in-app purchase, or subscription renewal.
2727
///
2828
/// [transactionId](https://developer.apple.com/documentation/appstoreserverapi/transactionid)
29-
#[serde(rename = "transactionId")]
3029
pub transaction_id: Option<String>,
3130

3231
/// The unique identifier of subscription-purchase events across devices, including renewals.
3332
///
3433
/// [webOrderLineItemId](https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid)
35-
#[serde(rename = "webOrderLineItemId")]
3634
pub web_order_line_item_id: Option<String>,
3735

3836
/// The bundle identifier of an app.
3937
///
4038
/// [bundle_id](https://developer.apple.com/documentation/appstoreserverapi/bundleid)
41-
#[serde(rename = "bundleId")]
4239
pub bundle_id: Option<String>,
4340

4441
/// The unique identifier for the product, that you create in App Store Connect.
4542
///
4643
/// [productId](https://developer.apple.com/documentation/appstoreserverapi/productid)
47-
#[serde(rename = "productId")]
4844
pub product_id: Option<String>,
4945

5046
/// The identifier of the subscription group that the subscription belongs to.
5147
///
5248
/// [subscriptionGroupIdentifier](https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier)
53-
#[serde(rename = "subscriptionGroupIdentifier")]
5449
pub subscription_group_identifier: Option<String>,
5550

5651
/// The time that the App Store charged the user’s account for an in-app purchase, a restored in-app purchase, a subscription, or a subscription renewal after a lapse.
5752
///
5853
/// [purchaseDate](https://developer.apple.com/documentation/appstoreserverapi/purchasedate)
59-
#[serde(rename = "purchaseDate")]
6054
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
6155
pub purchase_date: Option<DateTime<Utc>>,
6256

6357
/// The purchase date of the transaction associated with the original transaction identifier.
6458
///
6559
/// [originalPurchaseDate](https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate)
66-
#[serde(rename = "originalPurchaseDate")]
6760
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
6861
pub original_purchase_date: Option<DateTime<Utc>>,
6962

7063
/// The UNIX time, in milliseconds, an auto-renewable subscription expires or renews.
7164
///
7265
/// [expiresDate](https://developer.apple.com/documentation/appstoreserverapi/expiresdate)
73-
#[serde(rename = "expiresDate")]
7466
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
7567
pub expires_date: Option<DateTime<Utc>>,
7668

7769
/// The number of consumable products purchased.
7870
///
7971
/// [quantity](https://developer.apple.com/documentation/appstoreserverapi/quantity)
80-
#[serde(rename = "quantity")]
8172
pub quantity: Option<i32>,
8273

8374
/// The type of the in-app purchase.
8475
///
8576
/// [type](https://developer.apple.com/documentation/appstoreserverapi/type)
86-
#[serde(rename = "type")]
8777
pub r#type: Option<ProductType>,
8878

8979
/// The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction.
9080
///
9181
/// [appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken)
92-
#[serde(rename = "appAccountToken")]
82+
#[serde(skip_serializing_if = "Option::is_none")]
9383
pub app_account_token: Option<Uuid>,
9484

9585
/// A string that describes whether the transaction was purchased by the user, or is available to them through Family Sharing.
9686
///
9787
/// [inAppOwnershipType](https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype)
98-
#[serde(rename = "inAppOwnershipType")]
9988
pub in_app_ownership_type: Option<InAppOwnershipType>,
10089

10190
/// The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data.
10291
///
10392
/// [signedDate](https://developer.apple.com/documentation/appstoreserverapi/signeddate)
104-
#[serde(rename = "signedDate")]
10593
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
10694
pub signed_date: Option<DateTime<Utc>>,
10795

10896
/// The reason that the App Store refunded the transaction or revoked it from Family Sharing.
10997
///
11098
/// [revocationReason](https://developer.apple.com/documentation/appstoreserverapi/revocationreason)
111-
#[serde(rename = "revocationReason")]
11299
pub revocation_reason: Option<RevocationReason>,
113100

114101
/// The UNIX time, in milliseconds, that Apple Support refunded a transaction.
115102
///
116103
/// [revocationDate](https://developer.apple.com/documentation/appstoreserverapi/revocationdate)
117-
#[serde(rename = "revocationDate")]
118104
#[serde_as(as = "Option<TimestampMilliSeconds<String, Flexible>>")]
119105
pub revocation_date: Option<DateTime<Utc>>,
120106

121107
/// The Boolean value that indicates whether the user upgraded to another subscription.
122108
///
123109
/// [isUpgraded](https://developer.apple.com/documentation/appstoreserverapi/isupgraded)
124-
#[serde(rename = "isUpgraded")]
125110
pub is_upgraded: Option<bool>,
126111

127112
/// A value that represents the promotional offer type.
128113
///
129114
/// [offerType](https://developer.apple.com/documentation/appstoreserverapi/offertype)
130-
#[serde(rename = "offerType")]
131115
pub offer_type: Option<OfferType>,
132116

133117
/// The identifier that contains the offer code or the promotional offer identifier.
134118
///
135119
/// [offerIdentifier](https://developer.apple.com/documentation/appstoreserverapi/offeridentifier)
136-
#[serde(rename = "offerIdentifier")]
137120
pub offer_identifier: Option<String>,
138121

139122
/// The server environment, either sandbox or production.
@@ -149,13 +132,11 @@ pub struct JWSTransactionDecodedPayload {
149132
/// An Apple-defined value that uniquely identifies the App Store storefront associated with the purchase.
150133
///
151134
/// [storefrontId](https://developer.apple.com/documentation/appstoreserverapi/storefrontid)
152-
#[serde(rename = "storefrontId")]
153135
pub storefront_id: Option<String>,
154136

155137
/// The reason for the purchase transaction, which indicates whether it’s a customer’s purchase or a renewal for an auto-renewable subscription that the system initiates.
156138
///
157139
/// [transactionReason](https://developer.apple.com/documentation/appstoreserverapi/transactionreason)
158-
#[serde(rename = "transactionReason")]
159140
pub transaction_reason: Option<TransactionReason>,
160141

161142
/// The three-letter ISO 4217 currency code for the price of the product.
@@ -171,24 +152,20 @@ pub struct JWSTransactionDecodedPayload {
171152
/// The payment mode you configure for the offer.
172153
///
173154
/// [offerDiscountType](https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype)
174-
#[serde(rename = "offerDiscountType")]
175155
pub offer_discount_type: Option<OfferDiscountType>,
176156

177157
/// The unique identifier of the app download transaction.
178158
///
179159
/// [appTransactionId](https://developer.apple.com/documentation/appstoreserverapi/appTransactionId)
180-
#[serde(rename = "appTransactionId")]
181160
pub app_transaction_id: Option<String>,
182161

183162
/// The duration of the offer.
184163
///
185164
/// [offerPeriod](https://developer.apple.com/documentation/appstoreserverapi/offerPeriod)
186-
#[serde(rename = "offerPeriod")]
187165
pub offer_period: Option<String>,
188166

189167
/// Transaction information that is present only for Advanced Commerce SKUs.
190168
///
191169
/// [advancedCommerceTransactionInfo](https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactioninfo)
192-
#[serde(rename = "advancedCommerceInfo")]
193170
pub advanced_commerce_info: Option<AdvancedCommerceTransactionInfo>,
194171
}

src/primitives/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +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;

0 commit comments

Comments
 (0)