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            }],
164        ));
165    }
166
167    // Audit-off fast path: only the deep clone into the context survives.
168    let source_data_for_context =
169        resolve_parsed_source(config, message, &source, payload_fast_path);
170    set_nested_value_parts(&mut message.context, target_parts, source_data_for_context);
171
172    debug!("ParseJson: Successfully stored data to '{target_path_arc}'");
173
174    Ok((TaskOutcome::Success, Vec::new()))
175}
176
177/// Resolve the value `parse_json` stores into `data.{target}`. The payload
178/// fast-path clones straight out of the shared `Arc<OwnedDataValue>` payload;
179/// otherwise the source is extracted from a sub-tree, re-parsing a
180/// JSON-string source and falling back to the raw value on parse failure.
181fn resolve_parsed_source(
182    config: &ParseConfig,
183    message: &Message,
184    source: &str,
185    payload_fast_path: bool,
186) -> OwnedDataValue {
187    if payload_fast_path {
188        (*message.payload).clone()
189    } else {
190        let raw = config.extract_source(message, source);
191        match &raw {
192            OwnedDataValue::String(s) => {
193                OwnedDataValue::from_json(s).unwrap_or_else(|_| raw.clone())
194            }
195            _ => raw,
196        }
197    }
198}
199
200/// Same as `execute_parse_json` but also refreshes the supplied
201/// `ArenaContext` so subsequent sync tasks in the same workflow stretch see
202/// the written `data.<target>` slot without rebuilding the whole arena form.
203pub(crate) fn execute_parse_json_in_arena<'a>(
204    message: &mut Message,
205    config: &ParseConfig,
206    engine: &datalogic_rs::Engine,
207    arena_ctx: &mut ArenaContext<'a>,
208) -> Result<(TaskOutcome, Vec<Change>)> {
209    let p = ParamCtx::from_arena(engine, arena_ctx);
210    let result = parse_json_in(message, config, p)?;
211    // Refresh ONLY the affected depth-2 slot in the arena cache. For
212    // source == "payload" target = "input", this is `data.input` — the
213    // heavy slot — but it's re-arena'd exactly once per workflow stretch
214    // here, not once per subsequent map mapping.
215    let target = config.resolve_target_path(p)?;
216    let target_parts = &*target.1;
217    arena_ctx.refresh_for_path_parts(&message.context, target_parts);
218    Ok(result)
219}
220
221/// Execute `parse_xml`: read the source string, parse XML into a
222/// `serde_json::Value` (existing quick-xml path), convert to `OwnedDataValue`,
223/// store under `data.{target}`.
224pub fn execute_parse_xml(
225    message: &mut Message,
226    config: &ParseConfig,
227    engine: &Engine,
228) -> Result<(TaskOutcome, Vec<Change>)> {
229    with_arena(|arena| {
230        let arena_ctx = ArenaContext::from_owned(&message.context, arena);
231        parse_xml_in(message, config, ParamCtx::from_arena(engine, &arena_ctx))
232    })
233}
234
235pub(crate) fn parse_xml_in(
236    message: &mut Message,
237    config: &ParseConfig,
238    p: ParamCtx<'_>,
239) -> Result<(TaskOutcome, Vec<Change>)> {
240    let source = config.resolve_source(p)?;
241    debug!("ParseXml: Extracting from '{source}'");
242    let source_data = config.extract_source(message, &source);
243
244    let xml_string = match &source_data {
245        OwnedDataValue::String(s) => s.clone(),
246        _ => {
247            return Err(DataflowError::Validation(format!(
248                "ParseXml: Source '{source}' is not a string"
249            )));
250        }
251    };
252
253    let parsed_json = xml_to_json(&xml_string)?;
254    let parsed_owned = OwnedDataValue::from(&parsed_json);
255
256    let target = config.resolve_target_path(p)?;
257    let (target_path_arc, target_parts) = (&target.0, &*target.1);
258    let old_value = get_nested_value_parts(&message.context, target_parts)
259        .cloned()
260        .unwrap_or(OwnedDataValue::Null);
261
262    set_nested_value_parts(&mut message.context, target_parts, parsed_owned.clone());
263
264    debug!("ParseXml: Successfully parsed and stored XML to '{target_path_arc}'");
265
266    Ok((
267        TaskOutcome::Success,
268        vec![Change {
269            path: Arc::clone(target_path_arc),
270            old_value,
271            new_value: parsed_owned,
272        }],
273    ))
274}
275
276/// Convert an XML string to `serde_json::Value` using quick-xml's serde path.
277fn xml_to_json(xml: &str) -> Result<Value> {
278    use quick_xml::de::from_str;
279
280    let parsed: Value = from_str(xml)
281        .map_err(|e| DataflowError::Validation(format!("Failed to parse XML: {}", e)))?;
282
283    Ok(parsed)
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::engine::utils::set_nested_value;
290    use serde_json::json;
291    use std::sync::Arc;
292
293    /// The engine parameters are resolved against — the same construction
294    /// production uses, so a test cannot agree with an engine that never runs.
295    fn test_engine() -> Engine {
296        crate::engine::compiler::datalogic_engine_builder().build()
297    }
298
299    fn dv(v: serde_json::Value) -> OwnedDataValue {
300        OwnedDataValue::from(&v)
301    }
302
303    #[test]
304    fn test_parse_config_from_json() {
305        let input = json!({"source": "payload", "target": "input_data"});
306        let config = ParseConfig::from_json(&input).unwrap();
307        assert_eq!(config.source.as_json(), &json!("payload"));
308        assert_eq!(config.target.as_json(), &json!("input_data"));
309    }
310
311    #[test]
312    fn test_parse_config_missing_source() {
313        assert!(ParseConfig::from_json(&json!({"target": "input_data"})).is_err());
314    }
315
316    #[test]
317    fn test_parse_config_missing_target() {
318        assert!(ParseConfig::from_json(&json!({"source": "payload"})).is_err());
319    }
320
321    #[test]
322    fn test_execute_parse_json_from_payload() {
323        let payload = json!({"name": "John", "age": 30});
324        let mut message = Message::from_value(&payload);
325
326        let config = ParseConfig {
327            source: Template::from(json!("payload")),
328            target: PathTemplate::from("input"),
329        };
330
331        let result = execute_parse_json(&mut message, &config, &test_engine());
332        assert!(result.is_ok());
333
334        let (outcome, changes) = result.unwrap();
335        assert_eq!(outcome, TaskOutcome::Success);
336        assert_eq!(changes.len(), 1);
337        assert_eq!(changes[0].path.as_ref(), "data.input");
338
339        assert_eq!(message.data()["input"]["name"], dv(json!("John")));
340        assert_eq!(message.data()["input"]["age"], dv(json!(30)));
341    }
342
343    #[test]
344    fn test_execute_parse_json_from_nested_payload() {
345        let payload = json!({"body": {"user": {"name": "Alice"}}});
346        let mut message = Message::from_value(&payload);
347
348        let config = ParseConfig {
349            source: Template::from(json!("payload.body.user")),
350            target: PathTemplate::from("user_data"),
351        };
352
353        let result = execute_parse_json(&mut message, &config, &test_engine());
354        assert!(result.is_ok());
355
356        let (outcome, _) = result.unwrap();
357        assert_eq!(outcome, TaskOutcome::Success);
358        assert_eq!(message.data()["user_data"]["name"], dv(json!("Alice")));
359    }
360
361    #[test]
362    fn test_execute_parse_json_from_data() {
363        let mut message = Message::new(Arc::new(dv(json!({}))));
364        set_nested_value(
365            &mut message.context,
366            "data",
367            dv(json!({"existing": {"value": 42}})),
368        );
369
370        let config = ParseConfig {
371            source: Template::from(json!("data.existing")),
372            target: PathTemplate::from("copied"),
373        };
374
375        let result = execute_parse_json(&mut message, &config, &test_engine());
376        assert!(result.is_ok());
377
378        assert_eq!(message.data()["copied"]["value"], dv(json!(42)));
379    }
380
381    #[test]
382    fn test_execute_parse_xml_simple() {
383        let xml_payload = json!("<root><name>John</name><age>30</age></root>");
384        let mut message = Message::from_value(&xml_payload);
385
386        let config = ParseConfig {
387            source: Template::from(json!("payload")),
388            target: PathTemplate::from("parsed"),
389        };
390
391        let result = execute_parse_xml(&mut message, &config, &test_engine());
392        assert!(result.is_ok());
393
394        let (outcome, _) = result.unwrap();
395        assert_eq!(outcome, TaskOutcome::Success);
396
397        let parsed = &message.data()["parsed"];
398        assert!(parsed.is_object());
399    }
400
401    #[test]
402    fn test_execute_parse_xml_not_string() {
403        let payload = json!({"not": "a string"});
404        let mut message = Message::from_value(&payload);
405
406        let config = ParseConfig {
407            source: Template::from(json!("payload")),
408            target: PathTemplate::from("parsed"),
409        };
410
411        assert!(execute_parse_xml(&mut message, &config, &test_engine()).is_err());
412    }
413
414    #[test]
415    fn test_xml_to_json_simple() {
416        let xml = "<root><name>Test</name></root>";
417        let result = xml_to_json(xml);
418        assert!(result.is_ok());
419        let json = result.unwrap();
420        assert!(json.is_object());
421    }
422
423    #[test]
424    fn test_xml_to_json_invalid() {
425        let xml = "<root><unclosed>";
426        assert!(xml_to_json(xml).is_err());
427    }
428
429    #[test]
430    fn test_xml_to_json_with_attributes() {
431        let xml = r#"<person id="123"><name>John</name></person>"#;
432        assert!(xml_to_json(xml).is_ok());
433    }
434
435    #[test]
436    fn test_xml_to_json_nested() {
437        let xml = r#"<root><user><name>Alice</name><email>alice@example.com</email></user></root>"#;
438        let result = xml_to_json(xml);
439        assert!(result.is_ok());
440        let json = result.unwrap();
441        assert!(json.is_object());
442    }
443
444    #[test]
445    fn test_execute_parse_json_from_string_payload() {
446        let payload = Value::String(r#"{"name":"John","age":30}"#.to_string());
447        let mut message = Message::from_value(&payload);
448
449        let config = ParseConfig {
450            source: Template::from(json!("payload")),
451            target: PathTemplate::from("input"),
452        };
453
454        let result = execute_parse_json(&mut message, &config, &test_engine());
455        assert!(result.is_ok());
456
457        let (outcome, _) = result.unwrap();
458        assert_eq!(outcome, TaskOutcome::Success);
459
460        assert_eq!(message.data()["input"]["name"], dv(json!("John")));
461        assert_eq!(message.data()["input"]["age"], dv(json!(30)));
462    }
463}