1use 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
13pub 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 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
52fn 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
62struct Reader {
64 problems: Problems,
65}
66
67const 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 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 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 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 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
175fn 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 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
213fn 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;