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