1use std::fmt;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub enum Step {
25 Key(String),
27 Each,
29 At(usize),
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct FieldPath {
36 steps: Vec<Step>,
37}
38
39impl FieldPath {
40 pub fn parse(path: &str) -> Self {
43 let mut steps = Vec::new();
44 for segment in path.split('.') {
45 let (key, brackets) = split_brackets(segment);
46 steps.push(Step::Key(key.to_string()));
47 for group in brackets {
48 steps.push(group);
49 }
50 }
51 Self { steps }
52 }
53
54 pub fn steps(&self) -> &[Step] {
56 &self.steps
57 }
58
59 pub fn is_concrete(&self) -> bool {
61 !self.steps.iter().any(|s| matches!(s, Step::Each))
62 }
63
64 pub fn enters_list(&self) -> bool {
67 self.steps
68 .iter()
69 .any(|s| matches!(s, Step::Each | Step::At(_)))
70 }
71
72 pub fn head(&self) -> &str {
74 match &self.steps[0] {
75 Step::Key(k) => k,
76 _ => unreachable!("a field path starts with a key"),
78 }
79 }
80}
81
82impl fmt::Display for FieldPath {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write_steps(f, &self.steps)
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct Address {
92 steps: Vec<Step>,
93}
94
95impl Address {
96 pub fn parse(text: &str) -> Option<Self> {
98 let path = FieldPath::parse(text);
99 path.is_concrete().then_some(Self { steps: path.steps })
100 }
101
102 pub fn key(name: &str) -> Self {
104 Self {
105 steps: vec![Step::Key(name.to_string())],
106 }
107 }
108
109 pub fn item(mut self, index: usize) -> Self {
111 self.steps.push(Step::At(index));
112 self
113 }
114
115 pub fn steps(&self) -> &[Step] {
117 &self.steps
118 }
119
120 pub fn head(&self) -> &str {
122 match &self.steps[0] {
123 Step::Key(k) => k,
124 _ => unreachable!("an address starts with a key"),
125 }
126 }
127
128 pub fn segments(&self) -> Vec<fig::Segment<'_>> {
130 self.steps
131 .iter()
132 .map(|s| match s {
133 Step::Key(k) => fig::Segment::Key(k),
134 Step::At(i) => fig::Segment::Index(*i),
135 Step::Each => unreachable!("an address has no `[]` step"),
136 })
137 .collect()
138 }
139
140 pub fn as_item(&self) -> Option<(Address, usize)> {
144 match self.steps.last() {
145 Some(Step::At(i)) => Some((
146 Address {
147 steps: self.steps[..self.steps.len() - 1].to_vec(),
148 },
149 *i,
150 )),
151 _ => None,
152 }
153 }
154}
155
156impl fmt::Display for Address {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write_steps(f, &self.steps)
159 }
160}
161
162fn write_steps(f: &mut fmt::Formatter<'_>, steps: &[Step]) -> fmt::Result {
163 for (i, step) in steps.iter().enumerate() {
164 match step {
165 Step::Key(k) => {
166 if i > 0 {
167 f.write_str(".")?;
168 }
169 f.write_str(k)?;
170 }
171 Step::Each => f.write_str("[]")?,
172 Step::At(n) => write!(f, "[{n}]")?,
173 }
174 }
175 Ok(())
176}
177
178fn split_brackets(segment: &str) -> (&str, Vec<Step>) {
182 let Some(open) = segment.find('[') else {
183 return (segment, Vec::new());
184 };
185 let (key, rest) = segment.split_at(open);
186 let mut groups = Vec::new();
187 let mut rest = rest;
188 while !rest.is_empty() {
189 let Some(inner) = rest.strip_prefix('[') else {
190 return (segment, Vec::new());
191 };
192 let Some(close) = inner.find(']') else {
193 return (segment, Vec::new());
194 };
195 let (body, after) = inner.split_at(close);
196 let step = if body.is_empty() {
197 Step::Each
198 } else if let Ok(n) = body.parse::<usize>() {
199 Step::At(n)
200 } else {
201 return (segment, Vec::new());
202 };
203 groups.push(step);
204 rest = &after[1..];
205 }
206 if key.is_empty() {
207 return (segment, Vec::new());
208 }
209 (key, groups)
210}
211
212pub trait Navigate: Sized {
215 fn child(&self, key: &str) -> Option<&Self>;
217 fn items(&self) -> Option<&[Self]>;
219 fn text(&self) -> Option<&str>;
221}
222
223impl Navigate for crate::meta::Value {
224 fn child(&self, key: &str) -> Option<&Self> {
225 self.get(key)
226 }
227 fn items(&self) -> Option<&[Self]> {
228 self.as_sequence()
229 }
230 fn text(&self) -> Option<&str> {
231 self.as_str()
232 }
233}
234
235impl Navigate for fig::Value {
236 fn child(&self, key: &str) -> Option<&Self> {
237 self.get(key)
238 }
239 fn items(&self) -> Option<&[Self]> {
240 self.as_seq()
241 }
242 fn text(&self) -> Option<&str> {
243 self.as_str()
244 }
245}
246
247pub fn values_at<'v, V: Navigate>(root: &'v V, path: &FieldPath) -> Vec<(Address, &'v V)> {
252 let mut found = vec![(Address { steps: Vec::new() }, root)];
253 for step in path.steps() {
254 let mut next = Vec::new();
255 for (address, value) in found {
256 match step {
257 Step::Key(key) => {
258 if let Some(child) = value.child(key) {
259 let mut address = address;
260 address.steps.push(step.clone());
261 next.push((address, child));
262 }
263 }
264 Step::Each => {
265 if let Some(items) = value.items() {
266 for (i, item) in items.iter().enumerate() {
267 let mut address = address.clone();
268 address.steps.push(Step::At(i));
269 next.push((address, item));
270 }
271 }
272 }
273 Step::At(i) => {
274 if let Some(item) = value.items().and_then(|items| items.get(*i)) {
275 let mut address = address;
276 address.steps.push(step.clone());
277 next.push((address, item));
278 }
279 }
280 }
281 }
282 found = next;
283 }
284 found
285}
286
287pub fn strings_at<V: Navigate>(root: &V, path: &FieldPath) -> Vec<(Address, String)> {
294 let mut out = Vec::new();
295 for (address, value) in values_at(root, path) {
296 if let Some(s) = value.text() {
297 out.push((address, s.to_string()));
298 } else if let Some(items) = value.items() {
299 for (i, item) in items.iter().enumerate() {
300 if let Some(s) = item.text() {
301 out.push((address.clone().item(i), s.to_string()));
302 }
303 }
304 }
305 }
306 out
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::meta::{Mapping, Value};
313
314 fn doc() -> Value {
315 let mut a = Mapping::new();
316 a.insert("resource".into(), Value::String("a.md".into()));
317 a.insert("title".into(), Value::String("A".into()));
318 let mut b = Mapping::new();
319 b.insert("resource".into(), Value::String("b.md".into()));
320 let mut generated = Mapping::new();
321 generated.insert("how".into(), Value::String("drafted".into()));
322 let mut root = Mapping::new();
323 root.insert("title".into(), Value::String("x".into()));
324 root.insert(
325 "tags".into(),
326 Value::Sequence(vec![
327 Value::String("t1".into()),
328 Value::Int(3),
329 Value::String("t2".into()),
330 ]),
331 );
332 root.insert("generated".into(), Value::Mapping(generated));
333 root.insert(
334 "sources".into(),
335 Value::Sequence(vec![Value::Mapping(a), Value::Mapping(b), Value::Null]),
336 );
337 Value::Mapping(root)
338 }
339
340 #[test]
341 fn a_path_round_trips_through_display() {
342 for text in [
343 "tags",
344 "generated.how",
345 "sources[].resource",
346 "sources[2].resource[1]",
347 ] {
348 assert_eq!(FieldPath::parse(text).to_string(), text);
349 }
350 assert!(!FieldPath::parse("sources[].resource").is_concrete());
351 assert!(FieldPath::parse("sources[2].resource").is_concrete());
352 assert!(!FieldPath::parse("generated.how").enters_list());
353 assert!(FieldPath::parse("sources[].resource").enters_list());
354 }
355
356 #[test]
357 fn a_malformed_bracket_group_is_part_of_the_key() {
358 let path = FieldPath::parse("odd[x].y");
359 assert_eq!(
360 path.steps(),
361 &[Step::Key("odd[x]".into()), Step::Key("y".into())]
362 );
363 assert_eq!(
364 FieldPath::parse("open[").steps(),
365 &[Step::Key("open[".into())]
366 );
367 assert_eq!(FieldPath::parse("[]").steps(), &[Step::Key("[]".into())]);
368 }
369
370 #[test]
371 fn a_key_path_reaches_one_value_and_a_list_path_reaches_each_item() {
372 let doc = doc();
373 let hits = values_at(&doc, &FieldPath::parse("generated.how"));
374 assert_eq!(hits.len(), 1);
375 assert_eq!(hits[0].0.to_string(), "generated.how");
376 assert_eq!(hits[0].1.as_str(), Some("drafted"));
377
378 let hits = strings_at(&doc, &FieldPath::parse("sources[].resource"));
379 assert_eq!(
380 hits.iter()
381 .map(|(a, s)| (a.to_string(), s.clone()))
382 .collect::<Vec<_>>(),
383 vec![
384 ("sources[0].resource".to_string(), "a.md".to_string()),
385 ("sources[1].resource".to_string(), "b.md".to_string()),
386 ]
387 );
388 let hits = strings_at(&doc, &FieldPath::parse("sources[2].resource"));
390 assert!(hits.is_empty());
391 }
392
393 #[test]
394 fn a_list_leaf_yields_each_string_item_by_position() {
395 let doc = doc();
396 let hits = strings_at(&doc, &FieldPath::parse("tags"));
397 assert_eq!(
398 hits.iter()
399 .map(|(a, s)| (a.to_string(), s.clone()))
400 .collect::<Vec<_>>(),
401 vec![
402 ("tags[0]".to_string(), "t1".to_string()),
403 ("tags[2]".to_string(), "t2".to_string()),
404 ]
405 );
406 let hits = strings_at(&doc, &FieldPath::parse("title"));
407 assert_eq!(hits[0].0.to_string(), "title");
408 }
409
410 #[test]
411 fn a_path_through_the_wrong_shape_reaches_nothing() {
412 let doc = doc();
413 assert!(values_at(&doc, &FieldPath::parse("title.how")).is_empty());
414 assert!(values_at(&doc, &FieldPath::parse("title[]")).is_empty());
415 assert!(values_at(&doc, &FieldPath::parse("sources[9].resource")).is_empty());
416 assert!(values_at(&doc, &FieldPath::parse("missing")).is_empty());
417 }
418
419 #[test]
420 fn an_address_knows_its_list_and_its_editor_segments() {
421 let address = Address::parse("sources[2].resource").unwrap();
422 assert_eq!(address.head(), "sources");
423 assert!(address.as_item().is_none());
424 let (list, i) = Address::parse("contents[3]").unwrap().as_item().unwrap();
425 assert_eq!(list.to_string(), "contents");
426 assert_eq!(i, 3);
427 assert!(Address::parse("sources[].resource").is_none());
428 let segments = address.segments();
429 assert!(matches!(segments[0], fig::Segment::Key("sources")));
430 assert!(matches!(segments[1], fig::Segment::Index(2)));
431 assert!(matches!(segments[2], fig::Segment::Key("resource")));
432 }
433}