osp_cli/dsl/parse/
path.rs1use thiserror::Error;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct PathExpression {
5 pub absolute: bool,
6 pub segments: Vec<PathSegment>,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct PathSegment {
11 pub name: Option<String>,
12 pub selectors: Vec<Selector>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Selector {
17 Fanout,
18 Index(i64),
19 Slice {
20 start: Option<i64>,
21 stop: Option<i64>,
22 step: Option<i64>,
23 },
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Error)]
27pub enum PathParseError {
28 #[error("path expression cannot be empty")]
29 EmptyExpression,
30 #[error("path expression cannot be only '.'")]
31 DotOnlyExpression,
32 #[error("unmatched ']' in path expression")]
33 UnmatchedClosingBracket,
34 #[error("empty path segment")]
35 EmptySegment,
36 #[error("unclosed '[' in path expression")]
37 UnclosedBracket,
38 #[error("unexpected character in path segment")]
39 UnexpectedSegmentCharacter,
40 #[error("unclosed '[' in path segment")]
41 UnclosedSegmentBracket,
42 #[error("slice selector has too many components")]
43 SliceTooManyComponents,
44 #[error("invalid list index: {content}")]
45 InvalidListIndex { content: String },
46 #[error("invalid integer in slice selector: {value}")]
47 InvalidSliceInteger { value: String },
48}
49
50type Result<T> = std::result::Result<T, PathParseError>;
51
52pub fn parse_path(input: &str) -> Result<PathExpression> {
53 let trimmed = input.trim();
54 if trimmed.is_empty() {
55 return Err(PathParseError::EmptyExpression);
56 }
57
58 let absolute = trimmed.starts_with('.');
59 let body = if absolute { &trimmed[1..] } else { trimmed };
60 if body.is_empty() {
61 return Err(PathParseError::DotOnlyExpression);
62 }
63
64 let raw_segments = split_path_segments(body)?;
65 let mut segments = Vec::with_capacity(raw_segments.len());
66 for raw_segment in raw_segments {
67 segments.push(parse_segment(&raw_segment)?);
68 }
69
70 Ok(PathExpression { absolute, segments })
71}
72
73pub fn requires_materialization(path: &PathExpression) -> bool {
74 path.segments.iter().any(|segment| {
75 segment.selectors.iter().any(|selector| match selector {
76 Selector::Index(index) => *index < 0,
77 Selector::Fanout => false,
78 Selector::Slice { start, stop, step } => {
79 !(start.is_none() && stop.is_none() && step.is_none())
80 }
81 })
82 })
83}
84
85pub fn expression_to_flat_key(path: &PathExpression) -> Option<String> {
86 let mut output = String::new();
87
88 for (index, segment) in path.segments.iter().enumerate() {
89 if index > 0 {
90 output.push('.');
91 }
92
93 if let Some(name) = &segment.name {
94 output.push_str(name);
95 } else {
96 return None;
97 }
98
99 for selector in &segment.selectors {
100 match selector {
101 Selector::Index(value) if *value >= 0 => {
102 output.push('[');
103 output.push_str(&value.to_string());
104 output.push(']');
105 }
106 Selector::Fanout | Selector::Slice { .. } | Selector::Index(_) => return None,
107 }
108 }
109 }
110
111 if output.is_empty() {
112 None
113 } else {
114 Some(output)
115 }
116}
117
118fn split_path_segments(path: &str) -> Result<Vec<String>> {
119 let mut depth = 0usize;
120 let mut current = String::new();
121 let mut segments = Vec::new();
122
123 for ch in path.chars() {
124 match ch {
125 '[' => {
126 depth = depth.saturating_add(1);
127 current.push(ch);
128 }
129 ']' => {
130 if depth == 0 {
131 return Err(PathParseError::UnmatchedClosingBracket);
132 }
133 depth -= 1;
134 current.push(ch);
135 }
136 '.' if depth == 0 => {
137 if current.is_empty() {
138 return Err(PathParseError::EmptySegment);
139 }
140 segments.push(current);
141 current = String::new();
142 }
143 _ => current.push(ch),
144 }
145 }
146
147 if depth != 0 {
148 return Err(PathParseError::UnclosedBracket);
149 }
150 if current.is_empty() {
151 return Err(PathParseError::EmptySegment);
152 }
153 segments.push(current);
154
155 Ok(segments)
156}
157
158fn parse_segment(raw_segment: &str) -> Result<PathSegment> {
159 let mut name = String::new();
160 let mut selectors = Vec::new();
161 let chars: Vec<char> = raw_segment.chars().collect();
162 let mut index = 0usize;
163
164 while index < chars.len() && chars[index] != '[' {
165 name.push(chars[index]);
166 index += 1;
167 }
168
169 let name = if name.is_empty() { None } else { Some(name) };
170
171 while index < chars.len() {
172 if chars[index] != '[' {
173 return Err(PathParseError::UnexpectedSegmentCharacter);
174 }
175 index += 1;
176
177 let mut content = String::new();
178 while index < chars.len() && chars[index] != ']' {
179 content.push(chars[index]);
180 index += 1;
181 }
182 if index == chars.len() {
183 return Err(PathParseError::UnclosedSegmentBracket);
184 }
185 index += 1;
186
187 selectors.push(parse_selector(content.trim())?);
188 }
189
190 Ok(PathSegment { name, selectors })
191}
192
193fn parse_selector(content: &str) -> Result<Selector> {
194 if content.is_empty() {
195 return Ok(Selector::Fanout);
196 }
197
198 if content.contains(':') {
199 let parts: Vec<&str> = content.split(':').collect();
200 if parts.len() > 3 {
201 return Err(PathParseError::SliceTooManyComponents);
202 }
203
204 let start = parse_optional_i64(parts.first().copied().unwrap_or_default())?;
205 let stop = parse_optional_i64(parts.get(1).copied().unwrap_or_default())?;
206 let step = parse_optional_i64(parts.get(2).copied().unwrap_or_default())?;
207
208 return Ok(Selector::Slice { start, stop, step });
209 }
210
211 let index = content
212 .parse::<i64>()
213 .map_err(|_| PathParseError::InvalidListIndex {
214 content: content.to_string(),
215 })?;
216 Ok(Selector::Index(index))
217}
218
219fn parse_optional_i64(value: &str) -> Result<Option<i64>> {
220 if value.trim().is_empty() {
221 return Ok(None);
222 }
223 value
224 .trim()
225 .parse::<i64>()
226 .map(Some)
227 .map_err(|_| PathParseError::InvalidSliceInteger {
228 value: value.to_string(),
229 })
230}
231
232#[cfg(test)]
233mod tests {
234 use super::{
235 PathParseError, Selector, expression_to_flat_key, parse_path, requires_materialization,
236 };
237
238 #[test]
239 fn parses_dotted_path_with_selectors() {
240 let path = parse_path("members[0].uid").expect("path should parse");
241 assert_eq!(path.segments.len(), 2);
242 assert_eq!(path.segments[0].selectors, vec![Selector::Index(0)]);
243 }
244
245 #[test]
246 fn detects_materialization_for_negative_index() {
247 let path = parse_path("members[-1]").expect("path should parse");
248 assert!(requires_materialization(&path));
249 }
250
251 #[test]
252 fn full_slice_does_not_require_materialization() {
253 let path = parse_path("members[:]").expect("path should parse");
254 assert!(!requires_materialization(&path));
255 }
256
257 #[test]
258 fn non_full_slice_requires_materialization() {
259 let path = parse_path("members[1:]").expect("path should parse");
260 assert!(requires_materialization(&path));
261 }
262
263 #[test]
264 fn expression_to_flat_key_accepts_positive_index_only() {
265 let path = parse_path("members[0].uid").expect("path should parse");
266 assert_eq!(
267 expression_to_flat_key(&path),
268 Some("members[0].uid".to_string())
269 );
270
271 let path = parse_path("members[-1].uid").expect("path should parse");
272 assert_eq!(expression_to_flat_key(&path), None);
273 }
274
275 #[test]
276 fn parse_path_reports_typed_errors_for_common_invalid_inputs_unit() {
277 assert_eq!(
278 parse_path(" ").unwrap_err(),
279 PathParseError::EmptyExpression
280 );
281 assert_eq!(
282 parse_path(".").unwrap_err(),
283 PathParseError::DotOnlyExpression
284 );
285 assert_eq!(
286 parse_path("items.").unwrap_err(),
287 PathParseError::EmptySegment
288 );
289 assert_eq!(
290 parse_path("items[abc]").unwrap_err(),
291 PathParseError::InvalidListIndex {
292 content: "abc".to_string()
293 }
294 );
295 assert_eq!(
296 parse_path("items[:x]").unwrap_err(),
297 PathParseError::InvalidSliceInteger {
298 value: "x".to_string()
299 }
300 );
301 }
302}