Skip to main content

stepflow_flow/values/
json_path.rs

1// Copyright 2025 DataStax Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
4// in compliance with the License. You may obtain a copy of the License at
5//
6//     http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software distributed under the License
9// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
10// or implied. See the License for the specific language governing permissions and limitations under
11// the License.
12
13use serde::de::{self, Visitor};
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15use std::fmt;
16
17/// A single part of a JSON path.
18#[derive(Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)]
19pub enum PathPart {
20    /// Access a field by name
21    Field(String),
22    /// Access an array element by index
23    Index(usize),
24    /// Access an element by string key (for arrays that can be indexed by string)
25    IndexStr(String),
26}
27
28/// A JSON path represented as a sequence of path parts.
29/// This type serializes and deserializes as a string but internally
30/// maintains a structured representation for efficient processing.
31#[derive(Debug, Clone, PartialEq, Hash, Eq)]
32pub struct JsonPath(Vec<PathPart>);
33
34impl JsonPath {
35    /// Create a new empty path
36    pub fn new() -> Self {
37        Self(Vec::new())
38    }
39
40    /// Create a path from a vector of parts
41    pub fn from_parts(parts: Vec<PathPart>) -> Self {
42        Self(parts)
43    }
44
45    /// Get the parts of the path
46    pub fn parts(&self) -> &[PathPart] {
47        &self.0
48    }
49
50    /// Check if the path is empty
51    pub fn is_empty(&self) -> bool {
52        self.0.is_empty()
53    }
54
55    pub fn outer_field(&self) -> Option<&str> {
56        match self.0.first() {
57            Some(PathPart::Field(name)) => Some(name),
58            Some(PathPart::IndexStr(name)) => Some(name),
59            _ => None,
60        }
61    }
62
63    /// Parse a string path into a JsonPath
64    pub fn parse(s: &str) -> Result<Self, String> {
65        if s.is_empty() {
66            return Ok(Self::new());
67        }
68
69        let s = match s.trim().strip_prefix("$") {
70            Some(s) => s,
71            None => {
72                // If it doesn't start with $, treat it as a bare field name
73                return Ok(Self::from_parts(vec![PathPart::Field(s.to_string())]));
74            }
75        };
76
77        let mut parts = Vec::new();
78        let mut chars = s.chars().peekable();
79
80        while let Some(ch) = chars.next() {
81            match ch {
82                '[' => {
83                    let mut bracket_content = String::new();
84                    let mut found_end = false;
85
86                    for ch in chars.by_ref() {
87                        if ch == ']' {
88                            found_end = true;
89                            break;
90                        }
91                        bracket_content.push(ch);
92                    }
93
94                    if !found_end {
95                        return Err("Unclosed bracket '[' in path".to_string());
96                    }
97
98                    let bracket_content = bracket_content.trim();
99
100                    // Check if it's a quoted string
101                    if (bracket_content.starts_with('"') && bracket_content.ends_with('"'))
102                        || (bracket_content.starts_with('\'') && bracket_content.ends_with('\''))
103                    {
104                        let field_name = &bracket_content[1..bracket_content.len() - 1];
105                        parts.push(PathPart::Field(field_name.to_string()));
106                    } else if let Ok(index) = bracket_content.parse::<usize>() {
107                        parts.push(PathPart::Index(index));
108                    } else {
109                        return Err(format!(
110                            "Invalid index '{bracket_content}' in JSON path. Expected a number or a quoted string."
111                        ));
112                    }
113                }
114                '.' => {
115                    let mut field_name = String::new();
116
117                    while let Some(&ch) = chars.peek() {
118                        if ch == '[' || ch == '.' {
119                            break;
120                        }
121                        field_name.push(chars.next().unwrap());
122                    }
123
124                    if !field_name.is_empty() {
125                        parts.push(PathPart::Field(field_name));
126                    }
127                }
128                _ => {
129                    return Err(format!(
130                        "Invalid character '{ch}' in JSON path after $ or ']'. Expected '.' or '['"
131                    ));
132                }
133            }
134        }
135
136        Ok(Self::from_parts(parts))
137    }
138
139    /// Convert the path back to string representation
140    fn as_string(&self) -> String {
141        let mut result = String::from("$");
142
143        for part in &self.0 {
144            match part {
145                PathPart::Field(name) => {
146                    result.push_str(&format!(".{name}"));
147                }
148                PathPart::Index(idx) => {
149                    result.push_str(&format!("[{idx}]"));
150                }
151                PathPart::IndexStr(s) => {
152                    result.push_str(&format!("[\"{s}\"]"));
153                }
154            }
155        }
156
157        result
158    }
159}
160
161impl Default for JsonPath {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl fmt::Display for JsonPath {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}", self.as_string())
170    }
171}
172
173impl Serialize for JsonPath {
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: Serializer,
177    {
178        self.to_string().serialize(serializer)
179    }
180}
181
182impl<'de> Deserialize<'de> for JsonPath {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: Deserializer<'de>,
186    {
187        struct JsonPathVisitor;
188
189        impl<'de> Visitor<'de> for JsonPathVisitor {
190            type Value = JsonPath;
191
192            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193                formatter.write_str("a JSON path string")
194            }
195
196            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
197            where
198                E: de::Error,
199            {
200                JsonPath::parse(value).map_err(de::Error::custom)
201            }
202
203            fn visit_none<E>(self) -> Result<Self::Value, E>
204            where
205                E: de::Error,
206            {
207                Ok(JsonPath::new())
208            }
209
210            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
211            where
212                D: Deserializer<'de>,
213            {
214                deserializer.deserialize_str(self)
215            }
216        }
217
218        deserializer.deserialize_option(JsonPathVisitor)
219    }
220}
221
222impl schemars::JsonSchema for JsonPath {
223    fn schema_name() -> std::borrow::Cow<'static, str> {
224        "JsonPath".into()
225    }
226
227    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
228        schemars::json_schema!({
229            "type": "string",
230            "description": "JSON path expression to apply to the referenced value."
231        })
232    }
233}
234
235impl From<String> for JsonPath {
236    fn from(s: String) -> Self {
237        Self::parse(&s).unwrap_or_default()
238    }
239}
240
241impl From<&str> for JsonPath {
242    fn from(s: &str) -> Self {
243        Self::parse(s).unwrap_or_default()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_json_path_parsing() {
253        // Test backwards compatibility - bare field name
254        let path = JsonPath::parse("field").unwrap();
255        assert_eq!(path.parts(), &[PathPart::Field("field".to_string())]);
256        assert_eq!(path.to_string(), "$.field");
257
258        // Test $ prefix field access
259        let path = JsonPath::parse("$[\"field\"]").unwrap();
260        assert_eq!(path.parts(), &[PathPart::Field("field".to_string())]);
261        assert_eq!(path.to_string(), "$.field");
262
263        // Test dot notation
264        let path = JsonPath::parse("$.field").unwrap();
265        assert_eq!(path.parts(), &[PathPart::Field("field".to_string())]);
266        assert_eq!(path.to_string(), "$.field");
267
268        // Test array index
269        let path = JsonPath::parse("$[0]").unwrap();
270        assert_eq!(path.parts(), &[PathPart::Index(0)]);
271        assert_eq!(path.to_string(), "$[0]");
272
273        // Test complex path
274        let path = JsonPath::parse("$.field[0].nested").unwrap();
275        assert_eq!(
276            path.parts(),
277            &[
278                PathPart::Field("field".to_string()),
279                PathPart::Index(0),
280                PathPart::Field("nested".to_string())
281            ]
282        );
283        assert_eq!(path.to_string(), "$.field[0].nested");
284
285        // Test empty path
286        let path = JsonPath::parse("").unwrap();
287        assert!(path.is_empty());
288        assert_eq!(path.to_string(), "$");
289    }
290    #[test]
291
292    fn test_json_path_invalid_syntax() {
293        // Invalid -- missing `.` or `[` after `$`
294        assert_eq!(
295            JsonPath::parse("$field").unwrap_err(),
296            "Invalid character 'f' in JSON path after $ or ']'. Expected '.' or '['"
297        );
298
299        // Missing `.` or `[` after `]`
300        assert_eq!(
301            JsonPath::parse("$[0]field").unwrap_err(),
302            "Invalid character 'f' in JSON path after $ or ']'. Expected '.' or '['"
303        );
304
305        // Unterminated string
306        assert_eq!(
307            JsonPath::parse("$[\"hello").unwrap_err(),
308            "Unclosed bracket '[' in path"
309        );
310        // Unterminated square-brackets (string key)
311        assert_eq!(
312            JsonPath::parse("$[\"hello\"").unwrap_err(),
313            "Unclosed bracket '[' in path"
314        );
315        // Unterminated square-brackets (numeric key)
316        assert_eq!(
317            JsonPath::parse("$[5").unwrap_err(),
318            "Unclosed bracket '[' in path"
319        );
320        // Invalid non-digit in square brackets
321        assert_eq!(
322            JsonPath::parse("$[hello]").unwrap_err(),
323            "Invalid index 'hello' in JSON path. Expected a number or a quoted string."
324        );
325    }
326
327    #[test]
328    fn test_json_path_serialization() {
329        // Test that single field serializes with $ notation (changed from backwards compatibility)
330        let path = JsonPath::from("field");
331        let serialized = serde_json::to_string(&path).unwrap();
332        assert_eq!(serialized, "\"$.field\"");
333
334        // Test that complex path serializes with $ notation
335        let path = JsonPath::parse("$.field[0]").unwrap();
336        let serialized = serde_json::to_string(&path).unwrap();
337        assert_eq!(serialized, "\"$.field[0]\"");
338
339        // Test round-trip
340        let original = JsonPath::parse("$.items[0].name").unwrap();
341        let serialized = serde_json::to_string(&original).unwrap();
342        let deserialized: JsonPath = serde_json::from_str(&serialized).unwrap();
343        assert_eq!(original, deserialized);
344    }
345
346    #[test]
347    fn test_json_path_deserialization() {
348        let deserialized: JsonPath = serde_json::from_str(&"\"$\"").unwrap();
349        assert_eq!(deserialized, JsonPath::parse("$").unwrap());
350
351        let deserialized: JsonPath = serde_json::from_str(&"null").unwrap();
352        assert_eq!(deserialized, JsonPath::new());
353    }
354}