isoloom_spec/syntax/
spans.rs1use std::collections::BTreeMap;
5
6use saphyr_parser::{Event, Parser};
7
8use super::problem::{FieldPath, Problem, Span};
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct Spans(BTreeMap<FieldPath, Span>);
13
14enum Frame {
16 Map { path: FieldPath, key: Option<String> },
18 Seq { path: FieldPath, next: usize },
20}
21
22impl Spans {
23 #[must_use]
26 pub fn of(yaml: &str) -> Spans {
27 let mut spans = Spans::default();
28 let mut stack: Vec<Frame> = Vec::new();
29 for (event, span) in Parser::new_from_str(yaml).map_while(Result::ok) {
30 let at = Span {
31 line: span.start.line(),
32 column: span.start.col() + 1,
33 };
34 match event {
35 Event::Scalar(text, ..) => spans.scalar(&mut stack, &text, at),
36 Event::Alias(_) => {
37 spans.value(&mut stack, at);
38 }
39 Event::MappingStart(..) | Event::SequenceStart(..) => {
40 let path = spans.value(&mut stack, at).unwrap_or_default();
41 stack.push(if matches!(event, Event::MappingStart(..)) {
42 Frame::Map { path, key: None }
43 } else {
44 Frame::Seq { path, next: 0 }
45 });
46 }
47 Event::MappingEnd | Event::SequenceEnd => {
48 stack.pop();
49 }
50 _ => {}
51 }
52 }
53 spans
54 }
55
56 fn scalar(&mut self, stack: &mut [Frame], text: &str, at: Span) {
58 if let Some(Frame::Map { path, key: key @ None }) = stack.last_mut() {
59 self.0.entry(path.key(text)).or_insert(at);
60 *key = Some(text.to_owned());
61 } else {
62 self.value(stack, at);
63 }
64 }
65
66 fn value(&mut self, stack: &mut [Frame], at: Span) -> Option<FieldPath> {
68 match stack.last_mut()? {
69 Frame::Map { path, key } => Some(path.key(&key.take()?)),
70 Frame::Seq { path, next } => {
71 let item = path.index(*next);
72 *next += 1;
73 self.0.entry(item.clone()).or_insert(at);
74 Some(item)
75 }
76 }
77 }
78
79 #[must_use]
82 pub fn find(&self, path: &FieldPath) -> Option<Span> {
83 let levels = path.as_str().matches(['.', '[']).count() + 1;
85 std::iter::successors(Some(path.clone()), FieldPath::parent)
86 .take(levels)
87 .find_map(|p| self.0.get(&p).copied())
88 }
89
90 #[must_use]
92 pub fn place(&self, mut problem: Problem) -> Problem {
93 if problem.span.is_none() {
94 problem.span = self.find(&problem.path);
95 }
96 problem
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn an_alias_takes_its_place_in_a_list() {
106 let spans = Spans::of(
107 "base: &b { a: 1 }
108list:
109 - *b
110 - two
111",
112 );
113 let at = |p: &str| spans.find(&FieldPath::new(p)).map(|s| (s.line, s.column));
114 assert_eq!((at("list[0]"), at("list[1]")), (Some((3, 5)), Some((4, 5))));
115 }
116
117 #[test]
118 fn keys_and_items_are_placed_in_block_and_flow_style() {
119 let yaml = "version: 1\nmachines:\n web:\n networks: { lab: 10 }\n files:\n - { path: /a, input: FLAG }\n - path: /b\n";
120 let spans = Spans::of(yaml);
121 let at = |p: &str| spans.find(&FieldPath::new(p)).map(|s| (s.line, s.column));
122 assert_eq!(at("version"), Some((1, 1)));
123 assert_eq!(at("machines.web"), Some((3, 3)));
124 assert_eq!(at("machines.web.networks.lab"), Some((4, 17)));
125 assert_eq!(at("machines.web.files[0]"), Some((6, 9)));
126 assert_eq!(at("machines.web.files[0].input"), Some((6, 21)));
127 assert_eq!(at("machines.web.files[1].path"), Some((7, 9)));
128 assert_eq!(at("machines.web.resources.cpus"), Some((3, 3)));
130 assert_eq!(at("machines.nope.deep"), Some((2, 1)));
131 assert_eq!(at("checks"), None);
132 }
133}