1use anyhow::{anyhow, Context, Result};
21use drasi_core::models::{
22 Element, ElementMetadata, ElementPropertyMap, ElementReference, ElementValue,
23};
24use ordered_float::OrderedFloat;
25use serde_json::Value as JsonValue;
26use std::collections::HashMap;
27use std::sync::Arc;
28
29use crate::config::{ElementMappingConfig, ElementType, OperationType};
30use crate::pagination;
31use crate::template_engine::{TemplateContext, TemplateEngine};
32
33pub fn extract_items(body: &JsonValue, items_path: &str) -> Result<Vec<JsonValue>> {
35 let items_value = pagination::navigate_path(body, items_path).ok_or_else(|| {
37 anyhow!("Items path '{items_path}' did not resolve to a value in response")
38 })?;
39
40 match items_value {
41 JsonValue::Array(arr) => Ok(arr.clone()),
42 other if other.is_object() => Ok(vec![other.clone()]),
44 _ => Err(anyhow!(
45 "Items path '{items_path}' did not resolve to an array or object"
46 )),
47 }
48}
49
50pub enum MappedChange {
52 Upsert {
54 element: Element,
55 operation: OperationType,
56 },
57 Delete { metadata: ElementMetadata },
59}
60
61pub fn map_items_to_elements(
63 items: &[JsonValue],
64 mappings: &[ElementMappingConfig],
65 source_id: &str,
66 engine: &TemplateEngine,
67) -> Vec<Result<MappedChange>> {
68 let mut elements = Vec::new();
69
70 for (index, item) in items.iter().enumerate() {
71 let context = TemplateContext {
72 item: item.clone(),
73 index,
74 source_id: source_id.to_string(),
75 };
76
77 for mapping in mappings {
78 let result = map_single_item(&context, mapping, source_id, engine);
79 elements.push(result);
80 }
81 }
82
83 elements
84}
85
86fn map_single_item(
88 context: &TemplateContext,
89 mapping: &ElementMappingConfig,
90 source_id: &str,
91 engine: &TemplateEngine,
92) -> Result<MappedChange> {
93 let template = &mapping.template;
94 let is_delete = mapping.operation == OperationType::Delete;
95
96 let id = engine
98 .render_string(&template.id, context)
99 .context("Failed to render element ID template")?;
100
101 if id.is_empty() {
102 return Err(anyhow!("Element ID rendered to empty string"));
103 }
104
105 let mut labels = Vec::new();
107 for label_template in &template.labels {
108 let label = engine
109 .render_string(label_template, context)
110 .context("Failed to render label template")?;
111 if !label.is_empty() {
112 labels.push(Arc::from(label.as_str()));
113 }
114 }
115
116 if is_delete {
119 let metadata = ElementMetadata {
120 reference: ElementReference::new(source_id, &id),
121 labels: labels.into(),
122 effective_from: 0,
123 };
124 return Ok(MappedChange::Delete { metadata });
125 }
126
127 let properties = if let Some(ref props) = template.properties {
129 let rendered = engine
130 .render_properties(props, context)
131 .context("Failed to render properties")?;
132 json_map_to_element_properties(&rendered)
133 } else {
134 ElementPropertyMap::new()
135 };
136
137 let element = match mapping.element_type {
139 ElementType::Node => {
140 let metadata = ElementMetadata {
141 reference: ElementReference::new(source_id, &id),
142 labels: labels.into(),
143 effective_from: 0,
144 };
145 Element::Node {
146 metadata,
147 properties,
148 }
149 }
150 ElementType::Relation => {
151 let from_id = template
152 .from
153 .as_ref()
154 .ok_or_else(|| anyhow!("Relation mapping requires 'from' template"))?;
155 let to_id = template
156 .to
157 .as_ref()
158 .ok_or_else(|| anyhow!("Relation mapping requires 'to' template"))?;
159
160 let from_rendered = engine
161 .render_string(from_id, context)
162 .context("Failed to render 'from' template")?;
163 let to_rendered = engine
164 .render_string(to_id, context)
165 .context("Failed to render 'to' template")?;
166
167 let metadata = ElementMetadata {
168 reference: ElementReference::new(source_id, &id),
169 labels: labels.into(),
170 effective_from: 0,
171 };
172
173 Element::Relation {
174 metadata,
175 properties,
176 in_node: ElementReference::new(source_id, &from_rendered),
177 out_node: ElementReference::new(source_id, &to_rendered),
178 }
179 }
180 };
181
182 Ok(MappedChange::Upsert {
183 element,
184 operation: mapping.operation.clone(),
185 })
186}
187
188fn json_map_to_element_properties(map: &HashMap<String, JsonValue>) -> ElementPropertyMap {
190 let mut props = ElementPropertyMap::new();
191 for (key, value) in map {
192 if let Some(elem_value) = json_value_to_element_value(value) {
193 props.insert(key.as_str(), elem_value);
194 }
195 }
196 props
197}
198
199fn json_value_to_element_value(value: &JsonValue) -> Option<ElementValue> {
201 match value {
202 JsonValue::Null => Some(ElementValue::Null),
203 JsonValue::Bool(b) => Some(ElementValue::Bool(*b)),
204 JsonValue::Number(n) => {
205 if let Some(i) = n.as_i64() {
206 Some(ElementValue::Integer(i))
207 } else {
208 n.as_f64().map(|f| ElementValue::Float(OrderedFloat(f)))
209 }
210 }
211 JsonValue::String(s) => Some(ElementValue::String(s.clone().into())),
212 JsonValue::Array(arr) => {
213 let elements: Vec<ElementValue> =
214 arr.iter().filter_map(json_value_to_element_value).collect();
215 Some(ElementValue::List(elements))
216 }
217 JsonValue::Object(map) => {
218 let mut props = ElementPropertyMap::new();
219 for (key, v) in map {
220 if let Some(ev) = json_value_to_element_value(v) {
221 props.insert(key.as_str(), ev);
222 }
223 }
224 Some(ElementValue::Object(props))
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use serde_json::json;
233
234 #[test]
235 fn test_extract_items_top_level_array() {
236 let body = json!([{"id": "1"}, {"id": "2"}]);
237 let items = extract_items(&body, "$").unwrap();
238 assert_eq!(items.len(), 2);
239 }
240
241 #[test]
242 fn test_extract_items_nested_path() {
243 let body = json!({"data": [{"id": "1"}, {"id": "2"}]});
244 let items = extract_items(&body, "$.data").unwrap();
245 assert_eq!(items.len(), 2);
246 }
247
248 #[test]
249 fn test_extract_items_deep_nested() {
250 let body = json!({"response": {"results": [{"id": "1"}]}});
251 let items = extract_items(&body, "$.response.results").unwrap();
252 assert_eq!(items.len(), 1);
253 }
254
255 #[test]
256 fn test_map_items_to_nodes() {
257 let items = vec![
258 json!({"id": "1", "name": "Alice"}),
259 json!({"id": "2", "name": "Bob"}),
260 ];
261
262 let mappings = vec![ElementMappingConfig {
263 element_type: ElementType::Node,
264 operation: Default::default(),
265 template: crate::config::ElementTemplate {
266 id: "{{item.id}}".to_string(),
267 labels: vec!["User".to_string()],
268 properties: Some(json!({"name": "{{item.name}}"})),
269 from: None,
270 to: None,
271 },
272 }];
273
274 let engine = TemplateEngine::new();
275 let results = map_items_to_elements(&items, &mappings, "test-source", &engine);
276 assert_eq!(results.len(), 2);
277
278 let mapped = results[0].as_ref().unwrap();
279 match mapped {
280 MappedChange::Upsert { element, .. } => match element {
281 Element::Node { metadata, .. } => {
282 assert_eq!(&*metadata.reference.element_id, "1");
283 assert_eq!(metadata.labels.len(), 1);
284 assert_eq!(&*metadata.labels[0], "User");
285 }
286 _ => panic!("Expected Node"),
287 },
288 _ => panic!("Expected Upsert"),
289 }
290 }
291
292 #[test]
293 fn test_map_items_to_relations() {
294 let items = vec![json!({"id": "r1", "from": "n1", "to": "n2", "type": "KNOWS"})];
295
296 let mappings = vec![ElementMappingConfig {
297 element_type: ElementType::Relation,
298 operation: Default::default(),
299 template: crate::config::ElementTemplate {
300 id: "{{item.id}}".to_string(),
301 labels: vec!["{{item.type}}".to_string()],
302 properties: None,
303 from: Some("{{item.from}}".to_string()),
304 to: Some("{{item.to}}".to_string()),
305 },
306 }];
307
308 let engine = TemplateEngine::new();
309 let results = map_items_to_elements(&items, &mappings, "test-source", &engine);
310 assert_eq!(results.len(), 1);
311
312 let mapped = results[0].as_ref().unwrap();
313 match mapped {
314 MappedChange::Upsert { element, .. } => match element {
315 Element::Relation {
316 metadata,
317 in_node,
318 out_node,
319 ..
320 } => {
321 assert_eq!(&*metadata.reference.element_id, "r1");
322 assert_eq!(&*in_node.element_id, "n1");
323 assert_eq!(&*out_node.element_id, "n2");
324 }
325 _ => panic!("Expected Relation"),
326 },
327 _ => panic!("Expected Upsert"),
328 }
329 }
330}