1use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use stateset_primitives::IntegrationFieldMappingId;
12use strum::{Display, EnumString};
13
14#[derive(
16 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
17)]
18#[serde(rename_all = "snake_case")]
19#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
20#[non_exhaustive]
21pub enum FieldTransform {
22 #[default]
24 None,
25 Uppercase,
27 Lowercase,
29 Trim,
31}
32
33impl FieldTransform {
34 #[must_use]
36 pub fn apply(&self, value: &str) -> String {
37 match self {
38 Self::None => value.to_string(),
39 Self::Uppercase => value.to_uppercase(),
40 Self::Lowercase => value.to_lowercase(),
41 Self::Trim => value.trim().to_string(),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct IntegrationFieldMapping {
49 pub id: IntegrationFieldMappingId,
51 pub integration_account: String,
53 pub mapping_group: String,
55 pub source_field: String,
57 pub destination_field: String,
59 pub template: Option<String>,
61 pub transform: FieldTransform,
63 pub fallback: Option<String>,
65 pub is_active: bool,
67 pub created_at: DateTime<Utc>,
69 pub updated_at: DateTime<Utc>,
71}
72
73impl IntegrationFieldMapping {
74 #[must_use]
77 pub fn resolve_value(&self, source: Option<&str>) -> Option<String> {
78 let transformed = source.map(|v| self.transform.apply(v)).filter(|v| !v.is_empty());
79 transformed.or_else(|| self.fallback.clone())
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CreateIntegrationFieldMapping {
86 pub integration_account: String,
88 pub mapping_group: String,
90 pub source_field: String,
92 pub destination_field: String,
94 pub template: Option<String>,
96 #[serde(default)]
98 pub transform: FieldTransform,
99 pub fallback: Option<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, Default)]
105pub struct UpdateIntegrationFieldMapping {
106 pub destination_field: Option<String>,
108 pub template: Option<String>,
110 pub transform: Option<FieldTransform>,
112 pub fallback: Option<String>,
114 pub is_active: Option<bool>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, Default)]
120pub struct IntegrationFieldMappingFilter {
121 pub integration_account: Option<String>,
123 pub mapping_group: Option<String>,
125 pub source_field: Option<String>,
127 pub is_active: Option<bool>,
129 pub limit: Option<u32>,
131 pub offset: Option<u32>,
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn make(transform: FieldTransform, fallback: Option<&str>) -> IntegrationFieldMapping {
140 IntegrationFieldMapping {
141 id: IntegrationFieldMappingId::new(),
142 integration_account: "acct-1".into(),
143 mapping_group: "order".into(),
144 source_field: "order.customer.email".into(),
145 destination_field: "email".into(),
146 template: None,
147 transform,
148 fallback: fallback.map(String::from),
149 is_active: true,
150 created_at: Utc::now(),
151 updated_at: Utc::now(),
152 }
153 }
154
155 #[test]
156 fn transform_apply() {
157 assert_eq!(FieldTransform::Uppercase.apply("aBc"), "ABC");
158 assert_eq!(FieldTransform::Lowercase.apply("aBc"), "abc");
159 assert_eq!(FieldTransform::Trim.apply(" x "), "x");
160 assert_eq!(FieldTransform::None.apply(" x "), " x ");
161 }
162
163 #[test]
164 fn resolve_applies_transform() {
165 let m = make(FieldTransform::Uppercase, None);
166 assert_eq!(m.resolve_value(Some("hi")), Some("HI".to_string()));
167 }
168
169 #[test]
170 fn resolve_uses_fallback_when_empty_or_absent() {
171 let m = make(FieldTransform::None, Some("default@x.test"));
172 assert_eq!(m.resolve_value(None), Some("default@x.test".to_string()));
173 assert_eq!(m.resolve_value(Some("")), Some("default@x.test".to_string()));
174 assert_eq!(m.resolve_value(Some("real@x.test")), Some("real@x.test".to_string()));
175 }
176
177 #[test]
178 fn resolve_none_without_fallback() {
179 let m = make(FieldTransform::None, None);
180 assert_eq!(m.resolve_value(None), None);
181 }
182
183 #[test]
184 fn transform_roundtrip() {
185 for t in [
186 FieldTransform::None,
187 FieldTransform::Uppercase,
188 FieldTransform::Lowercase,
189 FieldTransform::Trim,
190 ] {
191 assert_eq!(t.to_string().parse::<FieldTransform>().unwrap(), t);
192 }
193 }
194}