Skip to main content

deps_cargo/
error.rs

1//! Errors specific to Cargo/Rust dependency handling.
2//!
3//! These errors cover parsing Cargo.toml files, validating semver specifications,
4//! and communicating with the crates.io registry.
5
6use thiserror::Error;
7
8/// Errors specific to Cargo/Rust dependency handling.
9///
10/// These errors cover parsing Cargo.toml files, validating semver specifications,
11/// and communicating with the crates.io registry.
12#[derive(Error, Debug)]
13pub enum CargoError {
14    /// Failed to parse Cargo.toml
15    #[error("Failed to parse Cargo.toml: {message}")]
16    TomlParseError { message: String },
17
18    /// Invalid semver version specifier
19    #[error("Invalid semver version specifier '{specifier}': {message}")]
20    InvalidVersionSpecifier { specifier: String, message: String },
21
22    /// Package not found on crates.io
23    #[error("Crate '{package}' not found on crates.io")]
24    PackageNotFound { package: String },
25
26    /// crates.io registry request failed
27    #[error("crates.io registry request failed for '{package}': {source}")]
28    RegistryError {
29        package: String,
30        #[source]
31        source: Box<dyn std::error::Error + Send + Sync>,
32    },
33
34    /// Failed to deserialize crates.io API response
35    #[error("Failed to parse crates.io API response for '{package}': {source}")]
36    ApiResponseError {
37        package: String,
38        #[source]
39        source: serde_json::Error,
40    },
41
42    /// Invalid Cargo.toml structure
43    #[error("Invalid Cargo.toml structure: {message}")]
44    InvalidStructure { message: String },
45
46    /// Missing required field in Cargo.toml
47    #[error("Missing required field '{field}' in {section}")]
48    MissingField { section: String, field: String },
49
50    /// Workspace configuration error
51    #[error("Workspace error: {message}")]
52    WorkspaceError { message: String },
53
54    /// Invalid file URI
55    #[error("Invalid file URI: {uri}")]
56    InvalidUri { uri: String },
57
58    /// Cache error
59    #[error("Cache error: {0}")]
60    CacheError(String),
61
62    /// I/O error
63    #[error("I/O error: {0}")]
64    Io(#[from] std::io::Error),
65
66    /// Generic error wrapper
67    #[error(transparent)]
68    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
69}
70
71/// Result type alias for Cargo operations.
72pub type Result<T> = std::result::Result<T, CargoError>;
73
74impl CargoError {
75    /// Create a registry error from any error type.
76    pub fn registry_error(
77        package: impl Into<String>,
78        error: impl std::error::Error + Send + Sync + 'static,
79    ) -> Self {
80        Self::RegistryError {
81            package: package.into(),
82            source: Box::new(error),
83        }
84    }
85
86    /// Create an API response error.
87    pub fn api_response_error(package: impl Into<String>, error: serde_json::Error) -> Self {
88        Self::ApiResponseError {
89            package: package.into(),
90            source: error,
91        }
92    }
93
94    /// Create an invalid structure error.
95    pub fn invalid_structure(message: impl Into<String>) -> Self {
96        Self::InvalidStructure {
97            message: message.into(),
98        }
99    }
100
101    /// Create a missing field error.
102    pub fn missing_field(section: impl Into<String>, field: impl Into<String>) -> Self {
103        Self::MissingField {
104            section: section.into(),
105            field: field.into(),
106        }
107    }
108
109    /// Create an invalid version specifier error.
110    pub fn invalid_version_specifier(
111        specifier: impl Into<String>,
112        message: impl Into<String>,
113    ) -> Self {
114        Self::InvalidVersionSpecifier {
115            specifier: specifier.into(),
116            message: message.into(),
117        }
118    }
119
120    /// Create a workspace error.
121    pub fn workspace_error(message: impl Into<String>) -> Self {
122        Self::WorkspaceError {
123            message: message.into(),
124        }
125    }
126
127    /// Create an invalid URI error.
128    pub fn invalid_uri(uri: impl Into<String>) -> Self {
129        Self::InvalidUri { uri: uri.into() }
130    }
131}
132
133/// Convert from deps_core::DepsError for compatibility
134impl From<deps_core::DepsError> for CargoError {
135    fn from(err: deps_core::DepsError) -> Self {
136        match err {
137            deps_core::DepsError::ParseError { source, .. } => Self::CacheError(source.to_string()),
138            deps_core::DepsError::CacheError(msg) => Self::CacheError(msg),
139            deps_core::DepsError::InvalidVersionReq(msg) => Self::InvalidVersionSpecifier {
140                specifier: String::new(),
141                message: msg,
142            },
143            deps_core::DepsError::Io(e) => Self::Io(e),
144            deps_core::DepsError::Json(e) => Self::ApiResponseError {
145                package: String::new(),
146                source: e,
147            },
148            other => Self::CacheError(other.to_string()),
149        }
150    }
151}
152
153/// Convert to deps_core::DepsError for interoperability
154impl From<CargoError> for deps_core::DepsError {
155    fn from(err: CargoError) -> Self {
156        match err {
157            CargoError::TomlParseError { message } => Self::ParseError {
158                file_type: "Cargo.toml".into(),
159                source: Box::new(std::io::Error::other(message)),
160            },
161            CargoError::InvalidVersionSpecifier { message, .. } => Self::InvalidVersionReq(message),
162            CargoError::PackageNotFound { package } => {
163                Self::CacheError(format!("Crate '{package}' not found"))
164            }
165            CargoError::RegistryError { package, source } => Self::ParseError {
166                file_type: format!("crates.io registry for {package}"),
167                source,
168            },
169            CargoError::ApiResponseError { source, .. } => Self::Json(source),
170            CargoError::InvalidStructure { message } => Self::CacheError(message),
171            CargoError::MissingField { section, field } => {
172                Self::CacheError(format!("Missing '{field}' in {section}"))
173            }
174            CargoError::WorkspaceError { message } => Self::CacheError(message),
175            CargoError::InvalidUri { uri } => Self::CacheError(format!("Invalid URI: {uri}")),
176            CargoError::CacheError(msg) => Self::CacheError(msg),
177            CargoError::Io(e) => Self::Io(e),
178            CargoError::Other(e) => Self::CacheError(e.to_string()),
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_error_display() {
189        let err = CargoError::PackageNotFound {
190            package: "nonexistent".into(),
191        };
192        assert_eq!(
193            err.to_string(),
194            "Crate 'nonexistent' not found on crates.io"
195        );
196
197        let err = CargoError::missing_field("dependencies", "serde");
198        assert_eq!(
199            err.to_string(),
200            "Missing required field 'serde' in dependencies"
201        );
202
203        let err = CargoError::invalid_structure("missing [package] section");
204        assert_eq!(
205            err.to_string(),
206            "Invalid Cargo.toml structure: missing [package] section"
207        );
208    }
209
210    #[test]
211    fn test_error_construction() {
212        let err =
213            CargoError::registry_error("serde", std::io::Error::from(std::io::ErrorKind::NotFound));
214        assert!(matches!(err, CargoError::RegistryError { .. }));
215
216        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
217        let err = CargoError::api_response_error("tokio", json_err);
218        assert!(matches!(err, CargoError::ApiResponseError { .. }));
219    }
220
221    #[test]
222    fn test_invalid_version_specifier() {
223        let err = CargoError::invalid_version_specifier("invalid", "not a valid semver");
224        assert!(err.to_string().contains("invalid"));
225        assert!(err.to_string().contains("not a valid semver"));
226    }
227
228    #[test]
229    fn test_workspace_error() {
230        let err = CargoError::workspace_error("workspace root not found");
231        assert!(err.to_string().contains("workspace root not found"));
232    }
233
234    #[test]
235    fn test_invalid_uri() {
236        let err = CargoError::invalid_uri("not-a-valid-uri");
237        assert!(err.to_string().contains("not-a-valid-uri"));
238    }
239
240    #[test]
241    fn test_conversion_to_deps_error() {
242        let cargo_err = CargoError::PackageNotFound {
243            package: "test".into(),
244        };
245        let deps_err: deps_core::DepsError = cargo_err.into();
246        assert!(deps_err.to_string().contains("not found"));
247    }
248}