use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
InvalidCurrencyLength(usize),
InvalidCurrencyFormat(String),
EmptyTaxCode,
EmptyTransactionId,
EmptyTargetProductId,
UuidTooLong(usize),
NegativePrice(i64),
DescriptionTooLong(usize),
DisplayNameTooLong(usize),
SkuTooLong(usize),
InvalidPeriodCount(i32),
EmptyItems,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::InvalidCurrencyLength(len) => {
write!(
f,
"Currency must be a 3-letter ISO 4217 code, got {} characters",
len
)
}
ValidationError::InvalidCurrencyFormat(currency) => {
write!(
f,
"Currency must contain only uppercase letters: {}",
currency
)
}
ValidationError::EmptyTaxCode => write!(f, "Tax code cannot be empty"),
ValidationError::EmptyTransactionId => write!(f, "Transaction ID cannot be empty"),
ValidationError::EmptyTargetProductId => write!(f, "Target Product ID cannot be empty"),
ValidationError::UuidTooLong(len) => {
write!(
f,
"UUID string representation cannot exceed {} characters, got {}",
MAXIMUM_REQUEST_REFERENCE_ID_LENGTH, len
)
}
ValidationError::NegativePrice(price) => {
write!(f, "Price cannot be negative: {}", price)
}
ValidationError::DescriptionTooLong(len) => {
write!(
f,
"Description length ({}) exceeds maximum allowed ({})",
len, MAXIMUM_DESCRIPTION_LENGTH
)
}
ValidationError::DisplayNameTooLong(len) => {
write!(
f,
"Display name length ({}) exceeds maximum allowed ({})",
len, MAXIMUM_DISPLAY_NAME_LENGTH
)
}
ValidationError::SkuTooLong(len) => {
write!(
f,
"SKU length ({}) exceeds maximum allowed ({})",
len, MAXIMUM_SKU_LENGTH
)
}
ValidationError::InvalidPeriodCount(period_count) => {
write!(
f,
"AdvancedCommercePeriod count must be between 1 and {} inclusive, got {}",
MAXIMUM_PERIOD_COUNT, period_count
)
}
ValidationError::EmptyItems => write!(f, "Items list cannot be empty"),
}
}
}
impl std::error::Error for ValidationError {}
pub const CURRENCY_CODE_LENGTH: usize = 3;
pub const MAXIMUM_STOREFRONT_LENGTH: usize = 10;
pub const MAXIMUM_REQUEST_REFERENCE_ID_LENGTH: usize = 36;
pub const MAXIMUM_DESCRIPTION_LENGTH: usize = 45;
pub const MAXIMUM_DISPLAY_NAME_LENGTH: usize = 30;
const MAXIMUM_SKU_LENGTH: usize = 128;
pub const MAXIMUM_PERIOD_COUNT: i32 = 12;
pub fn validate_currency(currency: &str) -> Result<String, ValidationError> {
if currency.len() != CURRENCY_CODE_LENGTH {
return Err(ValidationError::InvalidCurrencyLength(currency.len()));
}
if !currency
.chars()
.all(|c| c.is_ascii_uppercase())
{
return Err(ValidationError::InvalidCurrencyFormat(currency.to_string()));
}
Ok(currency.to_string())
}
pub fn validate_tax_code(tax_code: &str) -> Result<String, ValidationError> {
if tax_code.trim().is_empty() {
return Err(ValidationError::EmptyTaxCode);
}
Ok(tax_code.to_string())
}
pub fn validate_transaction_id(transaction_id: &str) -> Result<String, ValidationError> {
if transaction_id.trim().is_empty() {
return Err(ValidationError::EmptyTransactionId);
}
Ok(transaction_id.to_string())
}
pub fn validate_target_product_id(target_product_id: &str) -> Result<String, ValidationError> {
if target_product_id.trim().is_empty() {
return Err(ValidationError::EmptyTargetProductId);
}
Ok(target_product_id.to_string())
}
pub fn validate_uuid(uuid: &Uuid) -> Result<Uuid, ValidationError> {
let uuid_string = uuid.to_string();
if uuid_string.len() > MAXIMUM_REQUEST_REFERENCE_ID_LENGTH {
return Err(ValidationError::UuidTooLong(uuid_string.len()));
}
Ok(*uuid)
}
pub fn validate_price(price: i64) -> Result<i64, ValidationError> {
if price < 0 {
return Err(ValidationError::NegativePrice(price));
}
Ok(price)
}
pub fn validate_description(description: &str) -> Result<String, ValidationError> {
let length = description.chars().count();
if length > MAXIMUM_DESCRIPTION_LENGTH {
return Err(ValidationError::DescriptionTooLong(length));
}
Ok(description.to_string())
}
pub fn validate_display_name(display_name: &str) -> Result<String, ValidationError> {
let length = display_name.chars().count();
if length > MAXIMUM_DISPLAY_NAME_LENGTH {
return Err(ValidationError::DisplayNameTooLong(length));
}
Ok(display_name.to_string())
}
pub fn validate_sku(sku: &str) -> Result<String, ValidationError> {
let length = sku.chars().count();
if length > MAXIMUM_SKU_LENGTH {
return Err(ValidationError::SkuTooLong(length));
}
Ok(sku.to_string())
}
pub fn validate_period_count(period_count: i32) -> Result<i32, ValidationError> {
if !(1..=MAXIMUM_PERIOD_COUNT).contains(&period_count) {
return Err(ValidationError::InvalidPeriodCount(period_count));
}
Ok(period_count)
}
pub fn validate_items<T>(items: Vec<T>) -> Result<Vec<T>, ValidationError> {
if items.is_empty() {
return Err(ValidationError::EmptyItems);
}
Ok(items)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_currency_valid() {
assert_eq!(validate_currency("USD").unwrap(), "USD");
assert_eq!(validate_currency("EUR").unwrap(), "EUR");
assert_eq!(validate_currency("GBP").unwrap(), "GBP");
}
#[test]
fn test_validate_currency_invalid_length() {
assert!(matches!(
validate_currency("US"),
Err(ValidationError::InvalidCurrencyLength(2))
));
assert!(matches!(
validate_currency("USDD"),
Err(ValidationError::InvalidCurrencyLength(4))
));
}
#[test]
fn test_validate_currency_invalid_format() {
assert!(matches!(
validate_currency("usd"),
Err(ValidationError::InvalidCurrencyFormat(_))
));
assert!(matches!(
validate_currency("US1"),
Err(ValidationError::InvalidCurrencyFormat(_))
));
}
#[test]
fn test_validate_price_valid() {
assert_eq!(validate_price(0).unwrap(), 0);
assert_eq!(validate_price(100).unwrap(), 100);
assert_eq!(validate_price(999999).unwrap(), 999999);
}
#[test]
fn test_validate_price_invalid() {
assert!(matches!(
validate_price(-1),
Err(ValidationError::NegativePrice(-1))
));
assert!(matches!(
validate_price(-100),
Err(ValidationError::NegativePrice(-100))
));
}
#[test]
fn test_validate_empty_strings() {
assert!(matches!(
validate_tax_code(""),
Err(ValidationError::EmptyTaxCode)
));
assert!(matches!(
validate_tax_code(" "),
Err(ValidationError::EmptyTaxCode)
));
assert!(validate_tax_code("ABC123").is_ok());
}
#[test]
fn test_validate_lengths() {
let long_description = "a".repeat(46);
assert!(matches!(
validate_description(&long_description),
Err(ValidationError::DescriptionTooLong(46))
));
let ok_description = "a".repeat(45);
assert!(validate_description(&ok_description).is_ok());
let long_display_name = "a".repeat(31);
assert!(matches!(
validate_display_name(&long_display_name),
Err(ValidationError::DisplayNameTooLong(31))
));
let ok_display_name = "a".repeat(30);
assert!(validate_display_name(&ok_display_name).is_ok());
let long_sku = "a".repeat(129);
assert!(matches!(
validate_sku(&long_sku),
Err(ValidationError::SkuTooLong(129))
));
let ok_sku = "a".repeat(128);
assert!(validate_sku(&ok_sku).is_ok());
}
#[test]
fn test_validate_lengths_counts_characters_not_bytes() {
let description = "é".repeat(MAXIMUM_DESCRIPTION_LENGTH);
assert_eq!(description.len(), MAXIMUM_DESCRIPTION_LENGTH * 2);
assert!(validate_description(&description).is_ok());
let display_name = "日".repeat(MAXIMUM_DISPLAY_NAME_LENGTH);
assert_eq!(display_name.len(), MAXIMUM_DISPLAY_NAME_LENGTH * 3);
assert!(validate_display_name(&display_name).is_ok());
let sku = "é".repeat(MAXIMUM_SKU_LENGTH);
assert!(validate_sku(&sku).is_ok());
let long_description = "é".repeat(MAXIMUM_DESCRIPTION_LENGTH + 1);
assert!(matches!(
validate_description(&long_description),
Err(ValidationError::DescriptionTooLong(46))
));
let long_display_name = "日".repeat(MAXIMUM_DISPLAY_NAME_LENGTH + 1);
assert!(matches!(
validate_display_name(&long_display_name),
Err(ValidationError::DisplayNameTooLong(31))
));
let long_sku = "é".repeat(MAXIMUM_SKU_LENGTH + 1);
assert!(matches!(
validate_sku(&long_sku),
Err(ValidationError::SkuTooLong(129))
));
}
#[test]
fn test_validate_uuid() {
let uuid = Uuid::new_v4();
assert_eq!(validate_uuid(&uuid).unwrap(), uuid);
}
#[test]
fn test_validate_period_count_accepts_boundaries() {
assert_eq!(validate_period_count(1).unwrap(), 1);
assert_eq!(
validate_period_count(MAXIMUM_PERIOD_COUNT).unwrap(),
MAXIMUM_PERIOD_COUNT
);
assert_eq!(validate_period_count(6).unwrap(), 6);
}
#[test]
fn test_validate_period_count_rejects_out_of_range() {
assert!(matches!(
validate_period_count(0),
Err(ValidationError::InvalidPeriodCount(0))
));
assert!(matches!(
validate_period_count(13),
Err(ValidationError::InvalidPeriodCount(13))
));
assert!(matches!(
validate_period_count(-1),
Err(ValidationError::InvalidPeriodCount(-1))
));
}
#[test]
fn test_validate_items() {
let valid_list = vec!["item1"];
assert_eq!(validate_items(valid_list.clone()).unwrap(), valid_list);
let empty_list: Vec<&str> = vec![];
assert!(matches!(
validate_items(empty_list),
Err(ValidationError::EmptyItems)
));
}
}