Skip to main content

dataflow_rs/engine/functions/
parse.rs

1//! # Parse Function Module
2//!
3//! Parsing helpers that load payload data into the message's `data` context.
4//! Supports JSON (native) and XML (via `serde_json::Value` bridge — XML is the
5//! slow path, not worth a dedicated walker).
6//!
7//! Source paths:
8//! - `"payload"` — entire payload
9//! - `"payload.<path>"` — a nested field of the payload
10//! - `"data.<path>"` — a nested field of the existing data context
11//! - `"<path>"` — anything else is resolved against the full context
12
13use crate::engine::error::{DataflowError, Result};
14use crate::engine::executor::{ArenaContext, with_arena};
15use crate::engine::functions::path_template::{DataRoot, ParamCtx, PathTemplate, ResolvedPath};
16use crate::engine::functions::template::Template;
17use crate::engine::message::{Change, Message};
18use crate::engine::task_outcome::TaskOutcome;
19use crate::engine::utils::{get_nested_value, get_nested_value_parts, set_nested_value_parts};
20use datalogic_rs::Engine;
21use datavalue::OwnedDataValue;
22use log::debug;
23use serde::Deserialize;
24use serde_json::Value;
25use std::borrow::Cow;
26use std::sync::Arc;
27
28/// Configuration for parse functions.
29#[derive(Debug, Clone, Default, Deserialize)]
30pub struct ParseConfig {
31    /// Source location to read from — `"payload"`, `"payload.<path>"`,
32    /// `"data.<path>"`, or a bare context path.
33    ///
34    /// JSONLogic, so the location can be computed:
35    /// `{"cat": ["data.batches.", {"var": "temp_data.i"}]}`. It resolves to the
36    /// *name* of a location, never to the value at one — `payload` is not in
37    /// the JSONLogic evaluation context, so an expression could not read it
38    /// even if it tried. Read it through `Self::resolve_source`.
39    pub source: Template,
40
41    /// Target field name in `data` (stored at `data.{target}`).
42    ///
43    /// JSONLogic. A literal folds at compile time and keeps the precomputed
44    /// split this always had.
45    pub target: PathTemplate<DataRoot>,
46}
47
48impl ParseConfig {
49    pub fn from_json(input: &Value) -> Result<Self> {
50        let source = input.get("source").cloned().ok_or_else(|| {
51            DataflowError::Validation("Missing 'source' in parse config".to_string())
52        })?;
53
54        let target = input.get("target").cloned().ok_or_else(|| {
55            DataflowError::Validation("Missing 'target' in parse config".to_string())
56        })?;
57
58        Ok(Self {
59            source: Template::from(source),
60            target: PathTemplate::from(target),
61        })
62    }
63
64    /// The source location for this message.
65    ///
66    /// # Errors
67    ///
68    /// [`DataflowError::LogicEvaluation`] if the expression fails to evaluate.
69    pub(crate) fn resolve_source(&self, p: ParamCtx<'_>) -> Result<Cow<'_, str>> {
70        self.source.resolve_str_in_arena(p)
71    }
72
73    /// The write destination for this message, as `(dotted, parts)`.
74    ///
75    /// # Errors
76    ///
77    /// As `Self::resolve_source`.
78    pub(crate) fn resolve_target_path(&self, p: ParamCtx<'_>) -> Result<Cow<'_, ResolvedPath>> {
79        self.target.resolve_in_arena(p)
80    }
81
82    /// Extract the value at `source` as an owned `OwnedDataValue`.
83    ///
84    /// `payload` is reachable here and nowhere else: it is a separate field on
85    /// `Message`, not part of the JSONLogic evaluation context, so this
86    /// dispatch on the *resolved name* is the only way to read it.
87    fn extract_source(&self, message: &Message, source: &str) -> OwnedDataValue {
88        if source == "payload" {
89            (*message.payload).clone()
90        } else if let Some(path) = source.strip_prefix("payload.") {
91            get_nested_value(&message.payload, path)
92                .cloned()
93                .unwrap_or(OwnedDataValue::Null)
94        } else if let Some(path) = source.strip_prefix("data.") {
95            get_nested_value(message.data(), path)
96                .cloned()
97                .unwrap_or(OwnedDataValue::Null)
98        } else {
99            get_nested_value(&message.context, source)
100                .cloned()
101                .unwrap_or(OwnedDataValue::Null)
102        }
103    }
104}
105
106/// Execute `parse_json`: read the source value and store it under `data.{target}`.
107/// If the source is a JSON string, attempt to parse it; on failure, store the
108/// string as-is (matches prior behaviour).
109pub fn execute_parse_json(
110    message: &mut Message,
111    config: &ParseConfig,
112    engine: &Engine,
113) -> Result<(TaskOutcome, Vec<Change>)> {
114    // Opens its own arena, as `MapConfig::execute` does — this entry point is
115    // for callers outside a workflow sync stretch. Inside one,
116    // `execute_parse_json_in_arena` reuses the stretch's context instead.
117    with_arena(|arena| {
118        let arena_ctx = ArenaContext::from_owned(&message.context, arena);
119        parse_json_in(message, config, ParamCtx::from_arena(engine, &arena_ctx))
120    })
121}
122
123fn parse_json_in(
124    message: &mut Message,
125    config: &ParseConfig,
126    p: ParamCtx<'_>,
127) -> Result<(TaskOutcome, Vec<Change>)> {
128    let source = config.resolve_source(p)?;
129    let target = config.resolve_target_path(p)?;
130    let (target_path_arc, target_parts) = (&target.0, &*target.1);
131    debug!("ParseJson: Extracting from '{source}' to '{target_path_arc}'");
132
133    // Hot path: source == "payload" and not a JSON-string payload. The
134    // payload Arc is already on the message; clone-into-context once, reuse
135    // the Arc for the audit entry (refcount bump). This is the realistic
136    // benchmark's exact shape.
137    let payload_fast_path =
138        source == "payload" && !matches!(*message.payload, OwnedDataValue::String(_));
139
140    if message.capture_changes {
141        let old_value = get_nested_value_parts(&message.context, target_parts)
142            .cloned()
143            .unwrap_or(OwnedDataValue::Null);
144
145        // Resolve the source value once. For the payload fast-path we clone
146        // out of the shared `Arc<OwnedDataValue>` payload; for the slow path
147        // we extract from a sub-tree and re-parse JSON-string payloads.
148        let source_data = resolve_parsed_source(config, message, &source, payload_fast_path);
149
150        // Clone the source value once for the audit `new_value`; the original
151        // is moved into the context below. (No `Arc` wrapping in the audit
152        // entry — `Change` owns its values directly.)
153        let new_value = source_data.clone();
154
155        set_nested_value_parts(&mut message.context, target_parts, source_data);
156        debug!("ParseJson: Successfully stored data to '{target_path_arc}'");
157        return Ok((
158            TaskOutcome::Success,
159            vec![Change {
160                path: Arc::clone(target_path_arc),
161                old_value,
162                new_value,
163                removed: false,
164            }],
165        ));
166    }
167
168    // Audit-off fast path: only the deep clone into the context survives.
169    let source_data_for_context =
170        resolve_parsed_source(config, message, &source, payload_fast_path);
171    set_nested_value_parts(&mut message.context, target_parts, source_data_for_context);
172
173    debug!("ParseJson: Successfully stored data to '{target_path_arc}'");
174
175    Ok((TaskOutcome::Success, Vec::new()))
176}
177
178/// Resolve the value `parse_json` stores into `data.{target}`. The payload
179/// fast-path clones straight out of the shared `Arc<OwnedDataValue>` payload;
180/// otherwise the source is extracted from a sub-tree, re-parsing a
181/// JSON-string source and falling back to the raw value on parse failure.
182fn resolve_parsed_source(
183    config: &ParseConfig,
184    message: &Message,
185    source: &str,
186    payload_fast_path: bool,
187) -> OwnedDataValue {
188    if payload_fast_path {
189        (*message.payload).clone()
190    } else {
191        let raw = config.extract_source(message, source);
192        match &raw {
193            OwnedDataValue::String(s) => {
194                OwnedDataValue::from_json(s).unwrap_or_else(|_| raw.clone())
195            }
196            _ => raw,
197        }
198    }
199}
200
201/// Same as `execute_parse_json` but also refreshes the supplied
202/// `ArenaContext` so subsequent sync tasks in the same workflow stretch see
203/// the written `data.<target>` slot without rebuilding the whole arena form.
204pub(crate) fn execute_parse_json_in_arena<'a>(
205    message: &mut Message,
206    config: &ParseConfig,
207    engine: &datalogic_rs::Engine,
208    arena_ctx: &mut ArenaContext<'a>,
209) -> Result<(TaskOutcome, Vec<Change>)> {
210    let p = ParamCtx::from_arena(engine, arena_ctx);
211    let result = parse_json_in(message, config, p)?;
212    // Refresh ONLY the affected depth-2 slot in the arena cache. For
213    // source == "payload" target = "input", this is `data.input` — the
214    // heavy slot — but it's re-arena'd exactly once per workflow stretch
215    // here, not once per subsequent map mapping.
216    let target = config.resolve_target_path(p)?;
217    let target_parts = &*target.1;
218    arena_ctx.refresh_for_path_parts(&message.context, target_parts);
219    Ok(result)
220}
221
222/// Execute `parse_xml`: read the source string, parse XML into a
223/// `serde_json::Value` (existing quick-xml path), convert to `OwnedDataValue`,
224/// store under `data.{target}`.
225pub fn execute_parse_xml(
226    message: &mut Message,
227    config: &ParseConfig,
228    engine: &Engine,
229) -> Result<(TaskOutcome, Vec<Change>)> {
230    with_arena(|arena| {
231        let arena_ctx = ArenaContext::from_owned(&message.context, arena);
232        parse_xml_in(message, config, ParamCtx::from_arena(engine, &arena_ctx))
233    })
234}
235
236pub(crate) fn parse_xml_in(
237    message: &mut Message,
238    config: &ParseConfig,
239    p: ParamCtx<'_>,
240) -> Result<(TaskOutcome, Vec<Change>)> {
241    let source = config.resolve_source(p)?;
242    debug!("ParseXml: Extracting from '{source}'");
243    let source_data = config.extract_source(message, &source);
244
245    let xml_string = match &source_data {
246        OwnedDataValue::String(s) => s.clone(),
247        _ => {
248            return Err(DataflowError::Validation(format!(
249                "ParseXml: Source '{source}' is not a string"
250            )));
251        }
252    };
253
254    let parsed_json = xml_to_json(&xml_string)?;
255    let parsed_owned = OwnedDataValue::from(&parsed_json);
256
257    let target = config.resolve_target_path(p)?;
258    let (target_path_arc, target_parts) = (&target.0, &*target.1);
259    let old_value = get_nested_value_parts(&message.context, target_parts)
260        .cloned()
261        .unwrap_or(OwnedDataValue::Null);
262
263    set_nested_value_parts(&mut message.context, target_parts, parsed_owned.clone());
264
265    debug!("ParseXml: Successfully parsed and stored XML to '{target_path_arc}'");
266
267    Ok((
268        TaskOutcome::Success,
269        vec![Change {
270            path: Arc::clone(target_path_arc),
271            old_value,
272            new_value: parsed_owned,
273            removed: false,
274        }],
275    ))
276}
277
278/// Convert an XML string to `serde_json::Value` using quick-xml's serde path.
279fn xml_to_json(xml: &str) -> Result<Value> {
280    use quick_xml::de::from_str;
281
282    let parsed: Value = from_str(xml)
283        .map_err(|e| DataflowError::Validation(format!("Failed to parse XML: {}", e)))?;
284
285    Ok(parsed)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::engine::utils::set_nested_value;
292    use serde_json::json;
293    use std::sync::Arc;
294
295    /// The engine parameters are resolved against — the same construction
296    /// production uses, so a test cannot agree with an engine that never runs.
297    fn test_engine() -> Engine {
298        crate::engine::compiler::datalogic_engine_builder().build()
299    }
300
301    fn dv(v: serde_json::Value) -> OwnedDataValue {
302        OwnedDataValue::from(&v)
303    }
304
305    #[test]
306    fn test_parse_config_from_json() {
307        let input = json!({"source": "payload", "target": "input_data"});
308        let config = ParseConfig::from_json(&input).unwrap();
309        assert_eq!(config.source.as_json(), &json!("payload"));
310        assert_eq!(config.target.as_json(), &json!("input_data"));
311    }
312
313    #[test]
314    fn test_parse_config_missing_source() {
315        assert!(ParseConfig::from_json(&json!({"target": "input_data"})).is_err());
316    }
317
318    #[test]
319    fn test_parse_config_missing_target() {
320        assert!(ParseConfig::from_json(&json!({"source": "payload"})).is_err());
321    }
322
323    #[test]
324    fn test_execute_parse_json_from_payload() {
325        let payload = json!({"name": "John", "age": 30});
326        let mut message = Message::from_value(&payload);
327
328        let config = ParseConfig {
329            source: Template::from(json!("payload")),
330            target: PathTemplate::from("input"),
331        };
332
333        let result = execute_parse_json(&mut message, &config, &test_engine());
334        assert!(result.is_ok());
335
336        let (outcome, changes) = result.unwrap();
337        assert_eq!(outcome, TaskOutcome::Success);
338        assert_eq!(changes.len(), 1);
339        assert_eq!(changes[0].path.as_ref(), "data.input");
340
341        assert_eq!(message.data()["input"]["name"], dv(json!("John")));
342        assert_eq!(message.data()["input"]["age"], dv(json!(30)));
343    }
344
345    #[test]
346    fn test_execute_parse_json_from_nested_payload() {
347        let payload = json!({"body": {"user": {"name": "Alice"}}});
348        let mut message = Message::from_value(&payload);
349
350        let config = ParseConfig {
351            source: Template::from(json!("payload.body.user")),
352            target: PathTemplate::from("user_data"),
353        };
354
355        let result = execute_parse_json(&mut message, &config, &test_engine());
356        assert!(result.is_ok());
357
358        let (outcome, _) = result.unwrap();
359        assert_eq!(outcome, TaskOutcome::Success);
360        assert_eq!(message.data()["user_data"]["name"], dv(json!("Alice")));
361    }
362
363    #[test]
364    fn test_execute_parse_json_from_data() {
365        let mut message = Message::new(Arc::new(dv(json!({}))));
366        set_nested_value(
367            &mut message.context,
368            "data",
369            dv(json!({"existing": {"value": 42}})),
370        );
371
372        let config = ParseConfig {
373            source: Template::from(json!("data.existing")),
374            target: PathTemplate::from("copied"),
375        };
376
377        let result = execute_parse_json(&mut message, &config, &test_engine());
378        assert!(result.is_ok());
379
380        assert_eq!(message.data()["copied"]["value"], dv(json!(42)));
381    }
382
383    #[test]
384    fn test_execute_parse_xml_simple() {
385        let xml_payload = json!("<root><name>John</name><age>30</age></root>");
386        let mut message = Message::from_value(&xml_payload);
387
388        let config = ParseConfig {
389            source: Template::from(json!("payload")),
390            target: PathTemplate::from("parsed"),
391        };
392
393        let result = execute_parse_xml(&mut message, &config, &test_engine());
394        assert!(result.is_ok());
395
396        let (outcome, _) = result.unwrap();
397        assert_eq!(outcome, TaskOutcome::Success);
398
399        let parsed = &message.data()["parsed"];
400        assert!(parsed.is_object());
401    }
402
403    #[test]
404    fn test_execute_parse_xml_not_string() {
405        let payload = json!({"not": "a string"});
406        let mut message = Message::from_value(&payload);
407
408        let config = ParseConfig {
409            source: Template::from(json!("payload")),
410            target: PathTemplate::from("parsed"),
411        };
412
413        assert!(execute_parse_xml(&mut message, &config, &test_engine()).is_err());
414    }
415
416    #[test]
417    fn test_xml_to_json_simple() {
418        let xml = "<root><name>Test</name></root>";
419        let result = xml_to_json(xml);
420        assert!(result.is_ok());
421        let json = result.unwrap();
422        assert!(json.is_object());
423    }
424
425    #[test]
426    fn test_xml_to_json_invalid() {
427        let xml = "<root><unclosed>";
428        assert!(xml_to_json(xml).is_err());
429    }
430
431    #[test]
432    fn test_xml_to_json_with_attributes() {
433        let xml = r#"<person id="123"><name>John</name></person>"#;
434        assert!(xml_to_json(xml).is_ok());
435    }
436
437    #[test]
438    fn test_xml_to_json_nested() {
439        let xml = r#"<root><user><name>Alice</name><email>alice@example.com</email></user></root>"#;
440        let result = xml_to_json(xml);
441        assert!(result.is_ok());
442        let json = result.unwrap();
443        assert!(json.is_object());
444    }
445
446    #[test]
447    fn test_execute_parse_json_from_string_payload() {
448        let payload = Value::String(r#"{"name":"John","age":30}"#.to_string());
449        let mut message = Message::from_value(&payload);
450
451        let config = ParseConfig {
452            source: Template::from(json!("payload")),
453            target: PathTemplate::from("input"),
454        };
455
456        let result = execute_parse_json(&mut message, &config, &test_engine());
457        assert!(result.is_ok());
458
459        let (outcome, _) = result.unwrap();
460        assert_eq!(outcome, TaskOutcome::Success);
461
462        assert_eq!(message.data()["input"]["name"], dv(json!("John")));
463        assert_eq!(message.data()["input"]["age"], dv(json!(30)));
464    }
465}