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;
15use crate::engine::message::{Change, Message};
16use crate::engine::task_outcome::TaskOutcome;
17use crate::engine::utils::{get_nested_value, get_nested_value_parts, set_nested_value_parts};
18use datavalue::OwnedDataValue;
19use log::debug;
20use serde::Deserialize;
21use serde_json::Value;
22use std::sync::Arc;
23
24/// Configuration for parse functions.
25#[derive(Debug, Clone, Default, Deserialize)]
26pub struct ParseConfig {
27    /// Source path to read from.
28    pub source: String,
29
30    /// Target field name in `data` (stored at `data.{target}`).
31    pub target: String,
32
33    /// Engine-internal: precomputed `"data.{target}"`, populated by
34    /// `LogicCompiler` (and eagerly by [`Self::from_json`]). Cloned
35    /// (refcount bump) into `Change.path` instead of re-allocating per
36    /// call. Not part of the stable API.
37    #[doc(hidden)]
38    #[serde(skip)]
39    pub target_path_arc: Arc<str>,
40
41    /// Engine-internal: pre-split segments of the target path, consumed by
42    /// the `*_parts` tree walkers so the hot path never re-splits. Not part
43    /// of the stable API.
44    #[doc(hidden)]
45    #[serde(skip)]
46    pub target_path_parts: Arc<[Arc<str>]>,
47}
48
49impl ParseConfig {
50    pub fn from_json(input: &Value) -> Result<Self> {
51        let source = input
52            .get("source")
53            .and_then(Value::as_str)
54            .ok_or_else(|| {
55                DataflowError::Validation("Missing 'source' in parse config".to_string())
56            })?
57            .to_string();
58
59        let target = input
60            .get("target")
61            .and_then(Value::as_str)
62            .ok_or_else(|| {
63                DataflowError::Validation("Missing 'target' in parse config".to_string())
64            })?
65            .to_string();
66
67        let mut config = ParseConfig {
68            source,
69            target,
70            ..Default::default()
71        };
72        config.precompute_target_path();
73        Ok(config)
74    }
75
76    /// Populate the precomputed target-path fields from `target`. Called by
77    /// `LogicCompiler` for serde-built configs and by `from_json`.
78    pub(crate) fn precompute_target_path(&mut self) {
79        let path = format!("data.{}", self.target);
80        let parts: Vec<Arc<str>> = path.split('.').map(Arc::from).collect();
81        self.target_path_parts = parts.into();
82        self.target_path_arc = Arc::from(path);
83    }
84
85    /// Precomputed `(path, parts)` for `data.{target}` — falls back to
86    /// computing on the fly for directly-constructed configs (the test
87    /// surface), mirroring the `MapMapping` fallback pattern.
88    fn resolve_target_path(&self) -> (Arc<str>, Arc<[Arc<str>]>) {
89        if self.target_path_parts.is_empty() {
90            let path = format!("data.{}", self.target);
91            let parts: Vec<Arc<str>> = path.split('.').map(Arc::from).collect();
92            (Arc::from(path), parts.into())
93        } else {
94            (
95                Arc::clone(&self.target_path_arc),
96                Arc::clone(&self.target_path_parts),
97            )
98        }
99    }
100
101    /// Extract the source value as an owned `OwnedDataValue`.
102    fn extract_source(&self, message: &Message) -> OwnedDataValue {
103        if self.source == "payload" {
104            (*message.payload).clone()
105        } else if let Some(path) = self.source.strip_prefix("payload.") {
106            get_nested_value(&message.payload, path)
107                .cloned()
108                .unwrap_or(OwnedDataValue::Null)
109        } else if let Some(path) = self.source.strip_prefix("data.") {
110            get_nested_value(message.data(), path)
111                .cloned()
112                .unwrap_or(OwnedDataValue::Null)
113        } else {
114            get_nested_value(&message.context, &self.source)
115                .cloned()
116                .unwrap_or(OwnedDataValue::Null)
117        }
118    }
119}
120
121/// Execute `parse_json`: read the source value and store it under `data.{target}`.
122/// If the source is a JSON string, attempt to parse it; on failure, store the
123/// string as-is (matches prior behaviour).
124pub fn execute_parse_json(
125    message: &mut Message,
126    config: &ParseConfig,
127) -> Result<(TaskOutcome, Vec<Change>)> {
128    debug!(
129        "ParseJson: Extracting from '{}' to 'data.{}'",
130        config.source, config.target
131    );
132
133    let (target_path_arc, target_parts) = config.resolve_target_path();
134
135    // Hot path: source == "payload" and not a JSON-string payload. The
136    // payload Arc is already on the message; clone-into-context once, reuse
137    // the Arc for the audit entry (refcount bump). This is the realistic
138    // benchmark's exact shape.
139    let payload_fast_path =
140        config.source == "payload" && !matches!(*message.payload, OwnedDataValue::String(_));
141
142    if message.capture_changes {
143        let old_value = get_nested_value_parts(&message.context, &target_parts)
144            .cloned()
145            .unwrap_or(OwnedDataValue::Null);
146
147        // Resolve the source value once. For the payload fast-path we clone
148        // out of the shared `Arc<OwnedDataValue>` payload; for the slow path
149        // we extract from a sub-tree and re-parse JSON-string payloads.
150        let source_data = if payload_fast_path {
151            (*message.payload).clone()
152        } else {
153            let raw = config.extract_source(message);
154            match &raw {
155                OwnedDataValue::String(s) => {
156                    OwnedDataValue::from_json(s).unwrap_or_else(|_| raw.clone())
157                }
158                _ => raw,
159            }
160        };
161
162        // Clone the source value once for the audit `new_value`; the original
163        // is moved into the context below. (No `Arc` wrapping in the audit
164        // entry — `Change` owns its values directly.)
165        let new_value = source_data.clone();
166
167        set_nested_value_parts(&mut message.context, &target_parts, source_data);
168        debug!(
169            "ParseJson: Successfully stored data to 'data.{}'",
170            config.target
171        );
172        return Ok((
173            TaskOutcome::Success,
174            vec![Change {
175                path: target_path_arc,
176                old_value,
177                new_value,
178            }],
179        ));
180    }
181
182    // Audit-off fast path: only the deep clone into the context survives.
183    let source_data_for_context: OwnedDataValue = if payload_fast_path {
184        (*message.payload).clone()
185    } else {
186        let raw = config.extract_source(message);
187        match &raw {
188            OwnedDataValue::String(s) => {
189                OwnedDataValue::from_json(s).unwrap_or_else(|_| raw.clone())
190            }
191            _ => raw,
192        }
193    };
194    set_nested_value_parts(&mut message.context, &target_parts, source_data_for_context);
195
196    debug!(
197        "ParseJson: Successfully stored data to 'data.{}'",
198        config.target
199    );
200
201    Ok((TaskOutcome::Success, Vec::new()))
202}
203
204/// Same as `execute_parse_json` but also refreshes the supplied
205/// `ArenaContext` so subsequent sync tasks in the same workflow stretch see
206/// the written `data.<target>` slot without rebuilding the whole arena form.
207pub(crate) fn execute_parse_json_in_arena(
208    message: &mut Message,
209    config: &ParseConfig,
210    arena_ctx: &mut ArenaContext<'_>,
211) -> Result<(TaskOutcome, Vec<Change>)> {
212    let result = execute_parse_json(message, config)?;
213    // Refresh ONLY the affected depth-2 slot in the arena cache. For
214    // source == "payload" target = "input", this is `data.input` — the
215    // heavy slot — but it's re-arena'd exactly once per workflow stretch
216    // here, not once per subsequent map mapping.
217    let (_, target_parts) = config.resolve_target_path();
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) -> Result<(TaskOutcome, Vec<Change>)> {
229    debug!(
230        "ParseXml: Extracting from '{}' to 'data.{}'",
231        config.source, config.target
232    );
233
234    let source_data = config.extract_source(message);
235
236    let xml_string = match &source_data {
237        OwnedDataValue::String(s) => s.clone(),
238        _ => {
239            return Err(DataflowError::Validation(format!(
240                "ParseXml: Source '{}' is not a string",
241                config.source
242            )));
243        }
244    };
245
246    let parsed_json = xml_to_json(&xml_string)?;
247    let parsed_owned = OwnedDataValue::from(&parsed_json);
248
249    let (target_path_arc, target_parts) = config.resolve_target_path();
250    let old_value = get_nested_value_parts(&message.context, &target_parts)
251        .cloned()
252        .unwrap_or(OwnedDataValue::Null);
253
254    set_nested_value_parts(&mut message.context, &target_parts, parsed_owned.clone());
255
256    debug!(
257        "ParseXml: Successfully parsed and stored XML to 'data.{}'",
258        config.target
259    );
260
261    Ok((
262        TaskOutcome::Success,
263        vec![Change {
264            path: target_path_arc,
265            old_value,
266            new_value: parsed_owned,
267        }],
268    ))
269}
270
271/// Convert an XML string to `serde_json::Value` using quick-xml's serde path.
272fn xml_to_json(xml: &str) -> Result<Value> {
273    use quick_xml::de::from_str;
274
275    let parsed: Value = from_str(xml)
276        .map_err(|e| DataflowError::Validation(format!("Failed to parse XML: {}", e)))?;
277
278    Ok(parsed)
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::engine::utils::set_nested_value;
285    use serde_json::json;
286
287    fn dv(v: serde_json::Value) -> OwnedDataValue {
288        OwnedDataValue::from(&v)
289    }
290
291    #[test]
292    fn test_parse_config_from_json() {
293        let input = json!({"source": "payload", "target": "input_data"});
294        let config = ParseConfig::from_json(&input).unwrap();
295        assert_eq!(config.source, "payload");
296        assert_eq!(config.target, "input_data");
297    }
298
299    #[test]
300    fn test_parse_config_missing_source() {
301        assert!(ParseConfig::from_json(&json!({"target": "input_data"})).is_err());
302    }
303
304    #[test]
305    fn test_parse_config_missing_target() {
306        assert!(ParseConfig::from_json(&json!({"source": "payload"})).is_err());
307    }
308
309    #[test]
310    fn test_execute_parse_json_from_payload() {
311        let payload = json!({"name": "John", "age": 30});
312        let mut message = Message::from_value(&payload);
313
314        let config = ParseConfig {
315            source: "payload".to_string(),
316            target: "input".to_string(),
317            ..Default::default()
318        };
319
320        let result = execute_parse_json(&mut message, &config);
321        assert!(result.is_ok());
322
323        let (outcome, changes) = result.unwrap();
324        assert_eq!(outcome, TaskOutcome::Success);
325        assert_eq!(changes.len(), 1);
326        assert_eq!(changes[0].path.as_ref(), "data.input");
327
328        assert_eq!(message.data()["input"]["name"], dv(json!("John")));
329        assert_eq!(message.data()["input"]["age"], dv(json!(30)));
330    }
331
332    #[test]
333    fn test_execute_parse_json_from_nested_payload() {
334        let payload = json!({"body": {"user": {"name": "Alice"}}});
335        let mut message = Message::from_value(&payload);
336
337        let config = ParseConfig {
338            source: "payload.body.user".to_string(),
339            target: "user_data".to_string(),
340            ..Default::default()
341        };
342
343        let result = execute_parse_json(&mut message, &config);
344        assert!(result.is_ok());
345
346        let (outcome, _) = result.unwrap();
347        assert_eq!(outcome, TaskOutcome::Success);
348        assert_eq!(message.data()["user_data"]["name"], dv(json!("Alice")));
349    }
350
351    #[test]
352    fn test_execute_parse_json_from_data() {
353        let mut message = Message::new(Arc::new(dv(json!({}))));
354        set_nested_value(
355            &mut message.context,
356            "data",
357            dv(json!({"existing": {"value": 42}})),
358        );
359
360        let config = ParseConfig {
361            source: "data.existing".to_string(),
362            target: "copied".to_string(),
363            ..Default::default()
364        };
365
366        let result = execute_parse_json(&mut message, &config);
367        assert!(result.is_ok());
368
369        assert_eq!(message.data()["copied"]["value"], dv(json!(42)));
370    }
371
372    #[test]
373    fn test_execute_parse_xml_simple() {
374        let xml_payload = json!("<root><name>John</name><age>30</age></root>");
375        let mut message = Message::from_value(&xml_payload);
376
377        let config = ParseConfig {
378            source: "payload".to_string(),
379            target: "parsed".to_string(),
380            ..Default::default()
381        };
382
383        let result = execute_parse_xml(&mut message, &config);
384        assert!(result.is_ok());
385
386        let (outcome, _) = result.unwrap();
387        assert_eq!(outcome, TaskOutcome::Success);
388
389        let parsed = &message.data()["parsed"];
390        assert!(parsed.is_object());
391    }
392
393    #[test]
394    fn test_execute_parse_xml_not_string() {
395        let payload = json!({"not": "a string"});
396        let mut message = Message::from_value(&payload);
397
398        let config = ParseConfig {
399            source: "payload".to_string(),
400            target: "parsed".to_string(),
401            ..Default::default()
402        };
403
404        assert!(execute_parse_xml(&mut message, &config).is_err());
405    }
406
407    #[test]
408    fn test_xml_to_json_simple() {
409        let xml = "<root><name>Test</name></root>";
410        let result = xml_to_json(xml);
411        assert!(result.is_ok());
412        let json = result.unwrap();
413        assert!(json.is_object());
414    }
415
416    #[test]
417    fn test_xml_to_json_invalid() {
418        let xml = "<root><unclosed>";
419        assert!(xml_to_json(xml).is_err());
420    }
421
422    #[test]
423    fn test_xml_to_json_with_attributes() {
424        let xml = r#"<person id="123"><name>John</name></person>"#;
425        assert!(xml_to_json(xml).is_ok());
426    }
427
428    #[test]
429    fn test_xml_to_json_nested() {
430        let xml = r#"<root><user><name>Alice</name><email>alice@example.com</email></user></root>"#;
431        let result = xml_to_json(xml);
432        assert!(result.is_ok());
433        let json = result.unwrap();
434        assert!(json.is_object());
435    }
436
437    #[test]
438    fn test_execute_parse_json_from_string_payload() {
439        let payload = Value::String(r#"{"name":"John","age":30}"#.to_string());
440        let mut message = Message::from_value(&payload);
441
442        let config = ParseConfig {
443            source: "payload".to_string(),
444            target: "input".to_string(),
445            ..Default::default()
446        };
447
448        let result = execute_parse_json(&mut message, &config);
449        assert!(result.is_ok());
450
451        let (outcome, _) = result.unwrap();
452        assert_eq!(outcome, TaskOutcome::Success);
453
454        assert_eq!(message.data()["input"]["name"], dv(json!("John")));
455        assert_eq!(message.data()["input"]["age"], dv(json!(30)));
456    }
457}