Skip to main content

deps_bundler/
error.rs

1//! Errors specific to Bundler/Ruby dependency handling.
2
3use thiserror::Error;
4
5/// Errors specific to Bundler/Ruby dependency handling.
6#[derive(Error, Debug)]
7pub enum BundlerError {
8    /// Failed to parse Gemfile
9    #[error("Failed to parse Gemfile: {message}")]
10    ParseError { message: String },
11
12    /// Invalid version specifier
13    #[error("Invalid version specifier '{specifier}': {message}")]
14    InvalidVersionSpecifier { specifier: String, message: String },
15
16    /// Package not found on rubygems.org
17    #[error("Gem '{package}' not found on rubygems.org")]
18    PackageNotFound { package: String },
19
20    /// rubygems.org registry request failed
21    #[error("rubygems.org request failed for '{package}': {source}")]
22    RegistryError {
23        package: String,
24        #[source]
25        source: Box<dyn std::error::Error + Send + Sync>,
26    },
27
28    /// Failed to deserialize rubygems API response
29    #[error("Failed to parse rubygems API response for '{package}': {source}")]
30    ApiResponseError {
31        package: String,
32        #[source]
33        source: serde_json::Error,
34    },
35
36    /// Invalid Gemfile structure
37    #[error("Invalid Gemfile structure: {message}")]
38    InvalidStructure { message: String },
39
40    /// Invalid file URI
41    #[error("Invalid file URI: {uri}")]
42    InvalidUri { uri: String },
43
44    /// Cache error
45    #[error("Cache error: {0}")]
46    CacheError(String),
47
48    /// I/O error
49    #[error("I/O error: {0}")]
50    Io(#[from] std::io::Error),
51
52    /// Generic error wrapper
53    #[error(transparent)]
54    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
55}
56
57/// Result type alias for Bundler operations.
58pub type Result<T> = std::result::Result<T, BundlerError>;
59
60impl BundlerError {
61    /// Create a registry error from any error type.
62    pub fn registry_error(
63        package: impl Into<String>,
64        error: impl std::error::Error + Send + Sync + 'static,
65    ) -> Self {
66        Self::RegistryError {
67            package: package.into(),
68            source: Box::new(error),
69        }
70    }
71
72    /// Create an API response error.
73    pub fn api_response_error(package: impl Into<String>, error: serde_json::Error) -> Self {
74        Self::ApiResponseError {
75            package: package.into(),
76            source: error,
77        }
78    }
79
80    /// Create an invalid structure error.
81    pub fn invalid_structure(message: impl Into<String>) -> Self {
82        Self::InvalidStructure {
83            message: message.into(),
84        }
85    }
86
87    /// Create an invalid version specifier error.
88    pub fn invalid_version_specifier(
89        specifier: impl Into<String>,
90        message: impl Into<String>,
91    ) -> Self {
92        Self::InvalidVersionSpecifier {
93            specifier: specifier.into(),
94            message: message.into(),
95        }
96    }
97
98    /// Create an invalid URI error.
99    pub fn invalid_uri(uri: impl Into<String>) -> Self {
100        Self::InvalidUri { uri: uri.into() }
101    }
102}
103
104impl From<deps_core::DepsError> for BundlerError {
105    fn from(err: deps_core::DepsError) -> Self {
106        match err {
107            deps_core::DepsError::ParseError { source, .. } => Self::CacheError(source.to_string()),
108            deps_core::DepsError::CacheError(msg) => Self::CacheError(msg),
109            deps_core::DepsError::InvalidVersionReq(msg) => Self::InvalidVersionSpecifier {
110                specifier: String::new(),
111                message: msg,
112            },
113            deps_core::DepsError::Io(e) => Self::Io(e),
114            deps_core::DepsError::Json(e) => Self::ApiResponseError {
115                package: String::new(),
116                source: e,
117            },
118            other => Self::CacheError(other.to_string()),
119        }
120    }
121}
122
123impl From<BundlerError> for deps_core::DepsError {
124    fn from(err: BundlerError) -> Self {
125        match err {
126            BundlerError::ParseError { message } => Self::ParseError {
127                file_type: "Gemfile".into(),
128                source: Box::new(std::io::Error::other(message)),
129            },
130            BundlerError::InvalidVersionSpecifier { message, .. } => {
131                Self::InvalidVersionReq(message)
132            }
133            BundlerError::PackageNotFound { package } => {
134                Self::CacheError(format!("Gem '{package}' not found"))
135            }
136            BundlerError::RegistryError { package, source } => Self::ParseError {
137                file_type: format!("rubygems.org for {package}"),
138                source,
139            },
140            BundlerError::ApiResponseError { source, .. } => Self::Json(source),
141            BundlerError::InvalidStructure { message } => Self::CacheError(message),
142            BundlerError::InvalidUri { uri } => Self::CacheError(format!("Invalid URI: {uri}")),
143            BundlerError::CacheError(msg) => Self::CacheError(msg),
144            BundlerError::Io(e) => Self::Io(e),
145            BundlerError::Other(e) => Self::CacheError(e.to_string()),
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_error_display() {
156        let err = BundlerError::PackageNotFound {
157            package: "nonexistent".into(),
158        };
159        assert_eq!(
160            err.to_string(),
161            "Gem 'nonexistent' not found on rubygems.org"
162        );
163
164        let err = BundlerError::invalid_structure("missing source");
165        assert_eq!(err.to_string(), "Invalid Gemfile structure: missing source");
166    }
167
168    #[test]
169    fn test_error_construction() {
170        let err = BundlerError::registry_error(
171            "rails",
172            std::io::Error::from(std::io::ErrorKind::NotFound),
173        );
174        assert!(matches!(err, BundlerError::RegistryError { .. }));
175
176        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
177        let err = BundlerError::api_response_error("rails", json_err);
178        assert!(matches!(err, BundlerError::ApiResponseError { .. }));
179    }
180
181    #[test]
182    fn test_invalid_version_specifier() {
183        let err = BundlerError::invalid_version_specifier("invalid", "not a valid version");
184        assert!(err.to_string().contains("invalid"));
185        assert!(err.to_string().contains("not a valid version"));
186    }
187
188    #[test]
189    fn test_invalid_uri() {
190        let err = BundlerError::invalid_uri("not-a-valid-uri");
191        assert!(err.to_string().contains("not-a-valid-uri"));
192    }
193
194    #[test]
195    fn test_conversion_to_deps_error() {
196        let bundler_err = BundlerError::PackageNotFound {
197            package: "test".into(),
198        };
199        let deps_err: deps_core::DepsError = bundler_err.into();
200        assert!(deps_err.to_string().contains("not found"));
201    }
202
203    #[test]
204    fn test_parse_error_display() {
205        let err = BundlerError::ParseError {
206            message: "unexpected token".into(),
207        };
208        assert_eq!(err.to_string(), "Failed to parse Gemfile: unexpected token");
209    }
210
211    #[test]
212    fn test_parse_error_to_deps_error() {
213        let err = BundlerError::ParseError {
214            message: "syntax error".into(),
215        };
216        let deps_err: deps_core::DepsError = err.into();
217        assert!(matches!(deps_err, deps_core::DepsError::ParseError { .. }));
218    }
219
220    #[test]
221    fn test_invalid_version_specifier_to_deps_error() {
222        let err = BundlerError::InvalidVersionSpecifier {
223            specifier: "~>".into(),
224            message: "incomplete specifier".into(),
225        };
226        let deps_err: deps_core::DepsError = err.into();
227        assert!(matches!(
228            deps_err,
229            deps_core::DepsError::InvalidVersionReq(_)
230        ));
231    }
232
233    #[test]
234    fn test_registry_error_to_deps_error() {
235        let io_err = std::io::Error::other("connection refused");
236        let err = BundlerError::RegistryError {
237            package: "nokogiri".into(),
238            source: Box::new(io_err),
239        };
240        let deps_err: deps_core::DepsError = err.into();
241        assert!(matches!(deps_err, deps_core::DepsError::ParseError { .. }));
242    }
243
244    #[test]
245    fn test_api_response_error_to_deps_error() {
246        let json_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
247        let err = BundlerError::ApiResponseError {
248            package: "test".into(),
249            source: json_err,
250        };
251        let deps_err: deps_core::DepsError = err.into();
252        assert!(matches!(deps_err, deps_core::DepsError::Json(_)));
253    }
254
255    #[test]
256    fn test_invalid_structure_to_deps_error() {
257        let err = BundlerError::InvalidStructure {
258            message: "missing gem declaration".into(),
259        };
260        let deps_err: deps_core::DepsError = err.into();
261        assert!(matches!(deps_err, deps_core::DepsError::CacheError(_)));
262    }
263
264    #[test]
265    fn test_invalid_uri_to_deps_error() {
266        let err = BundlerError::InvalidUri {
267            uri: "invalid://uri".into(),
268        };
269        let deps_err: deps_core::DepsError = err.into();
270        assert!(matches!(deps_err, deps_core::DepsError::CacheError(_)));
271        assert!(deps_err.to_string().contains("Invalid URI"));
272    }
273
274    #[test]
275    fn test_cache_error_to_deps_error() {
276        let err = BundlerError::CacheError("cache miss".into());
277        let deps_err: deps_core::DepsError = err.into();
278        assert!(matches!(deps_err, deps_core::DepsError::CacheError(_)));
279    }
280
281    #[test]
282    fn test_io_error_to_deps_error() {
283        let io_err = std::io::Error::from(std::io::ErrorKind::NotFound);
284        let err = BundlerError::Io(io_err);
285        let deps_err: deps_core::DepsError = err.into();
286        assert!(matches!(deps_err, deps_core::DepsError::Io(_)));
287    }
288
289    #[test]
290    fn test_other_error_to_deps_error() {
291        let other_err: Box<dyn std::error::Error + Send + Sync> =
292            Box::new(std::io::Error::other("unknown error"));
293        let err = BundlerError::Other(other_err);
294        let deps_err: deps_core::DepsError = err.into();
295        assert!(matches!(deps_err, deps_core::DepsError::CacheError(_)));
296    }
297
298    #[test]
299    fn test_deps_error_to_bundler_error_parse() {
300        let deps_err = deps_core::DepsError::ParseError {
301            file_type: "test".into(),
302            source: Box::new(std::io::Error::other("parse failed")),
303        };
304        let bundler_err: BundlerError = deps_err.into();
305        assert!(matches!(bundler_err, BundlerError::CacheError(_)));
306    }
307
308    #[test]
309    fn test_deps_error_to_bundler_error_cache() {
310        let deps_err = deps_core::DepsError::CacheError("cache failure".into());
311        let bundler_err: BundlerError = deps_err.into();
312        assert!(matches!(bundler_err, BundlerError::CacheError(_)));
313    }
314
315    #[test]
316    fn test_deps_error_to_bundler_error_invalid_version() {
317        let deps_err = deps_core::DepsError::InvalidVersionReq("bad version".into());
318        let bundler_err: BundlerError = deps_err.into();
319        assert!(matches!(
320            bundler_err,
321            BundlerError::InvalidVersionSpecifier { .. }
322        ));
323    }
324
325    #[test]
326    fn test_deps_error_to_bundler_error_io() {
327        let io_err = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
328        let deps_err = deps_core::DepsError::Io(io_err);
329        let bundler_err: BundlerError = deps_err.into();
330        assert!(matches!(bundler_err, BundlerError::Io(_)));
331    }
332
333    #[test]
334    fn test_deps_error_to_bundler_error_json() {
335        let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
336        let deps_err = deps_core::DepsError::Json(json_err);
337        let bundler_err: BundlerError = deps_err.into();
338        assert!(matches!(bundler_err, BundlerError::ApiResponseError { .. }));
339    }
340
341    #[test]
342    fn test_io_error_from_impl() {
343        let io_err = std::io::Error::from(std::io::ErrorKind::NotFound);
344        let bundler_err: BundlerError = io_err.into();
345        assert!(matches!(bundler_err, BundlerError::Io(_)));
346    }
347
348    #[test]
349    fn test_error_display_api_response_error() {
350        let json_err = serde_json::from_str::<serde_json::Value>("[invalid").unwrap_err();
351        let err = BundlerError::ApiResponseError {
352            package: "rails".into(),
353            source: json_err,
354        };
355        let display = err.to_string();
356        assert!(display.contains("rails"));
357        assert!(display.contains("rubygems API response"));
358    }
359
360    #[test]
361    fn test_error_display_registry_error() {
362        let io_err = std::io::Error::other("timeout");
363        let err = BundlerError::RegistryError {
364            package: "nokogiri".into(),
365            source: Box::new(io_err),
366        };
367        let display = err.to_string();
368        assert!(display.contains("nokogiri"));
369        assert!(display.contains("rubygems.org"));
370    }
371
372    #[test]
373    fn test_error_debug() {
374        let err = BundlerError::PackageNotFound {
375            package: "test".into(),
376        };
377        let debug = format!("{:?}", err);
378        assert!(debug.contains("PackageNotFound"));
379        assert!(debug.contains("test"));
380    }
381}