schematic/config/
error.rs

1use super::merger::MergeError;
2use super::parser::ParserError;
3#[cfg(feature = "validate")]
4use super::validator::ValidatorError;
5use miette::Diagnostic;
6use starbase_styles::{Style, Stylize};
7use std::fmt::Display;
8use std::path::PathBuf;
9use thiserror::Error;
10
11/// All configuration based errors.
12#[derive(Error, Debug, Diagnostic)]
13pub enum ConfigError {
14    #[error(transparent)]
15    Handler(#[from] Box<HandlerError>),
16
17    #[error(transparent)]
18    Merge(#[from] Box<MergeError>),
19
20    #[diagnostic(code(config::enums::invalid_fallback))]
21    #[error("Invalid fallback variant {}, unable to parse type.", .0.style(Style::Symbol))]
22    EnumInvalidFallback(String),
23
24    #[diagnostic(code(config::enums::unknown_variant))]
25    #[error("Unknown enum variant {}.", .0.style(Style::Id))]
26    EnumUnknownVariant(String),
27
28    #[diagnostic(code(config::extends::no_source_code))]
29    #[error("Unable to extend, expected a file path or secure URL.")]
30    ExtendsFromNoCode,
31
32    #[diagnostic(code(config::extends::only_parent_file))]
33    #[error("Extending from a file is only allowed if the parent source is also a file.")]
34    ExtendsFromParentFileOnly,
35
36    #[diagnostic(code(config::url::https_only))]
37    #[error("Only secure URLs are allowed, received {}.", .0.style(Style::Url))]
38    HttpsOnly(String),
39
40    #[diagnostic(code(config::code::invalid))]
41    #[error("Invalid code block used as a source.")]
42    InvalidCode,
43
44    #[diagnostic(code(config::default::invalid))]
45    #[error("Invalid default value. {0}")]
46    InvalidDefaultValue(String),
47
48    #[diagnostic(code(config::file::invalid))]
49    #[error("Invalid file path used as a source.")]
50    InvalidFile,
51
52    #[diagnostic(code(config::url::invalid))]
53    #[error("Invalid URL used as a source.")]
54    InvalidUrl,
55
56    #[diagnostic(code(config::file::missing), help("Is the path absolute?"))]
57    #[error("File path {} does not exist.", .0.style(Style::Path))]
58    MissingFile(PathBuf),
59
60    #[diagnostic(
61        code(config::format::no_matching),
62        help("Is there a format registered for the file extension?")
63    )]
64    #[error(
65        "Unable to parse {} as there's no matching source format for extension {}.",
66        .src.style(Style::Path),
67        .ext.style(Style::File)
68    )]
69    NoMatchingFormat { src: String, ext: String },
70
71    #[diagnostic(code(config::file::read_failed))]
72    #[error("Failed to read file {}.", .path.style(Style::Path))]
73    ReadFileFailed {
74        path: PathBuf,
75        #[source]
76        error: Box<std::io::Error>,
77    },
78
79    #[cfg(feature = "url")]
80    #[diagnostic(code(config::url::read_failed))]
81    #[error("Failed to read URL {}.", .url.style(Style::Url))]
82    ReadUrlFailed {
83        url: String,
84        #[source]
85        error: Box<reqwest::Error>,
86    },
87
88    #[cfg(feature = "json")]
89    #[diagnostic(code(config::json::failed))]
90    #[error("Failed to strip comments from {}.", .file.style(Style::File))]
91    JsonStripCommentsFailed {
92        file: String,
93        #[source]
94        error: Box<std::io::Error>,
95    },
96
97    #[cfg(feature = "pkl")]
98    #[diagnostic(code(config::pkl::failed))]
99    #[error("Failed to evaluate Pkl file {}.", .path.style(Style::Path))]
100    PklEvalFailed {
101        path: PathBuf,
102        #[source]
103        error: Box<rpkl::Error>,
104    },
105
106    #[cfg(feature = "pkl")]
107    #[diagnostic(code(config::pkl::file_required))]
108    #[error("Pkl requires local file paths to evaluate, received a code snippet or URL.")]
109    PklFileRequired,
110
111    #[cfg(feature = "pkl")]
112    #[diagnostic(code(config::pkl::binary_required))]
113    #[error(
114        "Pkl configuration requires the {} binary to be installed and available.\nLearn more: {}",
115        "pkl".style(Style::Shell),
116        "https://pkl-lang.org/main/current/pkl-cli/index.html".style(Style::Url)
117    )]
118    PklRequired,
119
120    // Parser
121    #[diagnostic(code(config::parse::failed))]
122    #[error("Failed to parse {}.", .location.style(Style::File))]
123    Parser {
124        location: String,
125
126        #[diagnostic_source]
127        #[source]
128        error: Box<ParserError>,
129
130        #[help]
131        help: Option<String>,
132    },
133
134    // Validator
135    #[cfg(feature = "validate")]
136    #[diagnostic(code(config::validate::failed))]
137    #[error("Failed to validate {}.", .location.style(Style::File))]
138    Validator {
139        location: String,
140
141        #[diagnostic_source]
142        #[source]
143        error: Box<ValidatorError>,
144
145        #[help]
146        help: Option<String>,
147    },
148}
149
150impl ConfigError {
151    /// Return a full error string, disregarding `miette` diagnostic structure.
152    /// This is extremely useful for debugging and tests, and less for application use.
153    pub fn to_full_string(&self) -> String {
154        let mut message = self.to_string();
155
156        let mut push_end = || {
157            if !message.ends_with('\n') {
158                if !message.ends_with('.') && !message.ends_with(':') {
159                    message.push('.');
160                }
161                message.push(' ');
162            }
163        };
164
165        match self {
166            #[cfg(feature = "pkl")]
167            ConfigError::PklEvalFailed { error: inner, .. } => {
168                push_end();
169                message.push_str(&inner.to_string());
170            }
171            ConfigError::ReadFileFailed { error: inner, .. } => {
172                push_end();
173                message.push_str(&inner.to_string());
174            }
175            #[cfg(feature = "url")]
176            ConfigError::ReadUrlFailed { error: inner, .. } => {
177                push_end();
178                message.push_str(&inner.to_string());
179            }
180            ConfigError::Parser { error: inner, .. } => {
181                push_end();
182                message.push_str(&inner.to_string());
183            }
184            #[cfg(feature = "validate")]
185            ConfigError::Validator { error: inner, .. } => {
186                push_end();
187                for error in &inner.errors {
188                    message.push_str(format!("\n  {error}").as_str());
189                }
190            }
191            _ => {}
192        };
193
194        message.trim().to_string()
195    }
196}
197
198impl From<HandlerError> for ConfigError {
199    fn from(error: HandlerError) -> ConfigError {
200        ConfigError::Handler(Box::new(error))
201    }
202}
203
204impl From<MergeError> for ConfigError {
205    fn from(error: MergeError) -> ConfigError {
206        ConfigError::Merge(Box::new(error))
207    }
208}
209
210impl From<ParserError> for ConfigError {
211    fn from(error: ParserError) -> ConfigError {
212        ConfigError::Parser {
213            location: String::new(),
214            error: Box::new(error),
215            help: None,
216        }
217    }
218}
219
220/// Error for handler functions.
221#[derive(Error, Debug, Diagnostic)]
222#[error("{0}")]
223pub struct HandlerError(pub String);
224
225impl HandlerError {
226    pub fn new<T: Display>(message: T) -> Self {
227        Self(message.to_string())
228    }
229}