use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use uuid::Uuid;
use crate::{CommerceError, Result, validate_required_text, validate_sku};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CustomFieldType {
String,
#[strum(serialize = "integer", serialize = "int")]
Integer,
#[strum(serialize = "decimal", serialize = "number")]
Decimal,
#[strum(serialize = "boolean", serialize = "bool")]
Boolean,
#[strum(serialize = "date_time", serialize = "datetime")]
DateTime,
Uuid,
Json,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomFieldDefinition {
pub key: String,
pub field_type: CustomFieldType,
pub required: bool,
pub list: bool,
pub description: Option<String>,
}
impl CustomFieldDefinition {
pub fn validate(&self) -> Result<()> {
validate_sku(&self.key)?;
Ok(())
}
pub fn validate_value(&self, value: &serde_json::Value) -> Result<()> {
if value.is_null() {
if self.required {
return Err(CommerceError::InvalidInput {
field: format!("custom_object.values.{}", self.key),
message: "is required".into(),
});
}
return Ok(());
}
if self.list {
let arr = value.as_array().ok_or_else(|| CommerceError::InvalidInput {
field: format!("custom_object.values.{}", self.key),
message: "must be an array".into(),
})?;
for (idx, item) in arr.iter().enumerate() {
if item.is_null() {
return Err(CommerceError::InvalidInput {
field: format!("custom_object.values.{}[{}]", self.key, idx),
message: "cannot be null".into(),
});
}
validate_scalar(self.field_type, item).map_err(|mut e| {
if let CommerceError::InvalidInput { ref mut field, .. } = e {
*field = format!("custom_object.values.{}[{}]", self.key, idx);
}
e
})?;
}
return Ok(());
}
if value.is_array() {
return Err(CommerceError::InvalidInput {
field: format!("custom_object.values.{}", self.key),
message: "must be a scalar value".into(),
});
}
validate_scalar(self.field_type, value).map_err(|mut e| {
if let CommerceError::InvalidInput { ref mut field, .. } = e {
*field = format!("custom_object.values.{}", self.key);
}
e
})
}
}
fn validate_scalar(field_type: CustomFieldType, value: &serde_json::Value) -> Result<()> {
match field_type {
CustomFieldType::String => match value {
serde_json::Value::String(s) => {
if s.trim().is_empty() {
return Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "cannot be empty".into(),
});
}
Ok(())
}
_ => Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a string".into(),
}),
},
CustomFieldType::Integer => match value {
serde_json::Value::Number(n) => {
if n.is_i64() || n.is_u64() {
Ok(())
} else {
Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be an integer".into(),
})
}
}
serde_json::Value::String(s) => {
s.parse::<i64>().map_err(|_| CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be an integer".into(),
})?;
Ok(())
}
_ => Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be an integer".into(),
}),
},
CustomFieldType::Decimal => match value {
serde_json::Value::Number(n) => {
let s = n.to_string();
s.parse::<Decimal>().map_err(|_| CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a decimal".into(),
})?;
Ok(())
}
serde_json::Value::String(s) => {
s.parse::<Decimal>().map_err(|_| CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a decimal".into(),
})?;
Ok(())
}
_ => Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a decimal".into(),
}),
},
CustomFieldType::Boolean => match value {
serde_json::Value::Bool(_) => Ok(()),
_ => Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a boolean".into(),
}),
},
CustomFieldType::DateTime => match value {
serde_json::Value::String(s) => {
chrono::DateTime::parse_from_rfc3339(s).map_err(|_| {
CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be an RFC3339 datetime string".into(),
}
})?;
Ok(())
}
_ => Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be an RFC3339 datetime string".into(),
}),
},
CustomFieldType::Uuid => match value {
serde_json::Value::String(s) => {
Uuid::parse_str(s).map_err(|_| CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a UUID string".into(),
})?;
Ok(())
}
_ => Err(CommerceError::InvalidInput {
field: "custom_object.value".into(),
message: "must be a UUID string".into(),
}),
},
CustomFieldType::Json => Ok(()),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomObjectType {
pub id: Uuid,
pub handle: String,
pub display_name: String,
pub description: String,
pub fields: Vec<CustomFieldDefinition>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub version: i32,
}
impl CustomObjectType {
pub fn validate_values(&self, values: &serde_json::Value) -> Result<()> {
let obj = values.as_object().ok_or_else(|| CommerceError::InvalidInput {
field: "custom_object.values".into(),
message: "must be a JSON object".into(),
})?;
for key in obj.keys() {
if !self.fields.iter().any(|f| f.key == *key) {
return Err(CommerceError::InvalidInput {
field: format!("custom_object.values.{key}"),
message: "unknown field".into(),
});
}
}
for field in &self.fields {
field.validate()?;
match obj.get(&field.key) {
Some(v) => field.validate_value(v)?,
None if field.required => {
return Err(CommerceError::InvalidInput {
field: format!("custom_object.values.{}", field.key),
message: "is required".into(),
});
}
None => {}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreateCustomObjectType {
pub handle: String,
pub display_name: String,
pub description: Option<String>,
pub fields: Vec<CustomFieldDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateCustomObjectType {
pub display_name: Option<String>,
pub description: Option<String>,
pub fields: Option<Vec<CustomFieldDefinition>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CustomObjectTypeFilter {
pub search: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomObject {
pub id: Uuid,
pub type_id: Uuid,
pub type_handle: String,
pub handle: Option<String>,
pub owner_type: Option<String>,
pub owner_id: Option<String>,
pub values: serde_json::Value,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCustomObject {
pub type_handle: String,
pub handle: Option<String>,
pub owner_type: Option<String>,
pub owner_id: Option<String>,
pub values: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateCustomObject {
pub handle: Option<String>,
pub owner_type: Option<String>,
pub owner_id: Option<String>,
pub values: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CustomObjectFilter {
pub type_handle: Option<String>,
pub owner_type: Option<String>,
pub owner_id: Option<String>,
pub handle: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
pub fn validate_custom_object_type_input(input: &CreateCustomObjectType) -> Result<()> {
validate_sku(&input.handle)?;
validate_required_text("custom_object_type.display_name", &input.display_name, 128)?;
let mut keys = std::collections::HashSet::new();
for field in &input.fields {
field.validate()?;
if !keys.insert(field.key.clone()) {
return Err(CommerceError::InvalidInput {
field: "custom_object_type.fields".into(),
message: format!("duplicate field key: {}", field.key),
});
}
}
Ok(())
}