dataflow_rs/engine/functions/
publish.rs1use crate::engine::error::{DataflowError, Result};
9use crate::engine::message::{Change, Message};
10use crate::engine::task_outcome::TaskOutcome;
11use crate::engine::utils::{get_nested_value, get_nested_value_parts, set_nested_value_parts};
12use datavalue::OwnedDataValue;
13use log::debug;
14use serde::Deserialize;
15use serde_json::Value;
16use std::sync::Arc;
17
18#[derive(Debug, Clone, Deserialize)]
20pub struct PublishConfig {
21 pub source: String,
23
24 pub target: String,
26
27 #[serde(default)]
29 pub pretty: bool,
30
31 #[serde(default = "default_root_element")]
33 pub root_element: String,
34
35 #[doc(hidden)]
40 #[serde(skip)]
41 pub target_path_arc: Arc<str>,
42
43 #[doc(hidden)]
47 #[serde(skip)]
48 pub target_path_parts: Arc<[Arc<str>]>,
49}
50
51impl Default for PublishConfig {
54 fn default() -> Self {
55 Self {
56 source: String::new(),
57 target: String::new(),
58 pretty: false,
59 root_element: default_root_element(),
60 target_path_arc: Arc::from(""),
61 target_path_parts: Vec::new().into(),
62 }
63 }
64}
65
66fn default_root_element() -> String {
67 "root".to_string()
68}
69
70impl PublishConfig {
71 pub fn from_json(input: &Value) -> Result<Self> {
72 let source = input
73 .get("source")
74 .and_then(Value::as_str)
75 .ok_or_else(|| {
76 DataflowError::Validation("Missing 'source' in publish config".to_string())
77 })?
78 .to_string();
79
80 let target = input
81 .get("target")
82 .and_then(Value::as_str)
83 .ok_or_else(|| {
84 DataflowError::Validation("Missing 'target' in publish config".to_string())
85 })?
86 .to_string();
87
88 let pretty = input
89 .get("pretty")
90 .and_then(Value::as_bool)
91 .unwrap_or(false);
92
93 let root_element = input
94 .get("root_element")
95 .and_then(Value::as_str)
96 .map(String::from)
97 .unwrap_or_else(default_root_element);
98
99 let mut config = PublishConfig {
100 source,
101 target,
102 pretty,
103 root_element,
104 ..Default::default()
105 };
106 config.precompute_target_path();
107 Ok(config)
108 }
109
110 pub(crate) fn precompute_target_path(&mut self) {
113 let path = format!("data.{}", self.target);
114 let parts: Vec<Arc<str>> = path.split('.').map(Arc::from).collect();
115 self.target_path_parts = parts.into();
116 self.target_path_arc = Arc::from(path);
117 }
118
119 fn resolve_target_path(&self) -> (Arc<str>, Arc<[Arc<str>]>) {
123 if self.target_path_parts.is_empty() {
124 let path = format!("data.{}", self.target);
125 let parts: Vec<Arc<str>> = path.split('.').map(Arc::from).collect();
126 (Arc::from(path), parts.into())
127 } else {
128 (
129 Arc::clone(&self.target_path_arc),
130 Arc::clone(&self.target_path_parts),
131 )
132 }
133 }
134
135 fn resolve_source<'m>(&self, message: &'m Message) -> Option<&'m OwnedDataValue> {
140 if let Some(value) = message.data().get(&self.source) {
143 return Some(value);
144 }
145
146 if let Some(value) = get_nested_value(message.data(), &self.source) {
148 return Some(value);
149 }
150
151 if let Some(path) = self.source.strip_prefix("data.") {
153 return get_nested_value(message.data(), path);
154 }
155
156 None
157 }
158}
159
160pub fn execute_publish_json(
163 message: &mut Message,
164 config: &PublishConfig,
165) -> Result<(TaskOutcome, Vec<Change>)> {
166 debug!(
167 "PublishJson: Serializing 'data.{}' to 'data.{}'",
168 config.source, config.target
169 );
170
171 let source_data = match config.resolve_source(message) {
174 Some(v) if !matches!(v, OwnedDataValue::Null) => v,
175 _ => {
176 return Err(DataflowError::Validation(format!(
177 "PublishJson: Source 'data.{}' not found or is null",
178 config.source
179 )));
180 }
181 };
182
183 let json_string = if config.pretty {
189 let bridge = Value::from(source_data);
190 serde_json::to_string_pretty(&bridge)
191 .map_err(|e| DataflowError::Validation(format!("Failed to serialize to JSON: {}", e)))?
192 } else {
193 source_data.to_json_string()
194 };
195
196 let (target_path_arc, target_parts) = config.resolve_target_path();
197 let old_value = get_nested_value_parts(&message.context, &target_parts)
198 .cloned()
199 .unwrap_or(OwnedDataValue::Null);
200 let new_value = OwnedDataValue::String(json_string);
201
202 set_nested_value_parts(&mut message.context, &target_parts, new_value.clone());
203
204 Ok((
205 TaskOutcome::Success,
206 vec![Change {
207 path: target_path_arc,
208 old_value,
209 new_value,
210 }],
211 ))
212}
213
214pub fn execute_publish_xml(
218 message: &mut Message,
219 config: &PublishConfig,
220) -> Result<(TaskOutcome, Vec<Change>)> {
221 debug!(
222 "PublishXml: Serializing 'data.{}' to 'data.{}'",
223 config.source, config.target
224 );
225
226 let source_data = match config.resolve_source(message) {
229 Some(v) if !matches!(v, OwnedDataValue::Null) => v,
230 _ => {
231 return Err(DataflowError::Validation(format!(
232 "PublishXml: Source 'data.{}' not found or is null",
233 config.source
234 )));
235 }
236 };
237
238 let bridge = Value::from(source_data);
239 let xml_string = json_to_xml(&bridge, &config.root_element)?;
240
241 let (target_path_arc, target_parts) = config.resolve_target_path();
242 let old_value = get_nested_value_parts(&message.context, &target_parts)
243 .cloned()
244 .unwrap_or(OwnedDataValue::Null);
245 let new_value = OwnedDataValue::String(xml_string);
246
247 set_nested_value_parts(&mut message.context, &target_parts, new_value.clone());
248
249 Ok((
250 TaskOutcome::Success,
251 vec![Change {
252 path: target_path_arc,
253 old_value,
254 new_value,
255 }],
256 ))
257}
258
259fn json_to_xml(value: &Value, root_element: &str) -> Result<String> {
263 let mut buffer = String::new();
264
265 match value {
266 Value::Object(_) => {
267 buffer.push_str(&format!("<{}>", root_element));
268 let content = serialize_value_to_xml_content(value)?;
269 buffer.push_str(&content);
270 buffer.push_str(&format!("</{}>", root_element));
271 }
272 Value::Array(arr) => {
273 buffer.push_str(&format!("<{}>", root_element));
274 for item in arr {
275 buffer.push_str("<item>");
276 let content = serialize_value_to_xml_content(item)?;
277 buffer.push_str(&content);
278 buffer.push_str("</item>");
279 }
280 buffer.push_str(&format!("</{}>", root_element));
281 }
282 _ => {
283 buffer.push_str(&format!("<{}>", root_element));
284 buffer.push_str(&value_to_xml_string(value));
285 buffer.push_str(&format!("</{}>", root_element));
286 }
287 }
288
289 Ok(buffer)
290}
291
292fn serialize_value_to_xml_content(value: &Value) -> Result<String> {
293 let mut result = String::new();
294
295 match value {
296 Value::Object(map) => {
297 for (key, val) in map {
298 let safe_key = sanitize_xml_name(key);
299 result.push_str(&format!("<{}>", safe_key));
300 match val {
301 Value::Object(_) | Value::Array(_) => {
302 result.push_str(&serialize_value_to_xml_content(val)?);
303 }
304 _ => {
305 result.push_str(&value_to_xml_string(val));
306 }
307 }
308 result.push_str(&format!("</{}>", safe_key));
309 }
310 }
311 Value::Array(arr) => {
312 for item in arr {
313 result.push_str("<item>");
314 match item {
315 Value::Object(_) | Value::Array(_) => {
316 result.push_str(&serialize_value_to_xml_content(item)?);
317 }
318 _ => {
319 result.push_str(&value_to_xml_string(item));
320 }
321 }
322 result.push_str("</item>");
323 }
324 }
325 _ => {
326 result.push_str(&value_to_xml_string(value));
327 }
328 }
329
330 Ok(result)
331}
332
333fn value_to_xml_string(value: &Value) -> String {
334 match value {
335 Value::Null => String::new(),
336 Value::Bool(b) => b.to_string(),
337 Value::Number(n) => n.to_string(),
338 Value::String(s) => escape_xml(s),
339 _ => String::new(),
340 }
341}
342
343fn escape_xml(s: &str) -> String {
344 s.replace('&', "&")
345 .replace('<', "<")
346 .replace('>', ">")
347 .replace('"', """)
348 .replace('\'', "'")
349}
350
351fn sanitize_xml_name(name: &str) -> String {
352 let mut result = String::new();
353
354 for (i, c) in name.chars().enumerate() {
355 if i == 0 {
356 if c.is_ascii_alphabetic() || c == '_' {
357 result.push(c);
358 } else {
359 result.push('_');
360 if c.is_ascii_alphanumeric() {
361 result.push(c);
362 }
363 }
364 } else if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
365 result.push(c);
366 } else {
367 result.push('_');
368 }
369 }
370
371 if result.is_empty() {
372 result = "_element".to_string();
373 }
374
375 result
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::engine::utils::set_nested_value;
382 use serde_json::json;
383
384 fn dv(v: serde_json::Value) -> OwnedDataValue {
385 OwnedDataValue::from(&v)
386 }
387
388 fn message_with_data(initial: serde_json::Value) -> Message {
389 let mut m = Message::new(Arc::new(dv(json!({}))));
390 set_nested_value(&mut m.context, "data", dv(initial));
391 m
392 }
393
394 #[test]
395 fn test_publish_config_from_json() {
396 let input = json!({"source": "output", "target": "json_string"});
397 let config = PublishConfig::from_json(&input).unwrap();
398 assert_eq!(config.source, "output");
399 assert_eq!(config.target, "json_string");
400 assert!(!config.pretty);
401 assert_eq!(config.root_element, "root");
402 }
403
404 #[test]
405 fn test_publish_config_with_options() {
406 let input = json!({
407 "source": "data",
408 "target": "xml_output",
409 "pretty": true,
410 "root_element": "document"
411 });
412
413 let config = PublishConfig::from_json(&input).unwrap();
414 assert_eq!(config.source, "data");
415 assert_eq!(config.target, "xml_output");
416 assert!(config.pretty);
417 assert_eq!(config.root_element, "document");
418 }
419
420 #[test]
421 fn test_publish_config_missing_source() {
422 assert!(PublishConfig::from_json(&json!({"target": "output"})).is_err());
423 }
424
425 #[test]
426 fn test_publish_config_missing_target() {
427 assert!(PublishConfig::from_json(&json!({"source": "input"})).is_err());
428 }
429
430 #[test]
431 fn test_execute_publish_json() {
432 let mut message = message_with_data(json!({"user": {"name": "John", "age": 30}}));
433
434 let config = PublishConfig {
435 source: "user".to_string(),
436 target: "user_json".to_string(),
437 pretty: false,
438 root_element: "root".to_string(),
439 ..Default::default()
440 };
441
442 let result = execute_publish_json(&mut message, &config);
443 assert!(result.is_ok());
444
445 let (outcome, changes) = result.unwrap();
446 assert_eq!(outcome, TaskOutcome::Success);
447 assert_eq!(changes.len(), 1);
448
449 let json_string = message.data()["user_json"].as_str().unwrap();
450 assert!(json_string.contains("John"));
451 assert!(json_string.contains("30"));
452 }
453
454 #[test]
455 fn test_execute_publish_json_pretty() {
456 let mut message = message_with_data(json!({"user": {"name": "Alice"}}));
457
458 let config = PublishConfig {
459 source: "user".to_string(),
460 target: "output".to_string(),
461 pretty: true,
462 root_element: "root".to_string(),
463 ..Default::default()
464 };
465
466 let result = execute_publish_json(&mut message, &config);
467 assert!(result.is_ok());
468
469 let json_string = message.data()["output"].as_str().unwrap();
470 assert!(json_string.contains('\n'));
471 }
472
473 #[test]
474 fn test_execute_publish_json_not_found() {
475 let mut message = Message::new(Arc::new(dv(json!({}))));
476
477 let config = PublishConfig {
478 source: "nonexistent".to_string(),
479 target: "output".to_string(),
480 pretty: false,
481 root_element: "root".to_string(),
482 ..Default::default()
483 };
484
485 assert!(execute_publish_json(&mut message, &config).is_err());
486 }
487
488 #[test]
489 fn test_execute_publish_xml() {
490 let mut message = message_with_data(json!({"user": {"name": "John", "age": 30}}));
491
492 let config = PublishConfig {
493 source: "user".to_string(),
494 target: "user_xml".to_string(),
495 pretty: false,
496 root_element: "user".to_string(),
497 ..Default::default()
498 };
499
500 let result = execute_publish_xml(&mut message, &config);
501 assert!(result.is_ok());
502
503 let (outcome, _) = result.unwrap();
504 assert_eq!(outcome, TaskOutcome::Success);
505
506 let xml_string = message.data()["user_xml"].as_str().unwrap();
507 assert!(xml_string.contains("<user>"));
508 assert!(xml_string.contains("</user>"));
509 assert!(xml_string.contains("<name>John</name>"));
510 }
511
512 #[test]
513 fn test_execute_publish_xml_not_found() {
514 let mut message = Message::new(Arc::new(dv(json!({}))));
515
516 let config = PublishConfig {
517 source: "nonexistent".to_string(),
518 target: "output".to_string(),
519 pretty: false,
520 root_element: "root".to_string(),
521 ..Default::default()
522 };
523
524 assert!(execute_publish_xml(&mut message, &config).is_err());
525 }
526
527 #[test]
528 fn test_json_to_xml_simple() {
529 let value = json!({"name": "Test", "value": 42});
530 let xml = json_to_xml(&value, "root").unwrap();
531 assert!(xml.contains("<root>"));
532 assert!(xml.contains("</root>"));
533 assert!(xml.contains("<name>Test</name>"));
534 assert!(xml.contains("<value>42</value>"));
535 }
536
537 #[test]
538 fn test_json_to_xml_nested() {
539 let value = json!({"user": {"name": "Alice", "email": "alice@example.com"}});
540 let xml = json_to_xml(&value, "data").unwrap();
541 assert!(xml.contains("<data>"));
542 assert!(xml.contains("<user>"));
543 assert!(xml.contains("<name>Alice</name>"));
544 }
545
546 #[test]
547 fn test_json_to_xml_array() {
548 let value = json!([1, 2, 3]);
549 let xml = json_to_xml(&value, "numbers").unwrap();
550 assert!(xml.contains("<numbers>"));
551 assert!(xml.contains("<item>1</item>"));
552 assert!(xml.contains("<item>2</item>"));
553 assert!(xml.contains("<item>3</item>"));
554 }
555
556 #[test]
557 fn test_json_to_xml_special_chars() {
558 let value = json!({"text": "<script>alert('xss')</script>"});
559 let xml = json_to_xml(&value, "root").unwrap();
560 assert!(xml.contains("<script>"));
561 assert!(!xml.contains("<script>"));
562 }
563
564 #[test]
565 fn test_escape_xml() {
566 assert_eq!(escape_xml("hello"), "hello");
567 assert_eq!(escape_xml("<tag>"), "<tag>");
568 assert_eq!(escape_xml("a & b"), "a & b");
569 assert_eq!(escape_xml("\"quoted\""), ""quoted"");
570 }
571
572 #[test]
573 fn test_sanitize_xml_name() {
574 assert_eq!(sanitize_xml_name("valid"), "valid");
575 assert_eq!(sanitize_xml_name("_valid"), "_valid");
576 assert_eq!(sanitize_xml_name("123invalid"), "_123invalid");
577 assert_eq!(sanitize_xml_name("has spaces"), "has_spaces");
578 assert_eq!(sanitize_xml_name("has-dash"), "has-dash");
579 assert_eq!(sanitize_xml_name(""), "_element");
580 }
581
582 #[test]
583 fn test_execute_publish_json_nested_source() {
584 let mut message = message_with_data(json!({
585 "response": {"body": {"message": "success"}}
586 }));
587
588 let config = PublishConfig {
589 source: "response.body".to_string(),
590 target: "output".to_string(),
591 pretty: false,
592 root_element: "root".to_string(),
593 ..Default::default()
594 };
595
596 let result = execute_publish_json(&mut message, &config);
597 assert!(result.is_ok());
598
599 let json_string = message.data()["output"].as_str().unwrap();
600 assert!(json_string.contains("success"));
601 }
602}