use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use stateset_primitives::IntegrationFieldMappingId;
use strum::{Display, EnumString};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum FieldTransform {
#[default]
None,
Uppercase,
Lowercase,
Trim,
}
impl FieldTransform {
#[must_use]
pub fn apply(&self, value: &str) -> String {
match self {
Self::None => value.to_string(),
Self::Uppercase => value.to_uppercase(),
Self::Lowercase => value.to_lowercase(),
Self::Trim => value.trim().to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntegrationFieldMapping {
pub id: IntegrationFieldMappingId,
pub integration_account: String,
pub mapping_group: String,
pub source_field: String,
pub destination_field: String,
pub template: Option<String>,
pub transform: FieldTransform,
pub fallback: Option<String>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl IntegrationFieldMapping {
#[must_use]
pub fn resolve_value(&self, source: Option<&str>) -> Option<String> {
let transformed = source.map(|v| self.transform.apply(v)).filter(|v| !v.is_empty());
transformed.or_else(|| self.fallback.clone())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateIntegrationFieldMapping {
pub integration_account: String,
pub mapping_group: String,
pub source_field: String,
pub destination_field: String,
pub template: Option<String>,
#[serde(default)]
pub transform: FieldTransform,
pub fallback: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateIntegrationFieldMapping {
pub destination_field: Option<String>,
pub template: Option<String>,
pub transform: Option<FieldTransform>,
pub fallback: Option<String>,
pub is_active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IntegrationFieldMappingFilter {
pub integration_account: Option<String>,
pub mapping_group: Option<String>,
pub source_field: Option<String>,
pub is_active: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
fn make(transform: FieldTransform, fallback: Option<&str>) -> IntegrationFieldMapping {
IntegrationFieldMapping {
id: IntegrationFieldMappingId::new(),
integration_account: "acct-1".into(),
mapping_group: "order".into(),
source_field: "order.customer.email".into(),
destination_field: "email".into(),
template: None,
transform,
fallback: fallback.map(String::from),
is_active: true,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn transform_apply() {
assert_eq!(FieldTransform::Uppercase.apply("aBc"), "ABC");
assert_eq!(FieldTransform::Lowercase.apply("aBc"), "abc");
assert_eq!(FieldTransform::Trim.apply(" x "), "x");
assert_eq!(FieldTransform::None.apply(" x "), " x ");
}
#[test]
fn resolve_applies_transform() {
let m = make(FieldTransform::Uppercase, None);
assert_eq!(m.resolve_value(Some("hi")), Some("HI".to_string()));
}
#[test]
fn resolve_uses_fallback_when_empty_or_absent() {
let m = make(FieldTransform::None, Some("default@x.test"));
assert_eq!(m.resolve_value(None), Some("default@x.test".to_string()));
assert_eq!(m.resolve_value(Some("")), Some("default@x.test".to_string()));
assert_eq!(m.resolve_value(Some("real@x.test")), Some("real@x.test".to_string()));
}
#[test]
fn resolve_none_without_fallback() {
let m = make(FieldTransform::None, None);
assert_eq!(m.resolve_value(None), None);
}
#[test]
fn transform_roundtrip() {
for t in [
FieldTransform::None,
FieldTransform::Uppercase,
FieldTransform::Lowercase,
FieldTransform::Trim,
] {
assert_eq!(t.to_string().parse::<FieldTransform>().unwrap(), t);
}
}
}