Skip to main content

adnv/
error.rs

1use std::path::PathBuf;
2
3pub type Result<T, E = Error> = std::result::Result<T, E>;
4
5#[derive(Debug, thiserror::Error, miette::Diagnostic)]
6pub enum Error {
7    // common
8    //
9    #[error("Templating rendering error")]
10    #[diagnostic(code(template))]
11    TeraError(#[from] tera::Error),
12
13    #[error("Could not parse datetime")]
14    #[diagnostic(code(datetime_parsing))]
15    Chrono(#[from] chrono::ParseError),
16
17    #[error("Date or duration out of range")]
18    #[diagnostic(code(duration_out_of_range))]
19    OutOfRangeDate(#[from] chrono::OutOfRangeError),
20
21    #[error("{msg}")]
22    #[diagnostic(code(io))]
23    Io {
24        msg: String,
25        #[source]
26        err: std::io::Error,
27    },
28
29    #[error("Could not create temporary directory")]
30    #[diagnostic(code(tempdir))]
31    TempDirectory(#[source] std::io::Error),
32
33    #[error("{count} source(s) could not be checked")]
34    #[diagnostic(
35        code(check::sources_failed),
36        help("The errors are reported above. Fix them or drop the sources.")
37    )]
38    SourcesFailed { count: usize },
39
40    #[error("{msg}")]
41    #[diagnostic(code(git))]
42    Git {
43        msg: String,
44        #[source]
45        err: git2::Error,
46    },
47
48    #[error("{msg}")]
49    #[diagnostic(code(github))]
50    GitHub {
51        msg: String,
52        #[source]
53        err: octocrab::Error,
54    },
55
56    #[error("{msg}")]
57    #[diagnostic(code(gitlab))]
58    GitLab {
59        msg: String,
60        #[source]
61        err: gitlab::GitlabError,
62    },
63
64    #[error("{msg}")]
65    #[diagnostic(code(gitlab))]
66    GitLabBoxed {
67        msg: String,
68        #[source]
69        err: Box<dyn std::error::Error + Send + Sync + 'static>,
70    },
71
72    #[error("Valid env var required `{name}`: {msg}")]
73    #[diagnostic(code(env))]
74    Env {
75        name: String,
76        msg: String,
77        #[source]
78        err: std::env::VarError,
79    },
80
81    #[error(transparent)]
82    #[diagnostic(code(http))]
83    ReqwestError(#[from] reqwest::Error),
84
85    #[error(transparent)]
86    #[diagnostic(code(yaml))]
87    SerdeYamlError(#[from] serde_yaml_ng::Error),
88
89    #[error(transparent)]
90    #[diagnostic(code(json))]
91    SerdeJsonError(#[from] serde_json::Error),
92
93    #[error(transparent)]
94    Glob(#[from] wax::BuildError),
95
96    #[error(transparent)]
97    Walk(#[from] wax::walk::WalkError),
98
99    #[error(transparent)]
100    EnvExpand(#[from] shellexpand::LookupError<std::env::VarError>),
101
102    // config
103    //
104    #[error("Could not decode config file at `{}`", path.display())]
105    DeserializeConfig {
106        path: PathBuf,
107        #[source]
108        err: serde_yaml_ng::Error,
109    },
110
111    #[error(
112        "Unsupported config version (required: {}, got: {})",
113        required,
114        current
115    )]
116    UnsupportedConfigVersion { required: String, current: String },
117
118    // run::codegen
119    //
120    #[error("No input in pipe")]
121    #[diagnostic(code(adnv::no_input_in_pipe))]
122    NoInputInPipe,
123
124    #[error("Could not delete directory: {}", path.display())]
125    CouldNotDeleteDirectory {
126        path: PathBuf,
127        #[source]
128        err: std::io::Error,
129    },
130
131    #[error("Could not copy directory: {} -> {}", source.display(), destination.display())]
132    CouldNotCopyDirectory {
133        source: PathBuf,
134        destination: PathBuf,
135        #[source]
136        err: std::io::Error,
137    },
138
139    #[error("Could not copy file: {} -> {}", source.display(), destination.display())]
140    CouldNotCopyFile {
141        source: PathBuf,
142        destination: PathBuf,
143        #[source]
144        err: std::io::Error,
145    },
146
147    #[error("Could not open or create file: {}", path.display())]
148    CouldNotOpenFile {
149        path: PathBuf,
150        #[source]
151        err: std::io::Error,
152    },
153
154    #[error("Refusing to copy a symbolic link: {}", path.display())]
155    #[diagnostic(
156        code(fs::symlink_in_source),
157        help(
158            "A tree fetched from a source may not hold symbolic links: copying one verbatim would let a remote source reach outside the generated output."
159        )
160    )]
161    SymlinkInSource { path: PathBuf },
162
163    #[error("Could not update {}: the file changed since it was read", path.display())]
164    #[diagnostic(
165        code(edit::file_changed),
166        help("Run the command again so the versions are located in the file as it is now.")
167    )]
168    FileChangedSinceParsed {
169        path: PathBuf,
170        #[source]
171        err: ropey::Error,
172    },
173
174    #[error("Could not write content in file: {}", path.display())]
175    CouldNotWriteFile {
176        path: PathBuf,
177        #[source]
178        err: std::io::Error,
179    },
180
181    #[error("Could not strip prefix: {} from path: {}", path.display(), to_strip.display())]
182    CouldNotStripPrefix {
183        path: PathBuf,
184        to_strip: PathBuf,
185        #[source]
186        err: std::path::StripPrefixError,
187    },
188
189    #[error("Could not fetch URL `{url}`: {msg}")]
190    #[diagnostic(code(fetch_error))]
191    UrlFetch {
192        msg: String,
193        url: url::Url,
194        #[source]
195        err: reqwest::Error,
196    },
197
198    #[error("Invalid archive: {msg}")]
199    #[diagnostic(code(invalid_archive))]
200    InvalidArchive { msg: String, url: url::Url },
201
202    #[error("Could not create intermediate directory")]
203    IntermediateDirectory(#[source] std::io::Error),
204
205    // check
206    //
207    #[error("Unknown source type: {0}")]
208    UnknownSourceType(url::Url),
209
210    #[error("The URL fragment is missing for source: {0}")]
211    URLFragmentMissing(url::Url),
212
213    #[error("{msg}")]
214    BadQueryString {
215        msg: String,
216        #[source]
217        err: serde_qs::Error,
218    },
219
220    #[error("Could not send post to Mattermost")]
221    MattermostPost(
222        #[from]
223        mattermost_client::openapi::apis::Error<
224            mattermost_client::openapi::apis::posts_api::CreatePostError,
225        >,
226    ),
227
228    #[error("There is no outputs in config file")]
229    NoOutputsConfigFile,
230
231    #[error("There is no matching output in config file for \"{0}\"")]
232    NoMatchingOutputConfigFile(String),
233
234    #[error(
235        "The URL '{0}' doesn't match URL structure for npm registry. Must be npm://<PACKAGE NAME> or npm+<REGISTRY URL>/<PACKAGE NAME>"
236    )]
237    NpmRegistryURL(String),
238
239    // check::output
240    //
241    #[error("Invalid entry user_id in config file: {user_id}")]
242    InvalidUserId {
243        user_id: String,
244        #[source]
245        err: matrix_sdk::ruma::IdParseError,
246    },
247
248    #[error("Entry user_password in config file doesn't match user_id : {user_id} ")]
249    NonMatchingUserIdPassword {
250        user_id: String,
251        #[source]
252        err: matrix_sdk::Error,
253    },
254
255    #[error("Invalid entry room_id in config file: {room_id}")]
256    InvalidRoomId {
257        room_id: String,
258        #[source]
259        err: matrix_sdk::ruma::IdParseError,
260    },
261
262    #[error("Impossible to join room: {room_id}")]
263    ImpossibleToJoinRoom {
264        room_id: String,
265        #[source]
266        err: matrix_sdk::Error,
267    },
268
269    #[error("Impossible to get joined room {0} from client")]
270    ImpossibleToGetRoom(String),
271
272    #[error(transparent)]
273    BuildServer(#[from] matrix_sdk::ClientBuildError),
274
275    #[error(transparent)]
276    SyncServerState(#[from] matrix_sdk::Error),
277
278    #[error(transparent)]
279    ClientLogout(#[from] matrix_sdk::HttpError),
280
281    #[error("Invalid sender mail: \"{mail}\"")]
282    InvalidSenderMail {
283        mail: String,
284        #[source]
285        err: lettre::address::AddressError,
286    },
287    #[error("Invalid recipient mail: \"{mail}\"")]
288    InvalidRecipientMail {
289        mail: String,
290        #[source]
291        err: lettre::address::AddressError,
292    },
293
294    #[error(transparent)]
295    SMTP(#[from] lettre::transport::smtp::Error),
296
297    // sources
298    //
299    #[error(transparent)]
300    TerraformRegistry(#[from] terraform_registry::Error),
301
302    #[error("Version should be described following terraform version constraint syntax")]
303    TerraformVersion(#[from] terraform_version::Error),
304
305    #[error("Version requirements is not a valid SemVer version requirement")]
306    SemVerVersion(#[from] semver::Error),
307
308    #[error("There are no version available")]
309    NoVersionAvailable,
310
311    #[error("The URL fragment '{0}' doesn't match any entry from repository '{1}'")]
312    HelmEntryError(String, String),
313
314    #[error("Invalid source `{0}`: {1}")]
315    InvalidSource(url::Url, String),
316
317    #[error("Unsupported Docker Registry: {0}")]
318    UnsupportedDockerRegistry(url::Url),
319
320    #[error("No version described")]
321    NoVersionDescribed,
322
323    #[error("No comparator in the VersionRequirement")]
324    NoComparator,
325
326    // sources::parse
327    //
328    #[error(transparent)]
329    HclParse(#[from] crate::parsers::hcl::Error),
330
331    #[error(transparent)]
332    YamlParse(#[from] marked_yaml::LoadError),
333
334    #[error(transparent)]
335    URLParse(#[from] url::ParseError),
336
337    #[error("Invalid URL: {}, parsing impossible", repo_url)]
338    InvalidUrl {
339        repo_url: String,
340        #[source]
341        err: url::ParseError,
342    },
343
344    #[error("Inline hint error on file {file}")]
345    HclParsing {
346        file: String,
347        #[source]
348        err: crate::parsers::hcl::ast::error::Error,
349    },
350
351    #[error(
352        "Invalid inline hint source: {}, see documentation for more information about inline hints format",
353        repo_url
354    )]
355    InvalidInlineHintSource {
356        repo_url: String,
357        #[source]
358        err: Box<Error>,
359    },
360}
361
362impl Error {
363    pub(crate) fn io(err: std::io::Error, msg: impl Into<String>) -> Error {
364        Error::Io {
365            msg: msg.into(),
366            err,
367        }
368    }
369    pub(crate) fn git(err: git2::Error, msg: impl Into<String>) -> Self {
370        Self::Git {
371            msg: msg.into(),
372            err,
373        }
374    }
375    pub(crate) fn github(err: octocrab::Error, msg: impl Into<String>) -> Error {
376        Error::GitHub {
377            msg: msg.into(),
378            err,
379        }
380    }
381
382    pub(crate) fn gitlab(err: impl Into<gitlab::GitlabError>, msg: impl Into<String>) -> Error {
383        Error::GitLab {
384            msg: msg.into(),
385            err: err.into(),
386        }
387    }
388
389    pub(crate) fn gitlab_boxed(
390        err: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
391        msg: impl Into<String>,
392    ) -> Error {
393        Error::GitLabBoxed {
394            msg: msg.into(),
395            err: err.into(),
396        }
397    }
398
399    pub(crate) fn env(
400        err: std::env::VarError,
401        name: impl Into<String>,
402        msg: impl Into<String>,
403    ) -> Error {
404        Error::Env {
405            name: name.into(),
406            msg: msg.into(),
407            err,
408        }
409    }
410
411    // config
412    //
413    pub(crate) fn deserialize_config(err: serde_yaml_ng::Error, path: impl Into<PathBuf>) -> Error {
414        Error::DeserializeConfig {
415            path: path.into(),
416            err,
417        }
418    }
419
420    // check
421    //
422    pub(crate) fn serde_qs(err: serde_qs::Error, msg: impl Into<String>) -> Self {
423        Self::BadQueryString {
424            msg: msg.into(),
425            err,
426        }
427    }
428
429    // run::codegen
430    //
431    #[inline]
432    pub(crate) fn copy_directory(
433        err: std::io::Error,
434        source: impl Into<PathBuf>,
435        dest: impl Into<PathBuf>,
436    ) -> Error {
437        Error::CouldNotCopyDirectory {
438            source: source.into(),
439            destination: dest.into(),
440            err,
441        }
442    }
443
444    #[inline]
445    pub(crate) fn invalid_archive(url: url::Url, msg: impl Into<String>) -> Error {
446        Error::InvalidArchive {
447            msg: msg.into(),
448            url,
449        }
450    }
451
452    #[inline]
453    pub(crate) fn url_fetch(err: reqwest::Error, url: url::Url, msg: impl Into<String>) -> Error {
454        Error::UrlFetch {
455            msg: msg.into(),
456            url,
457            err,
458        }
459    }
460
461    // sources
462    //
463    pub(crate) fn invalid_source(url: url::Url, msg: impl Into<String>) -> Error {
464        Error::InvalidSource(url, msg.into())
465    }
466
467    // sources::parse
468    //
469    pub(crate) fn invalid_inline_hint(err: Error, repo_url: impl Into<String>) -> Error {
470        Error::InvalidInlineHintSource {
471            repo_url: repo_url.into(),
472            err: Box::new(err),
473        }
474    }
475}
476
477/// Boxed so that `Result<_, ErrorWithData<_>>` stays small enough to pass
478/// around by value, whatever the size of the attached data.
479#[derive(Debug)]
480pub struct ErrorWithData<T>(Box<(Error, T)>);
481
482impl<T> ErrorWithData<T> {
483    pub fn error(&self) -> &Error {
484        &self.0.0
485    }
486
487    pub fn data(&self) -> &T {
488        &self.0.1
489    }
490
491    pub fn split(self) -> (Error, T) {
492        *self.0
493    }
494}
495
496impl Error {
497    pub fn with_data<T>(self, data: T) -> ErrorWithData<T> {
498        ErrorWithData(Box::new((self, data)))
499    }
500}
501
502#[derive(Debug)]
503pub struct Results<T, E = Error>(Vec<T>, Vec<E>);
504
505impl<T, E> Results<T, E> {
506    pub fn values(&self) -> &[T] {
507        &self.0
508    }
509
510    pub fn errors(&self) -> &[E] {
511        &self.1
512    }
513
514    pub fn split(self) -> (Vec<T>, Vec<E>) {
515        (self.0, self.1)
516    }
517}
518
519// No `Deref<Target = [T]>` nor `AsRef<[T]>` on purpose: both let a `Results`
520// coerce into its successful values alone, which silently drops the errors at
521// the call site. Ask for `values()`, `errors()` or `split()` explicitly.
522
523impl<T, E> From<(Vec<T>, Vec<E>)> for Results<T, E> {
524    fn from((values, errors): (Vec<T>, Vec<E>)) -> Self {
525        Self(values, errors)
526    }
527}
528
529impl<T, E> From<Vec<Result<T, E>>> for Results<T, E> {
530    fn from(results: Vec<Result<T, E>>) -> Self {
531        use itertools::Itertools;
532
533        let (values, errors) = results.into_iter().partition_result();
534        Self(values, errors)
535    }
536}