1use miette::Diagnostic;
4use std::path::PathBuf;
5use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Error, Debug, Diagnostic)]
12pub enum Error {
13 #[error("Workspace not found at path: {path}")]
15 #[diagnostic(
16 code(cuenv::workspaces::workspace_not_found),
17 help(
18 "Ensure the path points to a valid workspace root directory with a workspace configuration file"
19 )
20 )]
21 WorkspaceNotFound {
22 path: PathBuf,
24 },
25
26 #[error("Invalid workspace configuration at {path}: {message}")]
28 #[diagnostic(
29 code(cuenv::workspaces::invalid_config),
30 help(
31 "Check the workspace configuration file for syntax errors or missing required fields"
32 )
33 )]
34 InvalidWorkspaceConfig {
35 path: PathBuf,
37 message: String,
39 },
40
41 #[error("Lockfile not found at path: {path}")]
43 #[diagnostic(
44 code(cuenv::workspaces::lockfile_not_found),
45 help(
46 "Run your package manager's install command to generate a lockfile (e.g., 'npm install', 'cargo build')"
47 )
48 )]
49 LockfileNotFound {
50 path: PathBuf,
52 },
53
54 #[error("Manifest file not found at path: {path}")]
56 #[diagnostic(
57 code(cuenv::workspaces::manifest_not_found),
58 help(
59 "Ensure the manifest file exists at the expected location (e.g., 'package.json', 'Cargo.toml')"
60 )
61 )]
62 ManifestNotFound {
63 path: PathBuf,
65 },
66
67 #[error("Failed to parse lockfile at {path}: {message}")]
69 #[diagnostic(
70 code(cuenv::workspaces::lockfile_parse_failed),
71 help("The lockfile may be corrupted. Try regenerating it with your package manager")
72 )]
73 LockfileParseFailed {
74 path: PathBuf,
76 message: String,
78 },
79
80 #[error("Workspace member '{name}' not found in workspace at {workspace_root}")]
82 #[diagnostic(
83 code(cuenv::workspaces::member_not_found),
84 help(
85 "Check that the member name is correct and the member is listed in the workspace configuration"
86 )
87 )]
88 MemberNotFound {
89 name: String,
91 workspace_root: PathBuf,
93 },
94
95 #[error("Failed to resolve dependencies: {message}")]
97 #[diagnostic(
98 code(cuenv::workspaces::dependency_resolution_failed),
99 help("Check for circular dependencies or missing dependencies in the lockfile")
100 )]
101 DependencyResolutionFailed {
102 message: String,
104 },
105
106 #[error("Unsupported package manager: {manager}")]
108 #[diagnostic(
109 code(cuenv::workspaces::unsupported_manager),
110 help("Supported package managers: npm, bun, pnpm, yarn, cargo")
111 )]
112 UnsupportedPackageManager {
113 manager: String,
115 },
116
117 #[error("I/O error during {operation}{}: {source}", path.as_ref().map(|p| format!(" at {}", p.display())).unwrap_or_default())]
119 #[diagnostic(
120 code(cuenv::workspaces::io_error),
121 help(
122 "Check that the referenced paths exist and that you have permission to read or write them"
123 )
124 )]
125 Io {
126 #[source]
128 source: std::io::Error,
129 path: Option<PathBuf>,
131 operation: String,
133 },
134
135 #[error("JSON parsing error{}: {source}", path.as_ref().map(|p| format!(" in {}", p.display())).unwrap_or_default())]
137 #[diagnostic(
138 code(cuenv::workspaces::json_error),
139 help(
140 "Ensure the JSON has valid syntax and matches the expected schema for workspace metadata"
141 )
142 )]
143 Json {
144 #[source]
146 source: serde_json::Error,
147 path: Option<PathBuf>,
149 },
150
151 #[cfg(feature = "serde_yaml")]
153 #[error("YAML parsing error{}: {source}", path.as_ref().map(|p| format!(" in {}", p.display())).unwrap_or_default())]
154 #[diagnostic(
155 code(cuenv::workspaces::yaml_error),
156 help(
157 "Ensure the YAML has valid syntax and matches the expected schema for workspace metadata"
158 )
159 )]
160 Yaml {
161 #[source]
163 source: serde_yaml::Error,
164 path: Option<PathBuf>,
166 },
167
168 #[cfg(feature = "toml")]
170 #[error("TOML parsing error{}: {source}", path.as_ref().map(|p| format!(" in {}", p.display())).unwrap_or_default())]
171 #[diagnostic(
172 code(cuenv::workspaces::toml_error),
173 help(
174 "Ensure the TOML has valid syntax and matches the expected schema for Cargo manifests"
175 )
176 )]
177 Toml {
178 #[source]
180 source: toml::de::Error,
181 path: Option<PathBuf>,
183 },
184}
185
186impl From<std::io::Error> for Error {
187 fn from(source: std::io::Error) -> Self {
188 Self::Io {
189 source,
190 path: None,
191 operation: "file operation".to_string(),
192 }
193 }
194}
195
196impl From<serde_json::Error> for Error {
197 fn from(source: serde_json::Error) -> Self {
198 Self::Json { source, path: None }
199 }
200}
201
202#[cfg(feature = "serde_yaml")]
203impl From<serde_yaml::Error> for Error {
204 fn from(source: serde_yaml::Error) -> Self {
205 Self::Yaml { source, path: None }
206 }
207}
208
209#[cfg(feature = "toml")]
210impl From<toml::de::Error> for Error {
211 fn from(source: toml::de::Error) -> Self {
212 Self::Toml { source, path: None }
213 }
214}
215
216#[cfg(test)]
217#[allow(clippy::unnecessary_wraps)]
218mod tests {
219 use super::*;
220 use std::path::PathBuf;
221
222 #[test]
223 fn test_workspace_not_found_error() {
224 let error = Error::WorkspaceNotFound {
225 path: PathBuf::from("/nonexistent"),
226 };
227
228 let message = error.to_string();
229 assert!(message.contains("Workspace not found"));
230 assert!(message.contains("/nonexistent"));
231 }
232
233 #[test]
234 fn test_invalid_workspace_config_error() {
235 let error = Error::InvalidWorkspaceConfig {
236 path: PathBuf::from("/workspace/package.json"),
237 message: "Missing 'workspaces' field".to_string(),
238 };
239
240 let message = error.to_string();
241 assert!(message.contains("Invalid workspace configuration"));
242 assert!(message.contains("package.json"));
243 assert!(message.contains("Missing 'workspaces' field"));
244 }
245
246 #[test]
247 fn test_lockfile_not_found_error() {
248 let error = Error::LockfileNotFound {
249 path: PathBuf::from("/workspace/package-lock.json"),
250 };
251
252 let message = error.to_string();
253 assert!(message.contains("Lockfile not found"));
254 assert!(message.contains("package-lock.json"));
255 }
256
257 #[test]
258 fn test_lockfile_parse_failed_error() {
259 let error = Error::LockfileParseFailed {
260 path: PathBuf::from("/workspace/Cargo.lock"),
261 message: "Invalid TOML syntax".to_string(),
262 };
263
264 let message = error.to_string();
265 assert!(message.contains("Failed to parse lockfile"));
266 assert!(message.contains("Cargo.lock"));
267 assert!(message.contains("Invalid TOML syntax"));
268 }
269
270 #[test]
271 fn test_member_not_found_error() {
272 let error = Error::MemberNotFound {
273 name: "my-package".to_string(),
274 workspace_root: PathBuf::from("/workspace"),
275 };
276
277 let message = error.to_string();
278 assert!(message.contains("Workspace member"));
279 assert!(message.contains("my-package"));
280 assert!(message.contains("not found"));
281 }
282
283 #[test]
284 fn test_dependency_resolution_failed_error() {
285 let error = Error::DependencyResolutionFailed {
286 message: "Circular dependency detected".to_string(),
287 };
288
289 let message = error.to_string();
290 assert!(message.contains("Failed to resolve dependencies"));
291 assert!(message.contains("Circular dependency"));
292 }
293
294 #[test]
295 fn test_unsupported_package_manager_error() {
296 let error = Error::UnsupportedPackageManager {
297 manager: "poetry".to_string(),
298 };
299
300 let message = error.to_string();
301 assert!(message.contains("Unsupported package manager"));
302 assert!(message.contains("poetry"));
303 }
304
305 #[test]
306 fn test_io_error_display() {
307 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
308 let error = Error::Io {
309 source: io_error,
310 path: Some(PathBuf::from("/test/file.txt")),
311 operation: "reading file".to_string(),
312 };
313
314 let message = error.to_string();
315 assert!(message.contains("I/O error during reading file"));
316 assert!(message.contains("/test/file.txt"));
317 }
318
319 #[test]
320 fn test_io_error_no_path() {
321 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
322 let error = Error::Io {
323 source: io_error,
324 path: None,
325 operation: "opening directory".to_string(),
326 };
327
328 let message = error.to_string();
329 assert!(message.contains("I/O error during opening directory"));
330 assert!(!message.contains(" at "));
331 }
332
333 #[test]
334 fn test_json_error_display() {
335 let json_str = "{ invalid json }";
336 let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
337 let error = Error::Json {
338 source: json_error,
339 path: Some(PathBuf::from("/workspace/package.json")),
340 };
341
342 let message = error.to_string();
343 assert!(message.contains("JSON parsing error"));
344 assert!(message.contains("package.json"));
345 }
346
347 #[test]
348 fn test_json_error_no_path() {
349 let json_str = "{ invalid json }";
350 let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
351 let error = Error::Json {
352 source: json_error,
353 path: None,
354 };
355
356 let message = error.to_string();
357 assert!(message.contains("JSON parsing error"));
358 assert!(!message.contains(" in "));
359 }
360
361 #[test]
362 fn test_io_error_conversion() {
363 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
364 let error: Error = io_error.into();
365
366 match error {
367 Error::Io {
368 source: _,
369 path,
370 operation,
371 } => {
372 assert_eq!(path, None);
373 assert_eq!(operation, "file operation");
374 }
375 _ => panic!("Expected Io error variant"),
376 }
377 }
378
379 #[test]
380 fn test_json_error_conversion() {
381 let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
382 let error: Error = json_error.into();
383
384 match error {
385 Error::Json { source: _, path } => {
386 assert_eq!(path, None);
387 }
388 _ => panic!("Expected Json error variant"),
389 }
390 }
391
392 #[cfg(feature = "serde_yaml")]
393 #[test]
394 fn test_yaml_error_display() {
395 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: : yaml").unwrap_err();
396 let error = Error::Yaml {
397 source: yaml_error,
398 path: Some(PathBuf::from("/workspace/pnpm-lock.yaml")),
399 };
400
401 let message = error.to_string();
402 assert!(message.contains("YAML parsing error"));
403 assert!(message.contains("pnpm-lock.yaml"));
404 }
405
406 #[cfg(feature = "serde_yaml")]
407 #[test]
408 fn test_yaml_error_no_path() {
409 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: : yaml").unwrap_err();
410 let error = Error::Yaml {
411 source: yaml_error,
412 path: None,
413 };
414
415 let message = error.to_string();
416 assert!(message.contains("YAML parsing error"));
417 assert!(message.starts_with("YAML parsing error: "));
419 }
420
421 #[cfg(feature = "serde_yaml")]
422 #[test]
423 fn test_yaml_error_conversion() {
424 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: : yaml").unwrap_err();
425 let error: Error = yaml_error.into();
426
427 match error {
428 Error::Yaml { source: _, path } => {
429 assert_eq!(path, None);
430 }
431 _ => panic!("Expected Yaml error variant"),
432 }
433 }
434
435 #[cfg(feature = "serde_yaml")]
436 #[test]
437 fn test_yaml_error_diagnostics() {
438 use miette::Diagnostic;
439
440 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: : yaml").unwrap_err();
441 let error = Error::Yaml {
442 source: yaml_error,
443 path: None,
444 };
445
446 assert_eq!(
447 error.code().map(|c| c.to_string()),
448 Some("cuenv::workspaces::yaml_error".to_string())
449 );
450 assert!(error.help().is_some());
451 }
452
453 #[cfg(feature = "toml")]
454 #[test]
455 fn test_toml_error_display() {
456 let toml_error = toml::from_str::<toml::Value>("not valid = [").unwrap_err();
457 let error = Error::Toml {
458 source: toml_error,
459 path: Some(PathBuf::from("/workspace/Cargo.toml")),
460 };
461
462 let message = error.to_string();
463 assert!(message.contains("TOML parsing error"));
464 assert!(message.contains("Cargo.toml"));
465 }
466
467 #[cfg(feature = "toml")]
468 #[test]
469 fn test_toml_error_no_path() {
470 let toml_error = toml::from_str::<toml::Value>("not valid = [").unwrap_err();
471 let error = Error::Toml {
472 source: toml_error,
473 path: None,
474 };
475
476 let message = error.to_string();
477 assert!(message.contains("TOML parsing error"));
478 assert!(!message.contains(" in "));
479 }
480
481 #[cfg(feature = "toml")]
482 #[test]
483 fn test_toml_error_conversion() {
484 let toml_error = toml::from_str::<toml::Value>("not valid = [").unwrap_err();
485 let error: Error = toml_error.into();
486
487 match error {
488 Error::Toml { source: _, path } => {
489 assert_eq!(path, None);
490 }
491 _ => panic!("Expected Toml error variant"),
492 }
493 }
494
495 #[cfg(feature = "toml")]
496 #[test]
497 fn test_toml_error_diagnostics() {
498 use miette::Diagnostic;
499
500 let toml_error = toml::from_str::<toml::Value>("not valid = [").unwrap_err();
501 let error = Error::Toml {
502 source: toml_error,
503 path: None,
504 };
505
506 assert_eq!(
507 error.code().map(|c| c.to_string()),
508 Some("cuenv::workspaces::toml_error".to_string())
509 );
510 assert!(error.help().is_some());
511 }
512
513 #[test]
514 fn test_result_type_with_question_mark() {
515 fn returns_result() -> Result<String> {
516 Ok("success".to_string())
517 }
518
519 fn uses_result() -> Result<String> {
520 let value = returns_result()?;
521 Ok(value)
522 }
523
524 assert!(uses_result().is_ok());
525 }
526
527 #[test]
528 fn test_diagnostic_codes() {
529 use miette::Diagnostic;
530
531 let error = Error::WorkspaceNotFound {
532 path: PathBuf::from("/test"),
533 };
534 assert!(error.code().is_some());
535
536 let error = Error::InvalidWorkspaceConfig {
537 path: PathBuf::from("/test"),
538 message: "test".to_string(),
539 };
540 assert!(error.code().is_some());
541
542 let error = Error::LockfileNotFound {
543 path: PathBuf::from("/test"),
544 };
545 assert!(error.code().is_some());
546
547 let error = Error::LockfileParseFailed {
548 path: PathBuf::from("/test"),
549 message: "test".to_string(),
550 };
551 assert!(error.code().is_some());
552
553 let error = Error::MemberNotFound {
554 name: "test".to_string(),
555 workspace_root: PathBuf::from("/test"),
556 };
557 assert!(error.code().is_some());
558
559 let error = Error::DependencyResolutionFailed {
560 message: "test".to_string(),
561 };
562 assert!(error.code().is_some());
563
564 let error = Error::UnsupportedPackageManager {
565 manager: "test".to_string(),
566 };
567 assert!(error.code().is_some());
568 }
569
570 #[test]
571 fn test_diagnostic_help_messages() {
572 use miette::Diagnostic;
573
574 let error = Error::WorkspaceNotFound {
575 path: PathBuf::from("/test"),
576 };
577 assert!(error.help().is_some());
578
579 let error = Error::InvalidWorkspaceConfig {
580 path: PathBuf::from("/test"),
581 message: "test".to_string(),
582 };
583 assert!(error.help().is_some());
584
585 let error = Error::LockfileNotFound {
586 path: PathBuf::from("/test"),
587 };
588 assert!(error.help().is_some());
589
590 let error = Error::LockfileParseFailed {
591 path: PathBuf::from("/test"),
592 message: "test".to_string(),
593 };
594 assert!(error.help().is_some());
595 }
596}