use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use rust_decimal::Decimal;
use serde::de::DeserializeOwned;
use stateset_core::{CommerceError, Result};
use uuid::Uuid;
pub(crate) fn parse_uuid(s: &str, entity: &str, field: &str) -> Result<Uuid> {
Uuid::parse_str(s).map_err(|e| {
CommerceError::DatabaseError(format!("Invalid UUID for {entity}.{field}: '{s}' - {e}"))
})
}
pub(crate) fn parse_uuid_opt(s: Option<String>, entity: &str, field: &str) -> Result<Option<Uuid>> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_uuid(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_datetime(s: &str, entity: &str, field: &str) -> Result<DateTime<Utc>> {
parse_datetime_any(s).ok_or_else(|| {
CommerceError::DatabaseError(format!(
"Invalid datetime for {entity}.{field}: '{s}' - expected RFC3339 or SQLite datetime"
))
})
}
pub(crate) fn parse_datetime_opt(
s: Option<String>,
entity: &str,
field: &str,
) -> Result<Option<DateTime<Utc>>> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_datetime(val, entity, field)?)),
_ => Ok(None),
}
}
#[allow(dead_code)]
pub(crate) fn parse_date(s: &str, entity: &str, field: &str) -> Result<NaiveDate> {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
CommerceError::DatabaseError(format!("Invalid date for {entity}.{field}: '{s}' - {e}"))
})
}
#[allow(dead_code)]
pub(crate) fn parse_date_opt(
s: Option<String>,
entity: &str,
field: &str,
) -> Result<Option<NaiveDate>> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_date(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_decimal(s: &str, entity: &str, field: &str) -> Result<Decimal> {
s.parse::<Decimal>().map_err(|e| {
CommerceError::DatabaseError(format!("Invalid decimal for {entity}.{field}: '{s}' - {e}"))
})
}
pub(crate) fn parse_decimal_opt(
s: Option<String>,
entity: &str,
field: &str,
) -> Result<Option<Decimal>> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_decimal(val, entity, field)?)),
_ => Ok(None),
}
}
#[allow(dead_code)]
pub(crate) fn parse_json<T: DeserializeOwned>(s: &str, entity: &str, field: &str) -> Result<T> {
serde_json::from_str(s).map_err(|e| {
let preview = if s.len() > 50 { &s[..50] } else { s };
CommerceError::DatabaseError(format!(
"Invalid JSON for {entity}.{field}: '{preview}...' - {e}"
))
})
}
#[allow(dead_code)]
pub(crate) fn parse_json_opt<T: DeserializeOwned>(
s: Option<String>,
entity: &str,
field: &str,
) -> Result<Option<T>> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_json(val, entity, field)?)),
_ => Ok(None),
}
}
#[allow(dead_code)]
pub(crate) fn parse_json_or_default<T: DeserializeOwned + Default>(s: &str) -> T {
if s.is_empty() {
return T::default();
}
serde_json::from_str(s).unwrap_or_default()
}
pub(crate) fn parse_enum<T>(s: &str, entity: &str, field: &str) -> Result<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
s.parse::<T>().map_err(|e| {
CommerceError::DatabaseError(format!(
"Invalid {} for {}.{}: '{}' - {}",
std::any::type_name::<T>(),
entity,
field,
s,
e
))
})
}
#[allow(dead_code)]
pub(crate) fn parse_enum_opt<T>(s: Option<String>, entity: &str, field: &str) -> Result<Option<T>>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_enum(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_enum_row<T>(
s: &str,
entity: &str,
field: &str,
) -> std::result::Result<T, rusqlite::Error>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
s.parse::<T>().map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid {} for {}.{}: '{}' - {}",
std::any::type_name::<T>(),
entity,
field,
s,
e
),
)),
)
})
}
#[allow(dead_code)]
pub(crate) fn parse_i32(s: &str, entity: &str, field: &str) -> Result<i32> {
s.parse::<i32>().map_err(|e| {
CommerceError::DatabaseError(format!("Invalid i32 for {entity}.{field}: '{s}' - {e}"))
})
}
#[allow(dead_code)]
pub(crate) fn parse_i64(s: &str, entity: &str, field: &str) -> Result<i64> {
s.parse::<i64>().map_err(|e| {
CommerceError::DatabaseError(format!("Invalid i64 for {entity}.{field}: '{s}' - {e}"))
})
}
#[macro_export]
macro_rules! parse_ctx {
($entity:expr, $field:expr) => {
($entity, $field)
};
}
pub(crate) fn parse_uuid_row(
s: &str,
entity: &str,
field: &str,
) -> std::result::Result<Uuid, rusqlite::Error> {
Uuid::parse_str(s).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid UUID for {entity}.{field}: '{s}' - {e}"),
)),
)
})
}
pub(crate) fn parse_uuid_opt_row(
s: Option<String>,
entity: &str,
field: &str,
) -> std::result::Result<Option<Uuid>, rusqlite::Error> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_uuid_row(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_datetime_row(
s: &str,
entity: &str,
field: &str,
) -> std::result::Result<DateTime<Utc>, rusqlite::Error> {
parse_datetime_any(s).ok_or_else(|| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid datetime for {entity}.{field}: '{s}' - expected RFC3339 or SQLite datetime"
),
)),
)
})
}
pub(crate) fn parse_datetime_opt_row(
s: Option<String>,
entity: &str,
field: &str,
) -> std::result::Result<Option<DateTime<Utc>>, rusqlite::Error> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_datetime_row(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_decimal_row(
s: &str,
entity: &str,
field: &str,
) -> std::result::Result<Decimal, rusqlite::Error> {
s.parse::<Decimal>().map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid decimal for {entity}.{field}: '{s}' - {e}"),
)),
)
})
}
pub(crate) fn parse_decimal_opt_row(
s: Option<String>,
entity: &str,
field: &str,
) -> std::result::Result<Option<Decimal>, rusqlite::Error> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_decimal_row(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_json_row<T: DeserializeOwned>(
s: &str,
entity: &str,
field: &str,
) -> std::result::Result<T, rusqlite::Error> {
serde_json::from_str(s).map_err(|e| {
let preview = if s.len() > 50 { &s[..50] } else { s };
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid JSON for {entity}.{field}: '{preview}...' - {e}"),
)),
)
})
}
pub(crate) fn parse_json_opt_row<T: DeserializeOwned>(
s: Option<String>,
entity: &str,
field: &str,
) -> std::result::Result<Option<T>, rusqlite::Error> {
match s {
Some(ref val) if !val.is_empty() => Ok(Some(parse_json_row(val, entity, field)?)),
_ => Ok(None),
}
}
pub(crate) fn parse_date_row(
s: &str,
entity: &str,
field: &str,
) -> std::result::Result<NaiveDate, rusqlite::Error> {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid date for {entity}.{field}: '{s}' - {e}"),
)),
)
})
}
fn parse_datetime_any(s: &str) -> Option<DateTime<Utc>> {
if let Ok(dt) = s.parse::<DateTime<Utc>>() {
return Some(dt);
}
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt.with_timezone(&Utc));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
return Some(dt.and_utc());
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
return Some(dt.and_utc());
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Some(dt.and_utc());
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
return Some(dt.and_utc());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_uuid_valid() {
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let result = parse_uuid(uuid_str, "test", "id");
assert!(result.is_ok());
}
#[test]
fn test_parse_uuid_invalid() {
let result = parse_uuid("not-a-uuid", "order", "id");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("order.id"));
assert!(err.contains("not-a-uuid"));
}
#[test]
fn test_parse_uuid_opt_none() {
let result = parse_uuid_opt(None, "test", "id");
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_parse_uuid_opt_empty() {
let result = parse_uuid_opt(Some(String::new()), "test", "id");
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_parse_decimal_valid() {
let result = parse_decimal("123.45", "order", "total");
assert!(result.is_ok());
assert_eq!(result.unwrap().to_string(), "123.45");
}
#[test]
fn test_parse_decimal_invalid() {
let result = parse_decimal("not-a-number", "order", "total");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("order.total"));
}
#[test]
fn test_parse_datetime_valid() {
let result = parse_datetime("2024-01-15T10:30:00Z", "order", "created_at");
assert!(result.is_ok());
}
#[test]
fn test_parse_datetime_sqlite_format() {
let result = parse_datetime("2026-01-15 06:35:19", "bill", "updated_at");
assert!(result.is_ok());
}
#[test]
fn test_parse_datetime_invalid() {
let result = parse_datetime("not-a-date", "order", "created_at");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("order.created_at"));
}
#[test]
fn test_parse_json_valid() {
let result: Result<Vec<String>> = parse_json("[\"a\", \"b\"]", "product", "tags");
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec!["a", "b"]);
}
#[test]
fn test_parse_json_invalid() {
let result: Result<Vec<String>> = parse_json("not-json", "product", "tags");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("product.tags"));
}
#[test]
fn test_parse_date_valid() {
let result = parse_date("2024-01-15", "invoice", "due_date");
assert!(result.is_ok());
}
#[test]
fn test_parse_date_invalid() {
let result = parse_date("15-01-2024", "invoice", "due_date");
assert!(result.is_err());
}
}