1use serde_json::Value;
25
26use crate::FaceError;
27
28pub fn resolve<'v>(value: &'v Value, path: &str) -> Result<&'v Value, FaceError> {
61 resolve_inner(value, path).map_err(|_| FaceError::UnknownItemsPath {
62 path: path.to_string(),
63 })
64}
65
66pub fn resolve_owned(value: &Value, path: &str) -> Result<Value, FaceError> {
84 resolve(value, path).cloned()
85}
86
87fn resolve_inner<'v>(value: &'v Value, path: &str) -> Result<&'v Value, &'static str> {
91 let trimmed = path.strip_prefix('.').unwrap_or(path);
92 if trimmed.is_empty() {
93 return Ok(value);
94 }
95
96 let mut current = value;
97 for segment in parse_segments(trimmed)? {
98 current = step(current, segment)?;
99 }
100 Ok(current)
101}
102
103enum Segment<'a> {
105 Key(&'a str),
107 Index(usize),
109}
110
111fn parse_segments(body: &str) -> Result<Vec<Segment<'_>>, &'static str> {
118 let mut out = Vec::new();
119 let mut rest = body;
120
121 while let Some(after_open) = rest.strip_prefix('[') {
123 let close = after_open.find(']').ok_or("unterminated `[`")?;
124 let index_str = &after_open[..close];
125 if index_str.is_empty() {
126 return Err("empty array index");
127 }
128 let index: usize = index_str.parse().map_err(|_| "non-integer array index")?;
129 out.push(Segment::Index(index));
130 rest = &after_open[close + 1..];
131 }
132
133 if let Some(after_dot) = rest.strip_prefix('.') {
135 if after_dot.is_empty() {
136 return Err("trailing `.`");
137 }
138 rest = after_dot;
139 }
140
141 while !rest.is_empty() {
142 let key_end = rest.find(['.', '[']).unwrap_or(rest.len());
144 let key = &rest[..key_end];
145 if key.is_empty() {
146 return Err("empty segment");
148 }
149 out.push(Segment::Key(key));
150 rest = &rest[key_end..];
151
152 while let Some(after_open) = rest.strip_prefix('[') {
154 let close = after_open.find(']').ok_or("unterminated `[`")?;
155 let index_str = &after_open[..close];
156 if index_str.is_empty() {
157 return Err("empty array index");
158 }
159 let index: usize = index_str.parse().map_err(|_| "non-integer array index")?;
160 out.push(Segment::Index(index));
161 rest = &after_open[close + 1..];
162 }
163
164 if let Some(after_dot) = rest.strip_prefix('.') {
166 if after_dot.is_empty() {
167 return Err("trailing `.`");
168 }
169 rest = after_dot;
170 }
171 }
172
173 Ok(out)
174}
175
176fn step<'v>(current: &'v Value, segment: Segment<'_>) -> Result<&'v Value, &'static str> {
178 match segment {
179 Segment::Key(key) => current
180 .as_object()
181 .ok_or("not an object")?
182 .get(key)
183 .ok_or("missing object key"),
184 Segment::Index(idx) => current
185 .as_array()
186 .ok_or("not an array")?
187 .get(idx)
188 .ok_or("index out of range"),
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use serde_json::json;
196
197 fn err_path(e: &FaceError) -> &str {
198 match e {
199 FaceError::UnknownItemsPath { path } => path,
200 other => panic!("expected UnknownItemsPath, got {other:?}"),
201 }
202 }
203
204 #[test]
205 fn resolves_identity() {
206 let v = json!({"a": 1});
207 assert_eq!(resolve(&v, ".").unwrap(), &v);
208 assert_eq!(resolve(&v, "").unwrap(), &v);
209 }
210
211 #[test]
212 fn resolves_nested_dotted() {
213 let v = json!({"data": {"path": {"text": "src/cli.rs"}}});
214 let got = resolve(&v, "data.path.text").unwrap();
215 assert_eq!(got, &json!("src/cli.rs"));
216 let got2 = resolve(&v, ".data.path.text").unwrap();
218 assert_eq!(got2, &json!("src/cli.rs"));
219 }
220
221 #[test]
222 fn resolves_array_index() {
223 let v = json!({"hits": [{"score": 10}, {"score": 20}]});
224 let got = resolve(&v, ".hits[1].score").unwrap();
225 assert_eq!(got, &json!(20));
226 }
227
228 #[test]
229 fn resolves_chained_indices() {
230 let v = json!({"matrix": [[1, 2], [3, 4]]});
231 let got = resolve(&v, ".matrix[1][0]").unwrap();
232 assert_eq!(got, &json!(3));
233 }
234
235 #[test]
236 fn resolves_top_level_array_index() {
237 let v = json!([10, 20, 30]);
238 let got = resolve(&v, ".[1]").unwrap();
239 assert_eq!(got, &json!(20));
240 }
241
242 #[test]
243 fn missing_path_errors() {
244 let v = json!({"a": 1});
245 let err = resolve(&v, ".b").unwrap_err();
246 assert_eq!(err_path(&err), ".b");
247 let err = resolve(&v, ".a.b").unwrap_err();
248 assert_eq!(err_path(&err), ".a.b");
249 }
250
251 #[test]
252 fn out_of_range_index_errors() {
253 let v = json!({"a": [1, 2]});
254 let err = resolve(&v, ".a[5]").unwrap_err();
255 assert_eq!(err_path(&err), ".a[5]");
256 }
257
258 #[test]
259 fn rejects_malformed_paths() {
260 let v = json!({"a": 1});
261 assert!(resolve(&v, "..").is_err());
262 assert!(resolve(&v, "a.").is_err());
263 assert!(resolve(&v, ".a[").is_err());
264 assert!(resolve(&v, ".a[]").is_err());
265 assert!(resolve(&v, ".a[abc]").is_err());
266 }
267
268 #[test]
269 fn resolve_owned_clones() {
270 let v = json!({"x": [1, 2, 3]});
271 let got = resolve_owned(&v, ".x").unwrap();
272 assert_eq!(got, json!([1, 2, 3]));
273 }
274}