app_store_server_library/models/
helper_validation_utils.rs1use std::fmt;
2
3use uuid::Uuid;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ValidationError {
8 InvalidCurrencyLength(usize),
9 InvalidCurrencyFormat(String),
10 EmptyTaxCode,
11 EmptyTransactionId,
12 EmptyTargetProductId,
13 UuidTooLong(usize),
14 NegativePrice(i64),
15 DescriptionTooLong(usize),
16 DisplayNameTooLong(usize),
17 SkuTooLong(usize),
18 InvalidPeriodCount(i32),
19 EmptyItems,
20}
21
22impl fmt::Display for ValidationError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 ValidationError::InvalidCurrencyLength(len) => {
26 write!(
27 f,
28 "Currency must be a 3-letter ISO 4217 code, got {} characters",
29 len
30 )
31 }
32 ValidationError::InvalidCurrencyFormat(currency) => {
33 write!(
34 f,
35 "Currency must contain only uppercase letters: {}",
36 currency
37 )
38 }
39 ValidationError::EmptyTaxCode => write!(f, "Tax code cannot be empty"),
40 ValidationError::EmptyTransactionId => write!(f, "Transaction ID cannot be empty"),
41 ValidationError::EmptyTargetProductId => write!(f, "Target Product ID cannot be empty"),
42 ValidationError::UuidTooLong(len) => {
43 write!(
44 f,
45 "UUID string representation cannot exceed {} characters, got {}",
46 MAXIMUM_REQUEST_REFERENCE_ID_LENGTH, len
47 )
48 }
49 ValidationError::NegativePrice(price) => {
50 write!(f, "Price cannot be negative: {}", price)
51 }
52 ValidationError::DescriptionTooLong(len) => {
53 write!(
54 f,
55 "Description length ({}) exceeds maximum allowed ({})",
56 len, MAXIMUM_DESCRIPTION_LENGTH
57 )
58 }
59 ValidationError::DisplayNameTooLong(len) => {
60 write!(
61 f,
62 "Display name length ({}) exceeds maximum allowed ({})",
63 len, MAXIMUM_DISPLAY_NAME_LENGTH
64 )
65 }
66 ValidationError::SkuTooLong(len) => {
67 write!(
68 f,
69 "SKU length ({}) exceeds maximum allowed ({})",
70 len, MAXIMUM_SKU_LENGTH
71 )
72 }
73 ValidationError::InvalidPeriodCount(period_count) => {
74 write!(
75 f,
76 "AdvancedCommercePeriod count must be between 1 and {} inclusive, got {}",
77 MAXIMUM_PERIOD_COUNT, period_count
78 )
79 }
80 ValidationError::EmptyItems => write!(f, "Items list cannot be empty"),
81 }
82 }
83}
84
85impl std::error::Error for ValidationError {}
86
87pub const CURRENCY_CODE_LENGTH: usize = 3;
89pub const MAXIMUM_STOREFRONT_LENGTH: usize = 10;
90pub const MAXIMUM_REQUEST_REFERENCE_ID_LENGTH: usize = 36;
91pub const MAXIMUM_DESCRIPTION_LENGTH: usize = 45;
92pub const MAXIMUM_DISPLAY_NAME_LENGTH: usize = 30;
93const MAXIMUM_SKU_LENGTH: usize = 128;
94pub const MAXIMUM_PERIOD_COUNT: i32 = 12;
96
97pub fn validate_currency(currency: &str) -> Result<String, ValidationError> {
106 if currency.len() != CURRENCY_CODE_LENGTH {
107 return Err(ValidationError::InvalidCurrencyLength(currency.len()));
108 }
109
110 if !currency
111 .chars()
112 .all(|c| c.is_ascii_uppercase())
113 {
114 return Err(ValidationError::InvalidCurrencyFormat(currency.to_string()));
115 }
116
117 Ok(currency.to_string())
118}
119
120pub fn validate_tax_code(tax_code: &str) -> Result<String, ValidationError> {
129 if tax_code.trim().is_empty() {
130 return Err(ValidationError::EmptyTaxCode);
131 }
132 Ok(tax_code.to_string())
133}
134
135pub fn validate_transaction_id(transaction_id: &str) -> Result<String, ValidationError> {
144 if transaction_id.trim().is_empty() {
145 return Err(ValidationError::EmptyTransactionId);
146 }
147 Ok(transaction_id.to_string())
148}
149
150pub fn validate_target_product_id(target_product_id: &str) -> Result<String, ValidationError> {
159 if target_product_id.trim().is_empty() {
160 return Err(ValidationError::EmptyTargetProductId);
161 }
162 Ok(target_product_id.to_string())
163}
164
165pub fn validate_uuid(uuid: &Uuid) -> Result<Uuid, ValidationError> {
174 let uuid_string = uuid.to_string();
175 if uuid_string.len() > MAXIMUM_REQUEST_REFERENCE_ID_LENGTH {
176 return Err(ValidationError::UuidTooLong(uuid_string.len()));
177 }
178 Ok(*uuid)
179}
180
181pub fn validate_price(price: i64) -> Result<i64, ValidationError> {
190 if price < 0 {
191 return Err(ValidationError::NegativePrice(price));
192 }
193 Ok(price)
194}
195
196pub fn validate_description(description: &str) -> Result<String, ValidationError> {
205 let length = description.chars().count();
206 if length > MAXIMUM_DESCRIPTION_LENGTH {
207 return Err(ValidationError::DescriptionTooLong(length));
208 }
209 Ok(description.to_string())
210}
211
212pub fn validate_display_name(display_name: &str) -> Result<String, ValidationError> {
221 let length = display_name.chars().count();
222 if length > MAXIMUM_DISPLAY_NAME_LENGTH {
223 return Err(ValidationError::DisplayNameTooLong(length));
224 }
225 Ok(display_name.to_string())
226}
227
228pub fn validate_sku(sku: &str) -> Result<String, ValidationError> {
237 let length = sku.chars().count();
238 if length > MAXIMUM_SKU_LENGTH {
239 return Err(ValidationError::SkuTooLong(length));
240 }
241 Ok(sku.to_string())
242}
243
244pub fn validate_period_count(period_count: i32) -> Result<i32, ValidationError> {
253 if !(1..=MAXIMUM_PERIOD_COUNT).contains(&period_count) {
254 return Err(ValidationError::InvalidPeriodCount(period_count));
255 }
256 Ok(period_count)
257}
258
259pub fn validate_items<T>(items: Vec<T>) -> Result<Vec<T>, ValidationError> {
268 if items.is_empty() {
269 return Err(ValidationError::EmptyItems);
270 }
271 Ok(items)
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_validate_currency_valid() {
280 assert_eq!(validate_currency("USD").unwrap(), "USD");
281 assert_eq!(validate_currency("EUR").unwrap(), "EUR");
282 assert_eq!(validate_currency("GBP").unwrap(), "GBP");
283 }
284
285 #[test]
286 fn test_validate_currency_invalid_length() {
287 assert!(matches!(
288 validate_currency("US"),
289 Err(ValidationError::InvalidCurrencyLength(2))
290 ));
291 assert!(matches!(
292 validate_currency("USDD"),
293 Err(ValidationError::InvalidCurrencyLength(4))
294 ));
295 }
296
297 #[test]
298 fn test_validate_currency_invalid_format() {
299 assert!(matches!(
300 validate_currency("usd"),
301 Err(ValidationError::InvalidCurrencyFormat(_))
302 ));
303 assert!(matches!(
304 validate_currency("US1"),
305 Err(ValidationError::InvalidCurrencyFormat(_))
306 ));
307 }
308
309 #[test]
310 fn test_validate_price_valid() {
311 assert_eq!(validate_price(0).unwrap(), 0);
312 assert_eq!(validate_price(100).unwrap(), 100);
313 assert_eq!(validate_price(999999).unwrap(), 999999);
314 }
315
316 #[test]
317 fn test_validate_price_invalid() {
318 assert!(matches!(
319 validate_price(-1),
320 Err(ValidationError::NegativePrice(-1))
321 ));
322 assert!(matches!(
323 validate_price(-100),
324 Err(ValidationError::NegativePrice(-100))
325 ));
326 }
327
328 #[test]
329 fn test_validate_empty_strings() {
330 assert!(matches!(
331 validate_tax_code(""),
332 Err(ValidationError::EmptyTaxCode)
333 ));
334 assert!(matches!(
335 validate_tax_code(" "),
336 Err(ValidationError::EmptyTaxCode)
337 ));
338 assert!(validate_tax_code("ABC123").is_ok());
339 }
340
341 #[test]
342 fn test_validate_lengths() {
343 let long_description = "a".repeat(46);
344 assert!(matches!(
345 validate_description(&long_description),
346 Err(ValidationError::DescriptionTooLong(46))
347 ));
348
349 let ok_description = "a".repeat(45);
350 assert!(validate_description(&ok_description).is_ok());
351
352 let long_display_name = "a".repeat(31);
353 assert!(matches!(
354 validate_display_name(&long_display_name),
355 Err(ValidationError::DisplayNameTooLong(31))
356 ));
357
358 let ok_display_name = "a".repeat(30);
359 assert!(validate_display_name(&ok_display_name).is_ok());
360
361 let long_sku = "a".repeat(129);
362 assert!(matches!(
363 validate_sku(&long_sku),
364 Err(ValidationError::SkuTooLong(129))
365 ));
366
367 let ok_sku = "a".repeat(128);
368 assert!(validate_sku(&ok_sku).is_ok());
369 }
370
371 #[test]
372 fn test_validate_lengths_counts_characters_not_bytes() {
373 let description = "é".repeat(MAXIMUM_DESCRIPTION_LENGTH);
376 assert_eq!(description.len(), MAXIMUM_DESCRIPTION_LENGTH * 2);
377 assert!(validate_description(&description).is_ok());
378
379 let display_name = "日".repeat(MAXIMUM_DISPLAY_NAME_LENGTH);
380 assert_eq!(display_name.len(), MAXIMUM_DISPLAY_NAME_LENGTH * 3);
381 assert!(validate_display_name(&display_name).is_ok());
382
383 let sku = "é".repeat(MAXIMUM_SKU_LENGTH);
384 assert!(validate_sku(&sku).is_ok());
385
386 let long_description = "é".repeat(MAXIMUM_DESCRIPTION_LENGTH + 1);
388 assert!(matches!(
389 validate_description(&long_description),
390 Err(ValidationError::DescriptionTooLong(46))
391 ));
392
393 let long_display_name = "日".repeat(MAXIMUM_DISPLAY_NAME_LENGTH + 1);
394 assert!(matches!(
395 validate_display_name(&long_display_name),
396 Err(ValidationError::DisplayNameTooLong(31))
397 ));
398
399 let long_sku = "é".repeat(MAXIMUM_SKU_LENGTH + 1);
400 assert!(matches!(
401 validate_sku(&long_sku),
402 Err(ValidationError::SkuTooLong(129))
403 ));
404 }
405
406 #[test]
407 fn test_validate_uuid() {
408 let uuid = Uuid::new_v4();
409 assert_eq!(validate_uuid(&uuid).unwrap(), uuid);
410 }
411
412 #[test]
413 fn test_validate_period_count_accepts_boundaries() {
414 assert_eq!(validate_period_count(1).unwrap(), 1);
415 assert_eq!(
416 validate_period_count(MAXIMUM_PERIOD_COUNT).unwrap(),
417 MAXIMUM_PERIOD_COUNT
418 );
419 assert_eq!(validate_period_count(6).unwrap(), 6);
420 }
421
422 #[test]
423 fn test_validate_period_count_rejects_out_of_range() {
424 assert!(matches!(
425 validate_period_count(0),
426 Err(ValidationError::InvalidPeriodCount(0))
427 ));
428 assert!(matches!(
429 validate_period_count(13),
430 Err(ValidationError::InvalidPeriodCount(13))
431 ));
432 assert!(matches!(
433 validate_period_count(-1),
434 Err(ValidationError::InvalidPeriodCount(-1))
435 ));
436 }
437
438 #[test]
439 fn test_validate_items() {
440 let valid_list = vec!["item1"];
441 assert_eq!(validate_items(valid_list.clone()).unwrap(), valid_list);
442
443 let empty_list: Vec<&str> = vec![];
444 assert!(matches!(
445 validate_items(empty_list),
446 Err(ValidationError::EmptyItems)
447 ));
448 }
449}