1use std::fmt::{self, Display, Formatter};
2use std::path::PathBuf;
3
4use serde_json::Value;
5use thiserror::Error;
6
7use crate::loader::{FileFormat, SourceTrace};
8
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10pub struct ValidationError {
12 pub path: String,
14 pub related_paths: Vec<String>,
16 pub message: String,
18 pub rule: Option<String>,
20 pub expected: Option<Value>,
22 pub actual: Option<Value>,
24}
25
26impl ValidationError {
27 #[must_use]
29 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
30 Self {
31 path: path.into(),
32 related_paths: Vec::new(),
33 message: message.into(),
34 rule: None,
35 expected: None,
36 actual: None,
37 }
38 }
39
40 #[must_use]
42 pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
43 self.rule = Some(rule.into());
44 self
45 }
46
47 #[must_use]
49 pub fn with_related_paths<I, S>(mut self, related_paths: I) -> Self
50 where
51 I: IntoIterator<Item = S>,
52 S: Into<String>,
53 {
54 self.related_paths = related_paths.into_iter().map(Into::into).collect();
55 self
56 }
57
58 #[must_use]
60 pub fn with_expected(mut self, expected: Value) -> Self {
61 self.expected = Some(expected);
62 self
63 }
64
65 #[must_use]
67 pub fn with_actual(mut self, actual: Value) -> Self {
68 self.actual = Some(actual);
69 self
70 }
71}
72
73impl Display for ValidationError {
74 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75 if self.path.is_empty() {
76 write!(f, "{}", self.message)
77 } else {
78 write!(f, "{}: {}", self.path, self.message)
79 }
80 }
81}
82
83#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84pub struct ValidationErrors {
86 errors: Vec<ValidationError>,
87}
88
89impl ValidationErrors {
90 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
98 pub fn from_error(error: ValidationError) -> Self {
99 Self {
100 errors: vec![error],
101 }
102 }
103
104 #[must_use]
106 pub fn from_message(path: impl Into<String>, message: impl Into<String>) -> Self {
107 Self::from_error(ValidationError::new(path, message))
108 }
109
110 pub fn push(&mut self, error: ValidationError) {
112 self.errors.push(error);
113 }
114
115 pub fn extend<I>(&mut self, errors: I)
117 where
118 I: IntoIterator<Item = ValidationError>,
119 {
120 self.errors.extend(errors);
121 }
122
123 #[must_use]
125 pub fn is_empty(&self) -> bool {
126 self.errors.is_empty()
127 }
128
129 #[must_use]
131 pub fn len(&self) -> usize {
132 self.errors.len()
133 }
134
135 pub fn into_vec(self) -> Vec<ValidationError> {
137 self.errors
138 }
139
140 pub fn iter(&self) -> impl Iterator<Item = &ValidationError> {
142 self.errors.iter()
143 }
144}
145
146impl IntoIterator for ValidationErrors {
147 type Item = ValidationError;
148 type IntoIter = std::vec::IntoIter<ValidationError>;
149
150 fn into_iter(self) -> Self::IntoIter {
151 self.errors.into_iter()
152 }
153}
154
155impl Display for ValidationErrors {
156 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
157 for (index, error) in self.errors.iter().enumerate() {
158 if index > 0 {
159 writeln!(f)?;
160 }
161 write!(f, "- {error}")?;
162 }
163 Ok(())
164 }
165}
166
167impl std::error::Error for ValidationErrors {}
168
169#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
170pub struct UnknownField {
172 pub path: String,
174 pub source: Option<SourceTrace>,
176 pub suggestion: Option<String>,
178}
179
180impl UnknownField {
181 #[must_use]
183 pub fn new(path: impl Into<String>) -> Self {
184 Self {
185 path: path.into(),
186 source: None,
187 suggestion: None,
188 }
189 }
190
191 #[must_use]
193 pub fn with_source(mut self, source: Option<SourceTrace>) -> Self {
194 self.source = source;
195 self
196 }
197
198 #[must_use]
200 pub fn with_suggestion(mut self, suggestion: Option<String>) -> Self {
201 self.suggestion = suggestion;
202 self
203 }
204}
205
206impl Display for UnknownField {
207 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
208 write!(f, "unknown field `{}`", self.path)?;
209 if let Some(source) = &self.source {
210 write!(f, " from {source}")?;
211 }
212 if let Some(suggestion) = &self.suggestion {
213 write!(f, "; did you mean `{suggestion}`?")?;
214 }
215 Ok(())
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct LineColumn {
222 pub line: usize,
224 pub column: usize,
226}
227
228impl Display for LineColumn {
229 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
230 write!(f, "line {}, column {}", self.line, self.column)
231 }
232}
233
234#[derive(Debug, Error)]
235pub enum ConfigError {
237 #[error("configuration root must serialize to a map-like object, got {actual}")]
239 RootMustBeObject {
240 actual: &'static str,
242 },
243
244 #[error("failed to serialize configuration state: {source}")]
246 Serialize {
247 #[from]
249 source: serde_json::Error,
250 },
251
252 #[error("failed to watch configuration files: {message}")]
254 Watch {
255 message: String,
257 },
258
259 #[error("required configuration file not found: {}", path.display())]
261 MissingFile {
262 path: PathBuf,
264 },
265
266 #[error("none of the required configuration files were found:\n{paths}", paths = format_missing_paths(paths))]
268 MissingFiles {
269 paths: Vec<PathBuf>,
271 },
272
273 #[error("failed to read configuration file {}: {source}", path.display())]
275 ReadFile {
276 path: PathBuf,
278 #[source]
280 source: std::io::Error,
281 },
282
283 #[error("failed to parse {format} configuration file {}{location}: {message}", path.display(), location = format_location(*location))]
285 ParseFile {
286 path: PathBuf,
288 format: FileFormat,
290 location: Option<LineColumn>,
292 message: String,
294 },
295
296 #[error("invalid environment variable {name} for path {path}: {message}")]
298 InvalidEnv {
299 name: String,
301 path: String,
303 message: String,
305 },
306
307 #[error("invalid CLI argument {arg}: {message}")]
309 InvalidArg {
310 arg: String,
312 message: String,
314 },
315
316 #[error("invalid patch {name} for path {path}: {message}")]
318 InvalidPatch {
319 name: String,
321 path: String,
323 message: String,
325 },
326
327 #[error(
329 "configuration paths `{first_path}` and `{second_path}` both resolve to `{canonical_path}`"
330 )]
331 PathConflict {
332 first_path: String,
334 second_path: String,
336 canonical_path: String,
338 },
339
340 #[error(
342 "source {trace} is not allowed to set `{path}`; allowed sources: {allowed}",
343 allowed = format_source_kind_list(allowed_sources)
344 )]
345 SourcePolicyViolation {
346 path: String,
348 trace: SourceTrace,
350 allowed_sources: Vec<crate::loader::SourceKind>,
352 },
353
354 #[error(
356 "configuration object key `{key}` under {location} cannot be represented in tier paths: {message}",
357 location = format_path_location(path)
358 )]
359 InvalidPathKey {
360 path: String,
362 key: String,
364 message: String,
366 },
367
368 #[error("metadata {kind} `{name}` is assigned to both `{first_path}` and `{second_path}`")]
370 MetadataConflict {
371 kind: &'static str,
373 name: String,
375 first_path: String,
377 second_path: String,
379 },
380
381 #[error("invalid metadata for `{path}`: {message}")]
383 MetadataInvalid {
384 path: String,
386 message: String,
388 },
389
390 #[error("missing value for CLI flag {flag}")]
392 MissingArgValue {
393 flag: String,
395 },
396
397 #[error("path template {} contains {{profile}} but no profile was set", path.display())]
399 MissingProfile {
400 path: PathBuf,
402 },
403
404 #[error(
406 "failed to deserialize merged configuration at {path}: {message}{source_suffix}",
407 source_suffix = deserialize_source_suffix(provenance)
408 )]
409 Deserialize {
410 path: String,
412 provenance: Option<SourceTrace>,
414 message: String,
416 },
417
418 #[error(
420 "cannot explain configuration path `{path}` because it does not exist in the final report"
421 )]
422 ExplainPathNotFound {
423 path: String,
425 },
426
427 #[error("unknown configuration fields:\n{fields}", fields = format_unknown_fields(fields))]
429 UnknownFields {
430 fields: Vec<UnknownField>,
432 },
433
434 #[error("normalizer {name} failed: {message}")]
436 Normalize {
437 name: String,
439 message: String,
441 },
442
443 #[error("validator {name} failed:\n{errors}")]
445 Validation {
446 name: String,
448 errors: ValidationErrors,
450 },
451
452 #[error("declared validation failed:\n{errors}")]
454 DeclaredValidation {
455 errors: ValidationErrors,
457 },
458}
459
460fn format_location(location: Option<LineColumn>) -> String {
461 match location {
462 Some(location) => format!(" ({location})"),
463 None => String::new(),
464 }
465}
466
467fn format_missing_paths(paths: &[PathBuf]) -> String {
468 paths
469 .iter()
470 .map(|path| format!("- {}", path.display()))
471 .collect::<Vec<_>>()
472 .join("\n")
473}
474
475fn format_unknown_fields(fields: &[UnknownField]) -> String {
476 fields
477 .iter()
478 .map(|field| format!("- {field}"))
479 .collect::<Vec<_>>()
480 .join("\n")
481}
482
483fn format_path_location(path: &str) -> String {
484 if path.is_empty() {
485 "the configuration root".to_owned()
486 } else {
487 format!("`{path}`")
488 }
489}
490
491fn format_source_kind_list(kinds: &[crate::loader::SourceKind]) -> String {
492 kinds
493 .iter()
494 .map(ToString::to_string)
495 .collect::<Vec<_>>()
496 .join(", ")
497}
498
499fn deserialize_source_suffix(provenance: &Option<SourceTrace>) -> String {
500 provenance
501 .as_ref()
502 .map_or_else(String::new, |source| format!(" from {source}"))
503}