use serde::de::DeserializeOwned;
use serde_yaml_ng::{Mapping, Value};
use super::mistake::Mistake;
use super::problem::{Code, FieldPath, Problem, Problems, closest};
use super::spans::Spans;
use super::{Spec, old_keys};
pub fn parse(yaml: &str) -> Result<Spec, Problems> {
let spans = Spans::of(yaml);
let mut doc: Value = serde_yaml_ng::from_str(yaml).map_err(|e| vec![yaml_problem(&e)])?;
let mut problems = old_keys::scan(&doc);
let clones = crate::clones::expand(&mut doc).unwrap_or_else(|refused| {
problems.push(refused);
indexmap::IndexMap::new()
});
if let Err(refused) = crate::groups::expand(&mut doc) {
problems.push(refused);
}
let Value::Mapping(top) = doc else {
return Err(vec![Problem::new(
Code::InvalidValue,
FieldPath::default(),
"a spec is a mapping of fields (`version`, `name`, `machines`, ...)",
)]);
};
let mut reader = Reader { problems: Vec::new() };
let mut spec = reader.spec(top);
spec.clones = clones;
let old = problems.clone();
problems.extend(reader.problems.into_iter().filter(|p| !old.iter().any(|o| p.path.within(&o.path))));
if problems.is_empty() {
spec.spans = spans;
return Ok(spec);
}
let mut placed: Problems = problems.into_iter().map(|p| spans.place(p)).collect();
placed.sort_by_key(|p| p.span.map(|s| (s.line, s.column)));
Err(placed)
}
fn yaml_problem(e: &serde_yaml_ng::Error) -> Problem {
let mut p = Problem::new(Code::InvalidValue, FieldPath::default(), format!("not valid YAML: {e}"));
p.span = e.location().map(|l| super::Span {
line: l.line(),
column: l.column(),
});
p
}
struct Reader {
problems: Problems,
}
const FIELDS: &[&str] = &[
"version",
"name",
"networks",
"reach",
"dns",
"inputs",
"secrets",
"volumes",
"machines",
"common",
"groups",
"provision",
"controller",
"checks",
"message",
"overrides",
"tools",
"targets",
"constraints",
"cloud",
];
impl Reader {
fn spec(&mut self, mut top: Mapping) -> Spec {
let mut spec = Spec::default();
for (k, _) in &top {
let key = k.as_str().unwrap_or_default();
if !FIELDS.contains(&key) {
self.problems.push(unknown(&FieldPath::default(), key, FIELDS));
}
}
if !self.set(&mut top, "version", &mut spec.version) {
self.problems
.push(Problem::new(Code::MissingField, FieldPath::new("version"), "a spec says its format's version").fix("`version: 1`"));
}
if !self.set(&mut top, "name", &mut spec.name) {
self.problems
.push(Problem::new(Code::MissingField, FieldPath::new("name"), "a spec has a name (kebab-case)").fix("`name: my-lab`"));
}
self.set(&mut top, "networks", &mut spec.networks);
self.set(&mut top, "reach", &mut spec.reach);
self.set(&mut top, "dns", &mut spec.dns);
self.set(&mut top, "inputs", &mut spec.inputs);
self.set(&mut top, "secrets", &mut spec.secrets);
self.set(&mut top, "volumes", &mut spec.volumes);
self.set(&mut top, "common", &mut spec.common);
self.set(&mut top, "groups", &mut spec.groups);
self.set(&mut top, "provision", &mut spec.provision);
self.set(&mut top, "controller", &mut spec.controller);
self.set(&mut top, "checks", &mut spec.checks);
self.set(&mut top, "message", &mut spec.message);
self.set(&mut top, "overrides", &mut spec.overrides);
self.set(&mut top, "tools", &mut spec.tools);
self.set(&mut top, "targets", &mut spec.targets);
self.set(&mut top, "constraints", &mut spec.constraints);
self.set(&mut top, "cloud", &mut spec.cloud);
if let Some(machines) = top.remove("machines") {
self.machines(machines, &mut spec);
}
spec
}
fn set<T: DeserializeOwned>(&mut self, top: &mut Mapping, key: &str, slot: &mut T) -> bool {
let Some(v) = top.remove(key) else { return false };
if let Some(read) = self.read(v, &FieldPath::new(key)) {
*slot = read;
}
true
}
fn machines(&mut self, machines: Value, spec: &mut Spec) {
let at = FieldPath::new("machines");
let Value::Mapping(machines) = machines else {
if !machines.is_null() {
self.problems
.push(Problem::new(Code::InvalidValue, at, "machines are a map of name to machine"));
}
return;
};
for (name, m) in machines {
let Some(name) = name.as_str().map(str::to_owned) else {
self.problems.push(Problem::new(Code::InvalidValue, at.clone(), "a machine's name is a text"));
continue;
};
let m = if m.is_null() { Value::Mapping(Mapping::new()) } else { m };
if let Some(machine) = self.read(m, &at.key(&name)) {
spec.machines.insert(name, machine);
}
}
}
fn read<T: DeserializeOwned>(&mut self, value: Value, at: &FieldPath) -> Option<T> {
match serde_path_to_error::deserialize(value) {
Ok(v) => Some(v),
Err(e) => {
self.problems.push(typed_problem(at, &e));
None
}
}
}
}
fn typed_problem(at: &FieldPath, e: &serde_path_to_error::Error<serde_yaml_ng::Error>) -> Problem {
let mut path = at.clone();
for segment in e.path() {
path = match segment {
serde_path_to_error::Segment::Seq { index } => path.index(*index),
serde_path_to_error::Segment::Map { key } => path.key(key),
serde_path_to_error::Segment::Enum { variant } => path.key(variant),
serde_path_to_error::Segment::Unknown => path,
};
}
let message = e.inner().to_string();
if let Some(coded) = Mistake::decode(&message, &path) {
return coded;
}
if let Some(rest) = message.strip_prefix("unknown field `") {
let (field, expected) = rest.split_once('`').unwrap_or((rest, ""));
let known: Vec<&str> = expected.split('`').skip(1).step_by(2).collect();
let parent = if path.as_str().ends_with(&format!(".{field}")) {
path.parent().unwrap_or_default()
} else {
path
};
return unknown(&parent, field, &known);
}
if let Some(rest) = message.strip_prefix("missing field `") {
let field = rest.split('`').next().unwrap_or(rest);
return Problem::new(Code::MissingField, path.key(field), format!("`{field}` is required here"));
}
let code = if message.contains("both given") || message.contains("are all given") {
Code::Conflict
} else {
Code::InvalidValue
};
Problem::new(code, path, message)
}
fn unknown(at: &FieldPath, field: &str, known: &[&str]) -> Problem {
let p = Problem::new(Code::UnknownField, at.key(field), format!("`{field}` isn't a field here"));
match closest(field, known) {
Some(k) => p.fix(format!("did you mean `{k}`?")),
None if known.is_empty() => p,
None => p.fix(format!(
"the fields here: {}",
known.iter().map(|k| format!("`{k}`")).collect::<Vec<_>>().join(", ")
)),
}
}
#[cfg(test)]
mod tests;