1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum BundlerError {
8 #[error("Failed to parse Gemfile: {message}")]
10 ParseError { message: String },
11
12 #[error("Invalid version specifier '{specifier}': {message}")]
14 InvalidVersionSpecifier { specifier: String, message: String },
15
16 #[error("Gem '{package}' not found on rubygems.org")]
18 PackageNotFound { package: String },
19
20 #[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 #[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 #[error("Invalid Gemfile structure: {message}")]
38 InvalidStructure { message: String },
39
40 #[error("Invalid file URI: {uri}")]
42 InvalidUri { uri: String },
43
44 #[error("Cache error: {0}")]
46 CacheError(String),
47
48 #[error("I/O error: {0}")]
50 Io(#[from] std::io::Error),
51
52 #[error(transparent)]
54 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
55}
56
57pub type Result<T> = std::result::Result<T, BundlerError>;
59
60impl BundlerError {
61 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 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 pub fn invalid_structure(message: impl Into<String>) -> Self {
82 Self::InvalidStructure {
83 message: message.into(),
84 }
85 }
86
87 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 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}