Skip to main content

isoloom_spec/syntax/
parse.rs

1//! YAML text → [`Spec`], or every problem found on the way, coded and placed. The old keys are
2//! reported first (with their fixes), `count:` clones and the shared fields fold in, then each
3//! top-level section and each machine is read on its own, so one mistake doesn't hide the next.
4
5use serde::de::DeserializeOwned;
6use serde_yaml_ng::{Mapping, Value};
7
8use super::mistake::Mistake;
9use super::problem::{Code, FieldPath, Problem, Problems, closest};
10use super::spans::Spans;
11use super::{Spec, old_keys};
12
13/// Reads a spec from its YAML text: the spec, or its problems in file order.
14///
15/// # Errors
16///
17/// Every problem found, coded and placed: YAML that doesn't parse, the earlier format's keys,
18/// unknown and missing fields, values of the wrong type or shape.
19pub fn parse(yaml: &str) -> Result<Spec, Problems> {
20    let spans = Spans::of(yaml);
21    let mut doc: Value = serde_yaml_ng::from_str(yaml).map_err(|e| vec![yaml_problem(&e)])?;
22    let mut problems = old_keys::scan(&doc);
23    let clones = crate::clones::expand(&mut doc).unwrap_or_else(|refused| {
24        problems.push(refused);
25        indexmap::IndexMap::new()
26    });
27    if let Err(refused) = crate::groups::expand(&mut doc) {
28        problems.push(refused);
29    }
30    let Value::Mapping(top) = doc else {
31        return Err(vec![Problem::new(
32            Code::InvalidValue,
33            FieldPath::default(),
34            "a spec is a mapping of fields (`version`, `name`, `machines`, ...)",
35        )]);
36    };
37    let mut reader = Reader { problems: Vec::new() };
38    let mut spec = reader.spec(top);
39    spec.clones = clones;
40    // A typed problem at or inside an old key's field repeats it: the old key's fix says it all.
41    let old = problems.clone();
42    problems.extend(reader.problems.into_iter().filter(|p| !old.iter().any(|o| p.path.within(&o.path))));
43    if problems.is_empty() {
44        spec.spans = spans;
45        return Ok(spec);
46    }
47    let mut placed: Problems = problems.into_iter().map(|p| spans.place(p)).collect();
48    placed.sort_by_key(|p| p.span.map(|s| (s.line, s.column)));
49    Err(placed)
50}
51
52/// A YAML syntax error, at its line.
53fn yaml_problem(e: &serde_yaml_ng::Error) -> Problem {
54    let mut p = Problem::new(Code::InvalidValue, FieldPath::default(), format!("not valid YAML: {e}"));
55    p.span = e.location().map(|l| super::Span {
56        line: l.line(),
57        column: l.column(),
58    });
59    p
60}
61
62/// Reads typed values out of the document, keeping every problem.
63struct Reader {
64    problems: Problems,
65}
66
67/// The top-level fields, in the order the schema lists them.
68const FIELDS: &[&str] = &[
69    "version",
70    "name",
71    "networks",
72    "reach",
73    "dns",
74    "inputs",
75    "secrets",
76    "volumes",
77    "machines",
78    "common",
79    "groups",
80    "provision",
81    "controller",
82    "checks",
83    "message",
84    "overrides",
85    "tools",
86    "targets",
87    "constraints",
88    "cloud",
89];
90
91impl Reader {
92    /// The spec's sections, each read on its own.
93    fn spec(&mut self, mut top: Mapping) -> Spec {
94        let mut spec = Spec::default();
95        for (k, _) in &top {
96            let key = k.as_str().unwrap_or_default();
97            if !FIELDS.contains(&key) {
98                self.problems.push(unknown(&FieldPath::default(), key, FIELDS));
99            }
100        }
101        if !self.set(&mut top, "version", &mut spec.version) {
102            self.problems
103                .push(Problem::new(Code::MissingField, FieldPath::new("version"), "a spec says its format's version").fix("`version: 1`"));
104        }
105        if !self.set(&mut top, "name", &mut spec.name) {
106            self.problems
107                .push(Problem::new(Code::MissingField, FieldPath::new("name"), "a spec has a name (kebab-case)").fix("`name: my-lab`"));
108        }
109        self.set(&mut top, "networks", &mut spec.networks);
110        self.set(&mut top, "reach", &mut spec.reach);
111        self.set(&mut top, "dns", &mut spec.dns);
112        self.set(&mut top, "inputs", &mut spec.inputs);
113        self.set(&mut top, "secrets", &mut spec.secrets);
114        self.set(&mut top, "volumes", &mut spec.volumes);
115        self.set(&mut top, "common", &mut spec.common);
116        self.set(&mut top, "groups", &mut spec.groups);
117        self.set(&mut top, "provision", &mut spec.provision);
118        self.set(&mut top, "controller", &mut spec.controller);
119        self.set(&mut top, "checks", &mut spec.checks);
120        self.set(&mut top, "message", &mut spec.message);
121        self.set(&mut top, "overrides", &mut spec.overrides);
122        self.set(&mut top, "tools", &mut spec.tools);
123        self.set(&mut top, "targets", &mut spec.targets);
124        self.set(&mut top, "constraints", &mut spec.constraints);
125        self.set(&mut top, "cloud", &mut spec.cloud);
126        if let Some(machines) = top.remove("machines") {
127            self.machines(machines, &mut spec);
128        }
129        spec
130    }
131
132    /// Reads field `key` of `top` into `slot`; whether the field was written.
133    fn set<T: DeserializeOwned>(&mut self, top: &mut Mapping, key: &str, slot: &mut T) -> bool {
134        let Some(v) = top.remove(key) else { return false };
135        if let Some(read) = self.read(v, &FieldPath::new(key)) {
136            *slot = read;
137        }
138        true
139    }
140
141    /// Each machine, read on its own.
142    fn machines(&mut self, machines: Value, spec: &mut Spec) {
143        let at = FieldPath::new("machines");
144        let Value::Mapping(machines) = machines else {
145            if !machines.is_null() {
146                self.problems
147                    .push(Problem::new(Code::InvalidValue, at, "machines are a map of name to machine"));
148            }
149            return;
150        };
151        for (name, m) in machines {
152            let Some(name) = name.as_str().map(str::to_owned) else {
153                self.problems.push(Problem::new(Code::InvalidValue, at.clone(), "a machine's name is a text"));
154                continue;
155            };
156            let m = if m.is_null() { Value::Mapping(Mapping::new()) } else { m };
157            if let Some(machine) = self.read(m, &at.key(&name)) {
158                spec.machines.insert(name, machine);
159            }
160        }
161    }
162
163    /// `value` read as a `T`, or its problem kept.
164    fn read<T: DeserializeOwned>(&mut self, value: Value, at: &FieldPath) -> Option<T> {
165        match serde_path_to_error::deserialize(value) {
166            Ok(v) => Some(v),
167            Err(e) => {
168                self.problems.push(typed_problem(at, &e));
169                None
170            }
171        }
172    }
173}
174
175/// A typed read's error as a problem: the path down to the field, the code its message says.
176fn typed_problem(at: &FieldPath, e: &serde_path_to_error::Error<serde_yaml_ng::Error>) -> Problem {
177    let mut path = at.clone();
178    for segment in e.path() {
179        path = match segment {
180            serde_path_to_error::Segment::Seq { index } => path.index(*index),
181            serde_path_to_error::Segment::Map { key } => path.key(key),
182            serde_path_to_error::Segment::Enum { variant } => path.key(variant),
183            serde_path_to_error::Segment::Unknown => path,
184        };
185    }
186    let message = e.inner().to_string();
187    if let Some(coded) = Mistake::decode(&message, &path) {
188        return coded;
189    }
190    if let Some(rest) = message.strip_prefix("unknown field `") {
191        let (field, expected) = rest.split_once('`').unwrap_or((rest, ""));
192        let known: Vec<&str> = expected.split('`').skip(1).step_by(2).collect();
193        // The path may already end at the unknown field.
194        let parent = if path.as_str().ends_with(&format!(".{field}")) {
195            path.parent().unwrap_or_default()
196        } else {
197            path
198        };
199        return unknown(&parent, field, &known);
200    }
201    if let Some(rest) = message.strip_prefix("missing field `") {
202        let field = rest.split('`').next().unwrap_or(rest);
203        return Problem::new(Code::MissingField, path.key(field), format!("`{field}` is required here"));
204    }
205    let code = if message.contains("both given") || message.contains("are all given") {
206        Code::Conflict
207    } else {
208        Code::InvalidValue
209    };
210    Problem::new(code, path, message)
211}
212
213/// An unknown field, with the closest known one as the fix.
214fn unknown(at: &FieldPath, field: &str, known: &[&str]) -> Problem {
215    let p = Problem::new(Code::UnknownField, at.key(field), format!("`{field}` isn't a field here"));
216    match closest(field, known) {
217        Some(k) => p.fix(format!("did you mean `{k}`?")),
218        None if known.is_empty() => p,
219        None => p.fix(format!(
220            "the fields here: {}",
221            known.iter().map(|k| format!("`{k}`")).collect::<Vec<_>>().join(", ")
222        )),
223    }
224}
225
226#[cfg(test)]
227mod tests;