Skip to main content

drasi_source_mapping/
config.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Configuration types for source payload mapping.
16
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20/// Mapping configuration from source payload to graph change event.
21///
22/// Defines how to transform an incoming payload into a `SourceChange`.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub struct SourceMapping {
25    /// Optional condition for when this mapping applies
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub when: Option<MappingCondition>,
28
29    /// Static operation type
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub operation: Option<OperationType>,
32
33    /// Path to extract operation from context (e.g., "payload.action")
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub operation_from: Option<String>,
36
37    /// Mapping from extracted values to operation types
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub operation_map: Option<HashMap<String, OperationType>>,
40
41    /// Element type to create
42    pub element_type: ElementType,
43
44    /// Timestamp configuration for effective_from
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub effective_from: Option<EffectiveFromConfig>,
47
48    /// Template for element creation
49    pub template: ElementTemplate,
50}
51
52/// Condition for when a mapping applies
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54pub struct MappingCondition {
55    /// Header to check (HTTP-specific, but included for compatibility)
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub header: Option<String>,
58
59    /// Payload field path to check (dot notation)
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub field: Option<String>,
62
63    /// Value must equal this
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub equals: Option<String>,
66
67    /// Value must contain this
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub contains: Option<String>,
70
71    /// Value must match this regex
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub regex: Option<String>,
74}
75
76/// Operation type for source changes
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78#[serde(rename_all = "lowercase")]
79pub enum OperationType {
80    Insert,
81    Update,
82    Delete,
83}
84
85/// Element type for source changes
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87#[serde(rename_all = "lowercase")]
88pub enum ElementType {
89    Node,
90    Relation,
91}
92
93/// Configuration for effective_from timestamp
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[serde(untagged)]
96pub enum EffectiveFromConfig {
97    /// Simple template string (auto-detect format)
98    Simple(String),
99    /// Explicit configuration with format
100    Explicit {
101        /// Template for the timestamp value
102        value: String,
103        /// Format of the timestamp
104        format: TimestampFormat,
105    },
106}
107
108/// Timestamp format for effective_from
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
110#[serde(rename_all = "snake_case")]
111pub enum TimestampFormat {
112    /// ISO 8601 datetime string
113    Iso8601,
114    /// Unix timestamp in seconds
115    UnixSeconds,
116    /// Unix timestamp in milliseconds
117    UnixMillis,
118    /// Unix timestamp in nanoseconds
119    UnixNanos,
120}
121
122/// Template for element creation
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct ElementTemplate {
125    /// Template for element ID
126    pub id: String,
127
128    /// Templates for element labels
129    pub labels: Vec<String>,
130
131    /// Templates for element properties (can be individual templates or a single object template)
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub properties: Option<serde_json::Value>,
134
135    /// Template for relation source node ID (relations only)
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub from: Option<String>,
138
139    /// Template for relation target node ID (relations only)
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub to: Option<String>,
142}
143
144impl SourceMapping {
145    /// Validate mapping configuration
146    pub fn validate(&self) -> anyhow::Result<()> {
147        // Must have either static operation or dynamic operation_from
148        if self.operation.is_none() && self.operation_from.is_none() {
149            return Err(anyhow::anyhow!(
150                "either 'operation' or 'operation_from' must be specified"
151            ));
152        }
153
154        // If using operation_from, should have operation_map
155        if self.operation_from.is_some() && self.operation_map.is_none() {
156            return Err(anyhow::anyhow!(
157                "'operation_map' is required when using 'operation_from'"
158            ));
159        }
160
161        // Validate template
162        self.template.validate(&self.element_type)?;
163
164        Ok(())
165    }
166}
167
168impl ElementTemplate {
169    /// Validate element template configuration
170    pub fn validate(&self, element_type: &ElementType) -> anyhow::Result<()> {
171        if self.id.is_empty() {
172            return Err(anyhow::anyhow!("template.id cannot be empty"));
173        }
174
175        if self.labels.is_empty() {
176            return Err(anyhow::anyhow!("template.labels cannot be empty"));
177        }
178
179        // Relations require from and to
180        if *element_type == ElementType::Relation {
181            if self.from.is_none() {
182                return Err(anyhow::anyhow!(
183                    "template.from is required for relation elements"
184                ));
185            }
186            if self.to.is_none() {
187                return Err(anyhow::anyhow!(
188                    "template.to is required for relation elements"
189                ));
190            }
191        }
192
193        Ok(())
194    }
195}