1use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23use crate::config::{
24 EffectiveFromConfig, ElementTemplate, ElementType, MappingCondition, OperationType,
25 SourceMapping, TimestampFormat,
26};
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "camelCase")]
31pub struct SourceMappingDto {
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub when: Option<MappingConditionDto>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub operation: Option<OperationTypeDto>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub operation_from: Option<String>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub operation_map: Option<HashMap<String, OperationTypeDto>>,
47
48 pub element_type: ElementTypeDto,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub effective_from: Option<EffectiveFromConfigDto>,
54
55 pub template: ElementTemplateDto,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61#[serde(rename_all = "camelCase")]
62pub struct MappingConditionDto {
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub header: Option<String>,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub field: Option<String>,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub equals: Option<String>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub contains: Option<String>,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub regex: Option<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86#[serde(rename_all = "lowercase")]
87pub enum OperationTypeDto {
88 Insert,
89 Update,
90 Delete,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[serde(rename_all = "lowercase")]
96pub enum ElementTypeDto {
97 Node,
98 Relation,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103#[serde(untagged)]
104pub enum EffectiveFromConfigDto {
105 Simple(String),
107 #[serde(rename_all = "camelCase")]
109 Explicit {
110 value: String,
112 format: TimestampFormatDto,
114 },
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119#[serde(rename_all = "snake_case")]
120pub enum TimestampFormatDto {
121 Iso8601,
122 UnixSeconds,
123 UnixMillis,
124 UnixNanos,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub struct ElementTemplateDto {
131 pub id: String,
133
134 pub labels: Vec<String>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub properties: Option<serde_json::Value>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub from: Option<String>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub to: Option<String>,
148}
149
150impl From<SourceMappingDto> for SourceMapping {
153 fn from(dto: SourceMappingDto) -> Self {
154 Self {
155 when: dto.when.map(Into::into),
156 operation: dto.operation.map(Into::into),
157 operation_from: dto.operation_from,
158 operation_map: dto
159 .operation_map
160 .map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect()),
161 element_type: dto.element_type.into(),
162 effective_from: dto.effective_from.map(Into::into),
163 template: dto.template.into(),
164 }
165 }
166}
167
168impl From<MappingConditionDto> for MappingCondition {
169 fn from(dto: MappingConditionDto) -> Self {
170 Self {
171 header: dto.header,
172 field: dto.field,
173 equals: dto.equals,
174 contains: dto.contains,
175 regex: dto.regex,
176 }
177 }
178}
179
180impl From<OperationTypeDto> for OperationType {
181 fn from(dto: OperationTypeDto) -> Self {
182 match dto {
183 OperationTypeDto::Insert => Self::Insert,
184 OperationTypeDto::Update => Self::Update,
185 OperationTypeDto::Delete => Self::Delete,
186 }
187 }
188}
189
190impl From<ElementTypeDto> for ElementType {
191 fn from(dto: ElementTypeDto) -> Self {
192 match dto {
193 ElementTypeDto::Node => Self::Node,
194 ElementTypeDto::Relation => Self::Relation,
195 }
196 }
197}
198
199impl From<EffectiveFromConfigDto> for EffectiveFromConfig {
200 fn from(dto: EffectiveFromConfigDto) -> Self {
201 match dto {
202 EffectiveFromConfigDto::Simple(s) => Self::Simple(s),
203 EffectiveFromConfigDto::Explicit { value, format } => Self::Explicit {
204 value,
205 format: format.into(),
206 },
207 }
208 }
209}
210
211impl From<TimestampFormatDto> for TimestampFormat {
212 fn from(dto: TimestampFormatDto) -> Self {
213 match dto {
214 TimestampFormatDto::Iso8601 => Self::Iso8601,
215 TimestampFormatDto::UnixSeconds => Self::UnixSeconds,
216 TimestampFormatDto::UnixMillis => Self::UnixMillis,
217 TimestampFormatDto::UnixNanos => Self::UnixNanos,
218 }
219 }
220}
221
222impl From<ElementTemplateDto> for ElementTemplate {
223 fn from(dto: ElementTemplateDto) -> Self {
224 Self {
225 id: dto.id,
226 labels: dto.labels,
227 properties: dto.properties,
228 from: dto.from,
229 to: dto.to,
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_dto_deserializes_camel_case() {
240 let json = r#"{
241 "elementType": "node",
242 "operation": "insert",
243 "operationFrom": "payload.action",
244 "operationMap": {
245 "created": "insert",
246 "deleted": "delete"
247 },
248 "effectiveFrom": {
249 "value": "{{payload.timestamp}}",
250 "format": "unix_millis"
251 },
252 "template": {
253 "id": "{{key}}",
254 "labels": ["Order"],
255 "properties": {"name": "{{payload.name}}"}
256 }
257 }"#;
258
259 let dto: SourceMappingDto = serde_json::from_str(json).unwrap();
260 assert_eq!(dto.element_type, ElementTypeDto::Node);
261 assert_eq!(dto.operation, Some(OperationTypeDto::Insert));
262 assert_eq!(dto.operation_from, Some("payload.action".to_string()));
263 assert!(dto.operation_map.is_some());
264
265 let runtime: SourceMapping = dto.into();
267 assert_eq!(runtime.element_type, ElementType::Node);
268 assert_eq!(runtime.operation, Some(OperationType::Insert));
269 }
270
271 #[test]
272 fn test_dto_with_condition() {
273 let json = r#"{
274 "when": {
275 "field": "payload.type",
276 "equals": "order"
277 },
278 "elementType": "node",
279 "operation": "insert",
280 "template": {
281 "id": "{{key}}",
282 "labels": ["Order"]
283 }
284 }"#;
285
286 let dto: SourceMappingDto = serde_json::from_str(json).unwrap();
287 let condition = dto.when.as_ref().unwrap();
288 assert_eq!(condition.field, Some("payload.type".to_string()));
289 assert_eq!(condition.equals, Some("order".to_string()));
290
291 let runtime: SourceMapping = dto.into();
292 let cond = runtime.when.unwrap();
293 assert_eq!(cond.field, Some("payload.type".to_string()));
294 }
295
296 #[test]
297 fn test_dto_simple_effective_from() {
298 let json = r#"{
299 "elementType": "node",
300 "operation": "update",
301 "effectiveFrom": "{{payload.ts}}",
302 "template": {
303 "id": "{{key}}",
304 "labels": ["Item"]
305 }
306 }"#;
307
308 let dto: SourceMappingDto = serde_json::from_str(json).unwrap();
309 assert_eq!(
310 dto.effective_from,
311 Some(EffectiveFromConfigDto::Simple("{{payload.ts}}".to_string()))
312 );
313
314 let runtime: SourceMapping = dto.into();
315 assert_eq!(
316 runtime.effective_from,
317 Some(EffectiveFromConfig::Simple("{{payload.ts}}".to_string()))
318 );
319 }
320}