Skip to main content

dependency_check_updates_core/
error.rs

1//! The crate-wide error type [`DcuError`] returned by manifest, registry, and
2//! patch operations.
3
4use miette::Diagnostic;
5use std::path::PathBuf;
6use thiserror::Error;
7
8/// Main error type for dependency-check-updates operations.
9///
10/// Marked `#[non_exhaustive]` so new error variants can be added in minor
11/// releases without breaking downstream `match` arms. Construction of the
12/// existing variants is unaffected; only exhaustive matching outside this
13/// crate requires a wildcard arm.
14#[derive(Debug, Error, Diagnostic)]
15#[non_exhaustive]
16pub enum DcuError {
17    /// Failed to read a manifest file from disk.
18    #[error("failed to read manifest at {path}")]
19    #[diagnostic(
20        code(dependency_check_updates::io_error),
21        help("make sure the file exists and is readable")
22    )]
23    Io {
24        /// Path of the file that could not be read.
25        path: PathBuf,
26        /// Underlying I/O error.
27        #[source]
28        source: std::io::Error,
29    },
30
31    /// Failed to parse a manifest's contents.
32    #[error("failed to parse manifest at {path}")]
33    #[diagnostic(code(dependency_check_updates::parse_error))]
34    ManifestParse {
35        /// Path of the manifest that failed to parse.
36        path: PathBuf,
37        /// Human-readable parser error detail.
38        detail: String,
39    },
40
41    /// A package-registry lookup failed.
42    #[error("registry lookup failed for package `{package}`: {detail}")]
43    #[diagnostic(
44        code(dependency_check_updates::registry_error),
45        help("check your internet connection, or set GITHUB_TOKEN if scanning workflows")
46    )]
47    RegistryLookup {
48        /// Name of the package being resolved.
49        package: String,
50        /// Human-readable failure detail.
51        detail: String,
52    },
53
54    /// Failed to apply a version patch to a manifest.
55    #[error("failed to apply patch to {path}")]
56    #[diagnostic(code(dependency_check_updates::patch_error))]
57    PatchFailed {
58        /// Path of the manifest being patched.
59        path: PathBuf,
60        /// Human-readable failure detail.
61        detail: String,
62    },
63
64    /// A version string could not be parsed as semver.
65    #[error("invalid semver: {input}")]
66    #[diagnostic(code(dependency_check_updates::semver_error))]
67    SemverParse {
68        /// The input that failed to parse.
69        input: String,
70        /// Human-readable failure detail.
71        detail: String,
72    },
73
74    /// No recognized manifest was found at the given location.
75    #[error("no manifest found in {path}")]
76    #[diagnostic(
77        code(dependency_check_updates::no_manifest),
78        help(
79            "run dependency-check-updates in a directory containing package.json, or use --manifest"
80        )
81    )]
82    NoManifest {
83        /// Directory or path that was searched.
84        path: PathBuf,
85    },
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use rstest::rstest;
92
93    /// Build an [`DcuError::Io`] case. Constructed via a helper because
94    /// `std::io::Error` is not `Clone`, so it cannot live directly in an
95    /// `#[rstest]` case literal that is materialised once per generated test.
96    fn io_err() -> DcuError {
97        DcuError::Io {
98            path: PathBuf::from("test.json"),
99            source: std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
100        }
101    }
102
103    fn manifest_parse_err() -> DcuError {
104        DcuError::ManifestParse {
105            path: PathBuf::from("package.json"),
106            detail: "unexpected token".to_owned(),
107        }
108    }
109
110    fn no_manifest_err() -> DcuError {
111        DcuError::NoManifest {
112            path: PathBuf::from("/some/dir"),
113        }
114    }
115
116    fn registry_lookup_err() -> DcuError {
117        DcuError::RegistryLookup {
118            package: "lodash".to_owned(),
119            detail: "connection timeout".to_owned(),
120        }
121    }
122
123    fn semver_parse_err() -> DcuError {
124        DcuError::SemverParse {
125            input: "not.a.version".to_owned(),
126            detail: "invalid semver format".to_owned(),
127        }
128    }
129
130    /// Verifies the [`std::fmt::Display`] output for every variant. Variants
131    /// whose message embeds a path use `Path::display()` so the expected
132    /// string is computed at case time to stay correct on every platform.
133    ///
134    /// The `RegistryLookup` case in particular asserts that `detail` is
135    /// surfaced — without it, users hit a dead-end when GitHub Tags API
136    /// rate-limits them (no hint to set `GITHUB_TOKEN`).
137    #[rstest]
138    #[case::io(
139        io_err(),
140        format!("failed to read manifest at {}", PathBuf::from("test.json").display())
141    )]
142    #[case::manifest_parse(
143        manifest_parse_err(),
144        format!("failed to parse manifest at {}", PathBuf::from("package.json").display())
145    )]
146    #[case::no_manifest(
147        no_manifest_err(),
148        format!("no manifest found in {}", PathBuf::from("/some/dir").display())
149    )]
150    #[case::registry_lookup(
151        registry_lookup_err(),
152        "registry lookup failed for package `lodash`: connection timeout".to_owned()
153    )]
154    #[case::semver_parse(
155        semver_parse_err(),
156        "invalid semver: not.a.version".to_owned()
157    )]
158    fn dcu_error_display(#[case] err: DcuError, #[case] expected: String) {
159        assert_eq!(err.to_string(), expected);
160    }
161}