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