1use std::fmt;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Debug, Clone)]
13pub struct Error {
14 pub kind: ErrorKind,
16 pub path: Option<String>,
18 pub source_location: Option<SourceLocation>,
20 pub help: Option<String>,
22 pub cause: Option<String>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SourceLocation {
29 pub file: String,
30 pub line: Option<usize>,
31 pub column: Option<usize>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ErrorKind {
37 Parse,
39 Resolver(ResolverErrorKind),
41 Validation,
43 PathNotFound,
45 CircularReference,
47 TypeCoercion,
49 Io,
51 Internal,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum ResolverErrorKind {
58 EnvNotFound { var_name: String },
60 FileNotFound { path: String },
62 HttpError { url: String, status: Option<u16> },
64 HttpDisabled,
66 HttpNotAllowed { url: String },
68 RefNotFound { ref_path: String },
70 UnknownResolver { name: String },
72 Custom { resolver: String, message: String },
74}
75
76impl Error {
77 pub fn parse(message: impl Into<String>) -> Self {
79 Self {
80 kind: ErrorKind::Parse,
81 path: None,
82 source_location: None,
83 help: None,
84 cause: Some(message.into()),
85 }
86 }
87
88 pub fn path_not_found(path: impl Into<String>) -> Self {
90 let path_str = path.into();
91 Self {
92 kind: ErrorKind::PathNotFound,
93 path: Some(path_str.clone()),
94 source_location: None,
95 help: Some(format!(
96 "Check that '{}' exists in the configuration",
97 path_str
98 )),
99 cause: None,
100 }
101 }
102
103 pub fn circular_reference(path: impl Into<String>, chain: Vec<String>) -> Self {
105 let chain_str = chain.join(" → ");
106 Self {
107 kind: ErrorKind::CircularReference,
108 path: Some(path.into()),
109 source_location: None,
110 help: Some("Break the circular dependency by removing one of the references".into()),
111 cause: Some(format!("Chain: {}", chain_str)),
112 }
113 }
114
115 pub fn env_not_found(var_name: impl Into<String>, config_path: Option<String>) -> Self {
117 let var = var_name.into();
118 Self {
119 kind: ErrorKind::Resolver(ResolverErrorKind::EnvNotFound {
120 var_name: var.clone(),
121 }),
122 path: config_path,
123 source_location: None,
124 help: Some(format!(
125 "Set the {} environment variable or provide a default: ${{env:{},default}}",
126 var, var
127 )),
128 cause: None,
129 }
130 }
131
132 pub fn ref_not_found(ref_path: impl Into<String>, config_path: Option<String>) -> Self {
134 let ref_p = ref_path.into();
135 Self {
136 kind: ErrorKind::Resolver(ResolverErrorKind::RefNotFound {
137 ref_path: ref_p.clone(),
138 }),
139 path: config_path,
140 source_location: None,
141 help: Some(format!(
142 "Check that '{}' exists in the configuration",
143 ref_p
144 )),
145 cause: None,
146 }
147 }
148
149 pub fn file_not_found(file_path: impl Into<String>, config_path: Option<String>) -> Self {
151 let fp = file_path.into();
152 Self {
153 kind: ErrorKind::Resolver(ResolverErrorKind::FileNotFound { path: fp.clone() }),
154 path: config_path,
155 source_location: None,
156 help: Some("Check that the file exists relative to the config file".into()),
157 cause: None,
158 }
159 }
160
161 pub fn unknown_resolver(name: impl Into<String>, config_path: Option<String>) -> Self {
163 let n = name.into();
164 Self {
165 kind: ErrorKind::Resolver(ResolverErrorKind::UnknownResolver { name: n.clone() }),
166 path: config_path,
167 source_location: None,
168 help: Some(format!("Register the '{}' resolver or check for typos", n)),
169 cause: None,
170 }
171 }
172
173 pub fn type_coercion(
175 path: impl Into<String>,
176 expected: impl Into<String>,
177 got: impl Into<String>,
178 ) -> Self {
179 Self {
180 kind: ErrorKind::TypeCoercion,
181 path: Some(path.into()),
182 source_location: None,
183 help: Some(format!(
184 "Ensure the value can be converted to {}",
185 expected.into()
186 )),
187 cause: Some(format!("Got: {}", got.into())),
188 }
189 }
190
191 pub fn validation(path: impl Into<String>, message: impl Into<String>) -> Self {
193 let p = path.into();
194 Self {
195 kind: ErrorKind::Validation,
196 path: if p.is_empty() || p == "<root>" {
197 None
198 } else {
199 Some(p)
200 },
201 source_location: None,
202 help: Some("Fix the value to match the schema requirements".into()),
203 cause: Some(message.into()),
204 }
205 }
206
207 pub fn with_path(mut self, path: impl Into<String>) -> Self {
209 self.path = Some(path.into());
210 self
211 }
212
213 pub fn with_source_location(mut self, loc: SourceLocation) -> Self {
215 self.source_location = Some(loc);
216 self
217 }
218
219 pub fn with_help(mut self, help: impl Into<String>) -> Self {
221 self.help = Some(help.into());
222 self
223 }
224}
225
226impl fmt::Display for Error {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match &self.kind {
230 ErrorKind::Parse => write!(f, "Parse error")?,
231 ErrorKind::Resolver(r) => match r {
232 ResolverErrorKind::EnvNotFound { var_name } => {
233 write!(f, "Environment variable not found: {}", var_name)?
234 }
235 ResolverErrorKind::FileNotFound { path } => write!(f, "File not found: {}", path)?,
236 ResolverErrorKind::HttpError { url, status } => {
237 write!(f, "HTTP request failed: {}", url)?;
238 if let Some(s) = status {
239 write!(f, " (status {})", s)?;
240 }
241 }
242 ResolverErrorKind::HttpDisabled => write!(f, "HTTP resolver is disabled")?,
243 ResolverErrorKind::HttpNotAllowed { url } => {
244 write!(f, "URL not in allowlist: {}", url)?
245 }
246 ResolverErrorKind::RefNotFound { ref_path } => {
247 write!(f, "Referenced path not found: {}", ref_path)?
248 }
249 ResolverErrorKind::UnknownResolver { name } => {
250 write!(f, "Unknown resolver: {}", name)?
251 }
252 ResolverErrorKind::Custom { resolver, message } => {
253 write!(f, "Resolver '{}' error: {}", resolver, message)?
254 }
255 },
256 ErrorKind::Validation => write!(f, "Validation error")?,
257 ErrorKind::PathNotFound => write!(f, "Path not found")?,
258 ErrorKind::CircularReference => write!(f, "Circular reference detected")?,
259 ErrorKind::TypeCoercion => write!(f, "Type coercion failed")?,
260 ErrorKind::Io => write!(f, "I/O error")?,
261 ErrorKind::Internal => write!(f, "Internal error")?,
262 }
263
264 if let Some(path) = &self.path {
266 write!(f, "\n Path: {}", path)?;
267 }
268
269 if let Some(loc) = &self.source_location {
271 write!(f, "\n File: {}", loc.file)?;
272 if let Some(line) = loc.line {
273 write!(f, ":{}", line)?;
274 }
275 }
276
277 if let Some(cause) = &self.cause {
279 write!(f, "\n {}", cause)?;
280 }
281
282 if let Some(help) = &self.help {
284 write!(f, "\n Help: {}", help)?;
285 }
286
287 Ok(())
288 }
289}
290
291impl std::error::Error for Error {}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_env_not_found_error_display() {
299 let err = Error::env_not_found("MY_VAR", Some("database.password".into()));
300 let display = format!("{}", err);
301
302 assert!(display.contains("Environment variable not found: MY_VAR"));
303 assert!(display.contains("Path: database.password"));
304 assert!(display.contains("Help:"));
305 assert!(display.contains("${env:MY_VAR,default}"));
306 }
307
308 #[test]
309 fn test_circular_reference_error_display() {
310 let err = Error::circular_reference(
311 "config.a",
312 vec!["a".into(), "b".into(), "c".into(), "a".into()],
313 );
314 let display = format!("{}", err);
315
316 assert!(display.contains("Circular reference detected"));
317 assert!(display.contains("a → b → c → a"));
318 }
319
320 #[test]
321 fn test_path_not_found_error() {
322 let err = Error::path_not_found("database.host");
323
324 assert_eq!(err.kind, ErrorKind::PathNotFound);
325 assert_eq!(err.path, Some("database.host".into()));
326 }
327}