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};
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 fn map_items_to_elements(
52 items: &[JsonValue],
53 mappings: &[ElementMappingConfig],
54 source_id: &str,
55 engine: &TemplateEngine,
56) -> Vec<Result<Element>> {
57 let mut elements = Vec::new();
58
59 for (index, item) in items.iter().enumerate() {
60 let context = TemplateContext {
61 item: item.clone(),
62 index,
63 source_id: source_id.to_string(),
64 };
65
66 for mapping in mappings {
67 let result = map_single_item(&context, mapping, source_id, engine);
68 elements.push(result);
69 }
70 }
71
72 elements
73}
74
75fn map_single_item(
77 context: &TemplateContext,
78 mapping: &ElementMappingConfig,
79 source_id: &str,
80 engine: &TemplateEngine,
81) -> Result<Element> {
82 let template = &mapping.template;
83
84 let id = engine
86 .render_string(&template.id, context)
87 .context("Failed to render element ID template")?;
88
89 if id.is_empty() {
90 return Err(anyhow!("Element ID rendered to empty string"));
91 }
92
93 let mut labels = Vec::new();
95 for label_template in &template.labels {
96 let label = engine
97 .render_string(label_template, context)
98 .context("Failed to render label template")?;
99 if !label.is_empty() {
100 labels.push(Arc::from(label.as_str()));
101 }
102 }
103
104 let properties = if let Some(ref props) = template.properties {
106 let rendered = engine
107 .render_properties(props, context)
108 .context("Failed to render properties")?;
109 json_map_to_element_properties(&rendered)
110 } else {
111 ElementPropertyMap::new()
112 };
113
114 match mapping.element_type {
116 ElementType::Node => {
117 let metadata = ElementMetadata {
118 reference: ElementReference::new(source_id, &id),
119 labels: labels.into(),
120 effective_from: 0,
121 };
122 Ok(Element::Node {
123 metadata,
124 properties,
125 })
126 }
127 ElementType::Relation => {
128 let from_id = template
129 .from
130 .as_ref()
131 .ok_or_else(|| anyhow!("Relation mapping requires 'from' template"))?;
132 let to_id = template
133 .to
134 .as_ref()
135 .ok_or_else(|| anyhow!("Relation mapping requires 'to' template"))?;
136
137 let from_rendered = engine
138 .render_string(from_id, context)
139 .context("Failed to render 'from' template")?;
140 let to_rendered = engine
141 .render_string(to_id, context)
142 .context("Failed to render 'to' template")?;
143
144 let metadata = ElementMetadata {
145 reference: ElementReference::new(source_id, &id),
146 labels: labels.into(),
147 effective_from: 0,
148 };
149
150 Ok(Element::Relation {
151 metadata,
152 properties,
153 in_node: ElementReference::new(source_id, &from_rendered),
154 out_node: ElementReference::new(source_id, &to_rendered),
155 })
156 }
157 }
158}
159
160fn json_map_to_element_properties(map: &HashMap<String, JsonValue>) -> ElementPropertyMap {
162 let mut props = ElementPropertyMap::new();
163 for (key, value) in map {
164 if let Some(elem_value) = json_value_to_element_value(value) {
165 props.insert(key.as_str(), elem_value);
166 }
167 }
168 props
169}
170
171fn json_value_to_element_value(value: &JsonValue) -> Option<ElementValue> {
173 match value {
174 JsonValue::Null => Some(ElementValue::Null),
175 JsonValue::Bool(b) => Some(ElementValue::Bool(*b)),
176 JsonValue::Number(n) => {
177 if let Some(i) = n.as_i64() {
178 Some(ElementValue::Integer(i))
179 } else {
180 n.as_f64().map(|f| ElementValue::Float(OrderedFloat(f)))
181 }
182 }
183 JsonValue::String(s) => Some(ElementValue::String(s.clone().into())),
184 JsonValue::Array(arr) => {
185 let elements: Vec<ElementValue> =
186 arr.iter().filter_map(json_value_to_element_value).collect();
187 Some(ElementValue::List(elements))
188 }
189 JsonValue::Object(map) => {
190 let mut props = ElementPropertyMap::new();
191 for (key, v) in map {
192 if let Some(ev) = json_value_to_element_value(v) {
193 props.insert(key.as_str(), ev);
194 }
195 }
196 Some(ElementValue::Object(props))
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use serde_json::json;
205
206 #[test]
207 fn test_extract_items_top_level_array() {
208 let body = json!([{"id": "1"}, {"id": "2"}]);
209 let items = extract_items(&body, "$").unwrap();
210 assert_eq!(items.len(), 2);
211 }
212
213 #[test]
214 fn test_extract_items_nested_path() {
215 let body = json!({"data": [{"id": "1"}, {"id": "2"}]});
216 let items = extract_items(&body, "$.data").unwrap();
217 assert_eq!(items.len(), 2);
218 }
219
220 #[test]
221 fn test_extract_items_deep_nested() {
222 let body = json!({"response": {"results": [{"id": "1"}]}});
223 let items = extract_items(&body, "$.response.results").unwrap();
224 assert_eq!(items.len(), 1);
225 }
226
227 #[test]
228 fn test_map_items_to_nodes() {
229 let items = vec![
230 json!({"id": "1", "name": "Alice"}),
231 json!({"id": "2", "name": "Bob"}),
232 ];
233
234 let mappings = vec![ElementMappingConfig {
235 element_type: ElementType::Node,
236 template: crate::config::ElementTemplate {
237 id: "{{item.id}}".to_string(),
238 labels: vec!["User".to_string()],
239 properties: Some(json!({"name": "{{item.name}}"})),
240 from: None,
241 to: None,
242 },
243 }];
244
245 let engine = TemplateEngine::new();
246 let results = map_items_to_elements(&items, &mappings, "test-source", &engine);
247 assert_eq!(results.len(), 2);
248
249 let elem = results[0].as_ref().unwrap();
250 match elem {
251 Element::Node { metadata, .. } => {
252 assert_eq!(&*metadata.reference.element_id, "1");
253 assert_eq!(metadata.labels.len(), 1);
254 assert_eq!(&*metadata.labels[0], "User");
255 }
256 _ => panic!("Expected Node"),
257 }
258 }
259
260 #[test]
261 fn test_map_items_to_relations() {
262 let items = vec![json!({"id": "r1", "from": "n1", "to": "n2", "type": "KNOWS"})];
263
264 let mappings = vec![ElementMappingConfig {
265 element_type: ElementType::Relation,
266 template: crate::config::ElementTemplate {
267 id: "{{item.id}}".to_string(),
268 labels: vec!["{{item.type}}".to_string()],
269 properties: None,
270 from: Some("{{item.from}}".to_string()),
271 to: Some("{{item.to}}".to_string()),
272 },
273 }];
274
275 let engine = TemplateEngine::new();
276 let results = map_items_to_elements(&items, &mappings, "test-source", &engine);
277 assert_eq!(results.len(), 1);
278
279 let elem = results[0].as_ref().unwrap();
280 match elem {
281 Element::Relation {
282 metadata,
283 in_node,
284 out_node,
285 ..
286 } => {
287 assert_eq!(&*metadata.reference.element_id, "r1");
288 assert_eq!(&*in_node.element_id, "n1");
289 assert_eq!(&*out_node.element_id, "n2");
290 }
291 _ => panic!("Expected Relation"),
292 }
293 }
294}