use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::config::{
EffectiveFromConfig, ElementTemplate, ElementType, MappingCondition, OperationType,
SourceMapping, TimestampFormat,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SourceMappingDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<MappingConditionDto>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation: Option<OperationTypeDto>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_map: Option<HashMap<String, OperationTypeDto>>,
pub element_type: ElementTypeDto,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_from: Option<EffectiveFromConfigDto>,
pub template: ElementTemplateDto,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MappingConditionDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub header: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equals: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contains: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub regex: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum OperationTypeDto {
Insert,
Update,
Delete,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ElementTypeDto {
Node,
Relation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum EffectiveFromConfigDto {
Simple(String),
#[serde(rename_all = "camelCase")]
Explicit {
value: String,
format: TimestampFormatDto,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TimestampFormatDto {
Iso8601,
UnixSeconds,
UnixMillis,
UnixNanos,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ElementTemplateDto {
pub id: String,
pub labels: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub properties: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
}
impl From<SourceMappingDto> for SourceMapping {
fn from(dto: SourceMappingDto) -> Self {
Self {
when: dto.when.map(Into::into),
operation: dto.operation.map(Into::into),
operation_from: dto.operation_from,
operation_map: dto
.operation_map
.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect()),
element_type: dto.element_type.into(),
effective_from: dto.effective_from.map(Into::into),
template: dto.template.into(),
}
}
}
impl From<MappingConditionDto> for MappingCondition {
fn from(dto: MappingConditionDto) -> Self {
Self {
header: dto.header,
field: dto.field,
equals: dto.equals,
contains: dto.contains,
regex: dto.regex,
}
}
}
impl From<OperationTypeDto> for OperationType {
fn from(dto: OperationTypeDto) -> Self {
match dto {
OperationTypeDto::Insert => Self::Insert,
OperationTypeDto::Update => Self::Update,
OperationTypeDto::Delete => Self::Delete,
}
}
}
impl From<ElementTypeDto> for ElementType {
fn from(dto: ElementTypeDto) -> Self {
match dto {
ElementTypeDto::Node => Self::Node,
ElementTypeDto::Relation => Self::Relation,
}
}
}
impl From<EffectiveFromConfigDto> for EffectiveFromConfig {
fn from(dto: EffectiveFromConfigDto) -> Self {
match dto {
EffectiveFromConfigDto::Simple(s) => Self::Simple(s),
EffectiveFromConfigDto::Explicit { value, format } => Self::Explicit {
value,
format: format.into(),
},
}
}
}
impl From<TimestampFormatDto> for TimestampFormat {
fn from(dto: TimestampFormatDto) -> Self {
match dto {
TimestampFormatDto::Iso8601 => Self::Iso8601,
TimestampFormatDto::UnixSeconds => Self::UnixSeconds,
TimestampFormatDto::UnixMillis => Self::UnixMillis,
TimestampFormatDto::UnixNanos => Self::UnixNanos,
}
}
}
impl From<ElementTemplateDto> for ElementTemplate {
fn from(dto: ElementTemplateDto) -> Self {
Self {
id: dto.id,
labels: dto.labels,
properties: dto.properties,
from: dto.from,
to: dto.to,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dto_deserializes_camel_case() {
let json = r#"{
"elementType": "node",
"operation": "insert",
"operationFrom": "payload.action",
"operationMap": {
"created": "insert",
"deleted": "delete"
},
"effectiveFrom": {
"value": "{{payload.timestamp}}",
"format": "unix_millis"
},
"template": {
"id": "{{key}}",
"labels": ["Order"],
"properties": {"name": "{{payload.name}}"}
}
}"#;
let dto: SourceMappingDto = serde_json::from_str(json).unwrap();
assert_eq!(dto.element_type, ElementTypeDto::Node);
assert_eq!(dto.operation, Some(OperationTypeDto::Insert));
assert_eq!(dto.operation_from, Some("payload.action".to_string()));
assert!(dto.operation_map.is_some());
let runtime: SourceMapping = dto.into();
assert_eq!(runtime.element_type, ElementType::Node);
assert_eq!(runtime.operation, Some(OperationType::Insert));
}
#[test]
fn test_dto_with_condition() {
let json = r#"{
"when": {
"field": "payload.type",
"equals": "order"
},
"elementType": "node",
"operation": "insert",
"template": {
"id": "{{key}}",
"labels": ["Order"]
}
}"#;
let dto: SourceMappingDto = serde_json::from_str(json).unwrap();
let condition = dto.when.as_ref().unwrap();
assert_eq!(condition.field, Some("payload.type".to_string()));
assert_eq!(condition.equals, Some("order".to_string()));
let runtime: SourceMapping = dto.into();
let cond = runtime.when.unwrap();
assert_eq!(cond.field, Some("payload.type".to_string()));
}
#[test]
fn test_dto_simple_effective_from() {
let json = r#"{
"elementType": "node",
"operation": "update",
"effectiveFrom": "{{payload.ts}}",
"template": {
"id": "{{key}}",
"labels": ["Item"]
}
}"#;
let dto: SourceMappingDto = serde_json::from_str(json).unwrap();
assert_eq!(
dto.effective_from,
Some(EffectiveFromConfigDto::Simple("{{payload.ts}}".to_string()))
);
let runtime: SourceMapping = dto.into();
assert_eq!(
runtime.effective_from,
Some(EffectiveFromConfig::Simple("{{payload.ts}}".to_string()))
);
}
}