app_store_server_library/primitives/advanced_commerce/
validation_utils.rs

1use std::fmt;
2use uuid::Uuid;
3
4/// Validation errors for Advanced Commerce API
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum ValidationError {
7    InvalidCurrencyLength(usize),
8    InvalidCurrencyFormat(String),
9    EmptyTaxCode,
10    EmptyTransactionId,
11    EmptyTargetProductId,
12    UuidTooLong(usize),
13    NegativePrice(i64),
14    DescriptionTooLong(usize),
15    DisplayNameTooLong(usize),
16    SkuTooLong(usize),
17}
18
19impl fmt::Display for ValidationError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            ValidationError::InvalidCurrencyLength(len) => {
23                write!(f, "Currency must be a 3-letter ISO 4217 code, got {} characters", len)
24            }
25            ValidationError::InvalidCurrencyFormat(currency) => {
26                write!(f, "Currency must contain only uppercase letters: {}", currency)
27            }
28            ValidationError::EmptyTaxCode => write!(f, "Tax code cannot be empty"),
29            ValidationError::EmptyTransactionId => write!(f, "Transaction ID cannot be empty"),
30            ValidationError::EmptyTargetProductId => write!(f, "Target Product ID cannot be empty"),
31            ValidationError::UuidTooLong(len) => {
32                write!(f, "UUID string representation cannot exceed {} characters, got {}", 
33                    MAXIMUM_REQUEST_REFERENCE_ID_LENGTH, len)
34            }
35            ValidationError::NegativePrice(price) => {
36                write!(f, "Price cannot be negative: {}", price)
37            }
38            ValidationError::DescriptionTooLong(len) => {
39                write!(f, "Description length ({}) exceeds maximum allowed ({})", 
40                    len, MAXIMUM_DESCRIPTION_LENGTH)
41            }
42            ValidationError::DisplayNameTooLong(len) => {
43                write!(f, "Display name length ({}) exceeds maximum allowed ({})", 
44                    len, MAXIMUM_DISPLAY_NAME_LENGTH)
45            }
46            ValidationError::SkuTooLong(len) => {
47                write!(f, "SKU length ({}) exceeds maximum allowed ({})", 
48                    len, MAXIMUM_SKU_LENGTH)
49            }
50        }
51    }
52}
53
54impl std::error::Error for ValidationError {}
55
56/// Validation constants
57pub const CURRENCY_CODE_LENGTH: usize = 3;
58pub const MAXIMUM_STOREFRONT_LENGTH: usize = 10;
59pub const MAXIMUM_REQUEST_REFERENCE_ID_LENGTH: usize = 36;
60pub const MAXIMUM_DESCRIPTION_LENGTH: usize = 45;
61pub const MAXIMUM_DISPLAY_NAME_LENGTH: usize = 30;
62const MAXIMUM_SKU_LENGTH: usize = 128;
63
64/// Validates currency code according to ISO 4217 standard.
65/// 
66/// # Arguments
67/// * `currency` - The currency code to validate
68/// 
69/// # Returns
70/// * `Ok(String)` - The validated currency code
71/// * `Err(ValidationError)` - If validation fails
72pub fn validate_currency(currency: &str) -> Result<String, ValidationError> {
73    if currency.len() != CURRENCY_CODE_LENGTH {
74        return Err(ValidationError::InvalidCurrencyLength(currency.len()));
75    }
76    
77    if !currency.chars().all(|c| c.is_ascii_uppercase()) {
78        return Err(ValidationError::InvalidCurrencyFormat(currency.to_string()));
79    }
80    
81    Ok(currency.to_string())
82}
83
84/// Validates tax code is not empty.
85/// 
86/// # Arguments
87/// * `tax_code` - The tax code to validate
88/// 
89/// # Returns
90/// * `Ok(String)` - The validated tax code
91/// * `Err(ValidationError)` - If validation fails
92pub fn validate_tax_code(tax_code: &str) -> Result<String, ValidationError> {
93    if tax_code.trim().is_empty() {
94        return Err(ValidationError::EmptyTaxCode);
95    }
96    Ok(tax_code.to_string())
97}
98
99/// Validates transaction ID is not empty.
100/// 
101/// # Arguments
102/// * `transaction_id` - The transaction ID to validate
103/// 
104/// # Returns
105/// * `Ok(String)` - The validated transaction ID
106/// * `Err(ValidationError)` - If validation fails
107pub fn validate_transaction_id(transaction_id: &str) -> Result<String, ValidationError> {
108    if transaction_id.trim().is_empty() {
109        return Err(ValidationError::EmptyTransactionId);
110    }
111    Ok(transaction_id.to_string())
112}
113
114/// Validates target product ID is not empty.
115/// 
116/// # Arguments
117/// * `target_product_id` - The target product ID to validate
118/// 
119/// # Returns
120/// * `Ok(String)` - The validated target product ID
121/// * `Err(ValidationError)` - If validation fails
122pub fn validate_target_product_id(target_product_id: &str) -> Result<String, ValidationError> {
123    if target_product_id.trim().is_empty() {
124        return Err(ValidationError::EmptyTargetProductId);
125    }
126    Ok(target_product_id.to_string())
127}
128
129/// Validates UUID string representation doesn't exceed maximum length.
130/// 
131/// # Arguments
132/// * `uuid` - The UUID to validate
133/// 
134/// # Returns
135/// * `Ok(Uuid)` - The validated UUID
136/// * `Err(ValidationError)` - If validation fails
137pub fn validate_uuid(uuid: &Uuid) -> Result<Uuid, ValidationError> {
138    let uuid_string = uuid.to_string();
139    if uuid_string.len() > MAXIMUM_REQUEST_REFERENCE_ID_LENGTH {
140        return Err(ValidationError::UuidTooLong(uuid_string.len()));
141    }
142    Ok(*uuid)
143}
144
145/// Validates price is non-negative.
146/// 
147/// # Arguments
148/// * `price` - The price to validate
149/// 
150/// # Returns
151/// * `Ok(i64)` - The validated price
152/// * `Err(ValidationError)` - If validation fails
153pub fn validate_price(price: i64) -> Result<i64, ValidationError> {
154    if price < 0 {
155        return Err(ValidationError::NegativePrice(price));
156    }
157    Ok(price)
158}
159
160/// Validates description does not exceed maximum length.
161/// 
162/// # Arguments
163/// * `description` - The description to validate
164/// 
165/// # Returns
166/// * `Ok(String)` - The validated description
167/// * `Err(ValidationError)` - If validation fails
168pub fn validate_description(description: &str) -> Result<String, ValidationError> {
169    if description.len() > MAXIMUM_DESCRIPTION_LENGTH {
170        return Err(ValidationError::DescriptionTooLong(description.len()));
171    }
172    Ok(description.to_string())
173}
174
175/// Validates display name does not exceed maximum length.
176/// 
177/// # Arguments
178/// * `display_name` - The display name to validate
179/// 
180/// # Returns
181/// * `Ok(String)` - The validated display name
182/// * `Err(ValidationError)` - If validation fails
183pub fn validate_display_name(display_name: &str) -> Result<String, ValidationError> {
184    if display_name.len() > MAXIMUM_DISPLAY_NAME_LENGTH {
185        return Err(ValidationError::DisplayNameTooLong(display_name.len()));
186    }
187    Ok(display_name.to_string())
188}
189
190/// Validates SKU does not exceed maximum length.
191/// 
192/// # Arguments
193/// * `sku` - The SKU to validate
194/// 
195/// # Returns
196/// * `Ok(String)` - The validated SKU
197/// * `Err(ValidationError)` - If validation fails
198pub fn validate_sku(sku: &str) -> Result<String, ValidationError> {
199    if sku.len() > MAXIMUM_SKU_LENGTH {
200        return Err(ValidationError::SkuTooLong(sku.len()));
201    }
202    Ok(sku.to_string())
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_validate_currency_valid() {
211        assert_eq!(validate_currency("USD").unwrap(), "USD");
212        assert_eq!(validate_currency("EUR").unwrap(), "EUR");
213        assert_eq!(validate_currency("GBP").unwrap(), "GBP");
214    }
215
216    #[test]
217    fn test_validate_currency_invalid_length() {
218        assert!(matches!(
219            validate_currency("US"),
220            Err(ValidationError::InvalidCurrencyLength(2))
221        ));
222        assert!(matches!(
223            validate_currency("USDD"),
224            Err(ValidationError::InvalidCurrencyLength(4))
225        ));
226    }
227
228    #[test]
229    fn test_validate_currency_invalid_format() {
230        assert!(matches!(
231            validate_currency("usd"),
232            Err(ValidationError::InvalidCurrencyFormat(_))
233        ));
234        assert!(matches!(
235            validate_currency("US1"),
236            Err(ValidationError::InvalidCurrencyFormat(_))
237        ));
238    }
239
240    #[test]
241    fn test_validate_price_valid() {
242        assert_eq!(validate_price(0).unwrap(), 0);
243        assert_eq!(validate_price(100).unwrap(), 100);
244        assert_eq!(validate_price(999999).unwrap(), 999999);
245    }
246
247    #[test]
248    fn test_validate_price_invalid() {
249        assert!(matches!(
250            validate_price(-1),
251            Err(ValidationError::NegativePrice(-1))
252        ));
253        assert!(matches!(
254            validate_price(-100),
255            Err(ValidationError::NegativePrice(-100))
256        ));
257    }
258
259    #[test]
260    fn test_validate_empty_strings() {
261        assert!(matches!(
262            validate_tax_code(""),
263            Err(ValidationError::EmptyTaxCode)
264        ));
265        assert!(matches!(
266            validate_tax_code("  "),
267            Err(ValidationError::EmptyTaxCode)
268        ));
269        assert!(validate_tax_code("ABC123").is_ok());
270    }
271
272    #[test]
273    fn test_validate_lengths() {
274        let long_description = "a".repeat(46);
275        assert!(matches!(
276            validate_description(&long_description),
277            Err(ValidationError::DescriptionTooLong(46))
278        ));
279        
280        let ok_description = "a".repeat(45);
281        assert!(validate_description(&ok_description).is_ok());
282        
283        let long_display_name = "a".repeat(31);
284        assert!(matches!(
285            validate_display_name(&long_display_name),
286            Err(ValidationError::DisplayNameTooLong(31))
287        ));
288        
289        let ok_display_name = "a".repeat(30);
290        assert!(validate_display_name(&ok_display_name).is_ok());
291        
292        let long_sku = "a".repeat(129);
293        assert!(matches!(
294            validate_sku(&long_sku),
295            Err(ValidationError::SkuTooLong(129))
296        ));
297        
298        let ok_sku = "a".repeat(128);
299        assert!(validate_sku(&ok_sku).is_ok());
300    }
301
302    #[test]
303    fn test_validate_uuid() {
304        let uuid = Uuid::new_v4();
305        assert_eq!(validate_uuid(&uuid).unwrap(), uuid);
306    }
307}