app_store_server_library/primitives/advanced_commerce/
validation_utils.rs1use std::fmt;
2use uuid::Uuid;
3
4#[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
56pub 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
64pub 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
84pub 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
99pub 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
114pub 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
129pub 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
145pub fn validate_price(price: i64) -> Result<i64, ValidationError> {
154 if price < 0 {
155 return Err(ValidationError::NegativePrice(price));
156 }
157 Ok(price)
158}
159
160pub 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
175pub 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
190pub 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}