Skip to main content

fakecloud_stepfunctions/
io_processing.rs

1use serde_json::Value;
2
3/// Apply InputPath to extract a subset of the raw input.
4/// - `None` or `Some("$")` → return input unchanged
5/// - `Some("null")` handled at call site (pass `{}`)
6/// - `Some("$.foo.bar")` → extract nested field
7pub fn apply_input_path(input: &Value, path: Option<&str>) -> Value {
8    match path {
9        None | Some("$") => input.clone(),
10        Some(p) => resolve_path(input, p),
11    }
12}
13
14/// Apply OutputPath to extract a subset of the effective output.
15/// Same semantics as InputPath.
16pub fn apply_output_path(output: &Value, path: Option<&str>) -> Value {
17    match path {
18        None | Some("$") => output.clone(),
19        Some(p) => resolve_path(output, p),
20    }
21}
22
23/// Apply ResultPath to merge a state's result into the input.
24/// - `None` or `Some("$")` → result replaces input entirely
25/// - `Some("null")` → discard result, return original input
26/// - `Some("$.foo")` → set result at that path within input
27pub fn apply_result_path(input: &Value, result: &Value, path: Option<&str>) -> Value {
28    match path {
29        None | Some("$") => result.clone(),
30        Some("null") => input.clone(),
31        Some(p) => set_at_path(input, p, result),
32    }
33}
34
35/// Resolve a simple JsonPath expression against a JSON value.
36/// Supports: `$`, `$.field`, `$.field.nested`, `$.arr[0]`
37pub fn resolve_path(root: &Value, path: &str) -> Value {
38    if path == "$" {
39        return root.clone();
40    }
41
42    let path = path.strip_prefix("$.").unwrap_or(path);
43    let mut current = root;
44
45    for segment in split_path_segments(path) {
46        match segment {
47            PathSegment::Field(name) => {
48                current = match current.get(name) {
49                    Some(v) => v,
50                    None => return Value::Null,
51                };
52            }
53            PathSegment::Index(name, idx) => {
54                current = match current.get(name) {
55                    Some(v) => match v.get(idx) {
56                        Some(v) => v,
57                        None => return Value::Null,
58                    },
59                    None => return Value::Null,
60                };
61            }
62        }
63    }
64
65    current.clone()
66}
67
68/// Set a value at a simple JsonPath within a JSON structure.
69fn set_at_path(root: &Value, path: &str, value: &Value) -> Value {
70    let mut result = root.clone();
71    let path = path.strip_prefix("$.").unwrap_or(path);
72    let segments: Vec<&str> = path.split('.').collect();
73
74    let mut current = &mut result;
75    for (i, segment) in segments.iter().enumerate() {
76        if i == segments.len() - 1 {
77            // Last segment — set the value
78            if let Some(obj) = current.as_object_mut() {
79                obj.insert(segment.to_string(), value.clone());
80            }
81        } else {
82            // Intermediate — ensure object exists
83            if current.get(*segment).is_none() {
84                if let Some(obj) = current.as_object_mut() {
85                    obj.insert(segment.to_string(), serde_json::json!({}));
86                }
87            }
88            current = current.get_mut(*segment).unwrap();
89        }
90    }
91
92    result
93}
94
95enum PathSegment<'a> {
96    Field(&'a str),
97    Index(&'a str, usize),
98}
99
100fn split_path_segments(path: &str) -> Vec<PathSegment<'_>> {
101    let mut segments = Vec::new();
102    for part in path.split('.') {
103        if let Some(bracket_pos) = part.find('[') {
104            let name = &part[..bracket_pos];
105            let idx_str = &part[bracket_pos + 1..part.len() - 1];
106            if let Ok(idx) = idx_str.parse::<usize>() {
107                segments.push(PathSegment::Index(name, idx));
108            } else {
109                segments.push(PathSegment::Field(part));
110            }
111        } else {
112            segments.push(PathSegment::Field(part));
113        }
114    }
115    segments
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use serde_json::json;
122
123    #[test]
124    fn test_resolve_path_root() {
125        let input = json!({"a": 1});
126        assert_eq!(resolve_path(&input, "$"), input);
127    }
128
129    #[test]
130    fn test_resolve_path_simple_field() {
131        let input = json!({"name": "hello", "value": 42});
132        assert_eq!(resolve_path(&input, "$.name"), json!("hello"));
133        assert_eq!(resolve_path(&input, "$.value"), json!(42));
134    }
135
136    #[test]
137    fn test_resolve_path_nested() {
138        let input = json!({"a": {"b": {"c": 99}}});
139        assert_eq!(resolve_path(&input, "$.a.b.c"), json!(99));
140    }
141
142    #[test]
143    fn test_resolve_path_missing() {
144        let input = json!({"a": 1});
145        assert_eq!(resolve_path(&input, "$.missing"), Value::Null);
146    }
147
148    #[test]
149    fn test_resolve_path_array_index() {
150        let input = json!({"items": [10, 20, 30]});
151        assert_eq!(resolve_path(&input, "$.items[0]"), json!(10));
152        assert_eq!(resolve_path(&input, "$.items[2]"), json!(30));
153    }
154
155    #[test]
156    fn test_apply_input_path_default() {
157        let input = json!({"x": 1});
158        assert_eq!(apply_input_path(&input, None), input);
159        assert_eq!(apply_input_path(&input, Some("$")), input);
160    }
161
162    #[test]
163    fn test_apply_result_path_default() {
164        let input = json!({"x": 1});
165        let result = json!({"y": 2});
166        // Default: result replaces input
167        assert_eq!(apply_result_path(&input, &result, None), result);
168        assert_eq!(apply_result_path(&input, &result, Some("$")), result);
169    }
170
171    #[test]
172    fn test_apply_result_path_null() {
173        let input = json!({"x": 1});
174        let result = json!({"y": 2});
175        // null: discard result, keep input
176        assert_eq!(apply_result_path(&input, &result, Some("null")), input);
177    }
178
179    #[test]
180    fn test_apply_result_path_nested() {
181        let input = json!({"x": 1});
182        let result = json!("hello");
183        let output = apply_result_path(&input, &result, Some("$.result"));
184        assert_eq!(output, json!({"x": 1, "result": "hello"}));
185    }
186
187    #[test]
188    fn test_apply_output_path() {
189        let output = json!({"a": 1, "b": 2});
190        assert_eq!(apply_output_path(&output, Some("$.a")), json!(1));
191        assert_eq!(apply_output_path(&output, None), output);
192    }
193}