use std::collections::BTreeMap;
use saphyr_parser::{Event, Parser};
use super::problem::{FieldPath, Problem, Span};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Spans(BTreeMap<FieldPath, Span>);
enum Frame {
Map { path: FieldPath, key: Option<String> },
Seq { path: FieldPath, next: usize },
}
impl Spans {
#[must_use]
pub fn of(yaml: &str) -> Spans {
let mut spans = Spans::default();
let mut stack: Vec<Frame> = Vec::new();
for (event, span) in Parser::new_from_str(yaml).map_while(Result::ok) {
let at = Span {
line: span.start.line(),
column: span.start.col() + 1,
};
match event {
Event::Scalar(text, ..) => spans.scalar(&mut stack, &text, at),
Event::Alias(_) => {
spans.value(&mut stack, at);
}
Event::MappingStart(..) | Event::SequenceStart(..) => {
let path = spans.value(&mut stack, at).unwrap_or_default();
stack.push(if matches!(event, Event::MappingStart(..)) {
Frame::Map { path, key: None }
} else {
Frame::Seq { path, next: 0 }
});
}
Event::MappingEnd | Event::SequenceEnd => {
stack.pop();
}
_ => {}
}
}
spans
}
fn scalar(&mut self, stack: &mut [Frame], text: &str, at: Span) {
if let Some(Frame::Map { path, key: key @ None }) = stack.last_mut() {
self.0.entry(path.key(text)).or_insert(at);
*key = Some(text.to_owned());
} else {
self.value(stack, at);
}
}
fn value(&mut self, stack: &mut [Frame], at: Span) -> Option<FieldPath> {
match stack.last_mut()? {
Frame::Map { path, key } => Some(path.key(&key.take()?)),
Frame::Seq { path, next } => {
let item = path.index(*next);
*next += 1;
self.0.entry(item.clone()).or_insert(at);
Some(item)
}
}
}
#[must_use]
pub fn find(&self, path: &FieldPath) -> Option<Span> {
let levels = path.as_str().matches(['.', '[']).count() + 1;
std::iter::successors(Some(path.clone()), FieldPath::parent)
.take(levels)
.find_map(|p| self.0.get(&p).copied())
}
#[must_use]
pub fn place(&self, mut problem: Problem) -> Problem {
if problem.span.is_none() {
problem.span = self.find(&problem.path);
}
problem
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_alias_takes_its_place_in_a_list() {
let spans = Spans::of(
"base: &b { a: 1 }
list:
- *b
- two
",
);
let at = |p: &str| spans.find(&FieldPath::new(p)).map(|s| (s.line, s.column));
assert_eq!((at("list[0]"), at("list[1]")), (Some((3, 5)), Some((4, 5))));
}
#[test]
fn keys_and_items_are_placed_in_block_and_flow_style() {
let yaml = "version: 1\nmachines:\n web:\n networks: { lab: 10 }\n files:\n - { path: /a, input: FLAG }\n - path: /b\n";
let spans = Spans::of(yaml);
let at = |p: &str| spans.find(&FieldPath::new(p)).map(|s| (s.line, s.column));
assert_eq!(at("version"), Some((1, 1)));
assert_eq!(at("machines.web"), Some((3, 3)));
assert_eq!(at("machines.web.networks.lab"), Some((4, 17)));
assert_eq!(at("machines.web.files[0]"), Some((6, 9)));
assert_eq!(at("machines.web.files[0].input"), Some((6, 21)));
assert_eq!(at("machines.web.files[1].path"), Some((7, 9)));
assert_eq!(at("machines.web.resources.cpus"), Some((3, 3)));
assert_eq!(at("machines.nope.deep"), Some((2, 1)));
assert_eq!(at("checks"), None);
}
}