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