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