cuenv_core/
lib.rs

1//! Core types and utilities for cuenv
2//!
3//! This crate provides enhanced error handling with miette diagnostics,
4//! structured error reporting, and contextual information.
5//!
6//! ## Type-safe wrappers
7//!
8//! This crate provides validated newtype wrappers for common domain types:
9//!
10//! - [`PackageDir`] - A validated directory path that must exist and be a directory
11//! - [`PackageName`] - A validated package name following CUE package naming rules
12//!
13//! ## Examples
14//!
15//! ```rust
16//! use cuenv_core::{PackageDir, PackageName};
17//! use std::path::Path;
18//!
19//! // Validate a directory exists and is actually a directory
20//! let pkg_dir = match PackageDir::try_from(Path::new(".")) {
21//!     Ok(dir) => dir,
22//!     Err(e) => {
23//!         eprintln!("Invalid directory: {}", e);
24//!         return;
25//!     }
26//! };
27//!
28//! // Validate a package name follows naming rules
29//! let pkg_name = match PackageName::try_from("my-package") {
30//!     Ok(name) => name,
31//!     Err(e) => {
32//!         eprintln!("Invalid package name: {}", e);
33//!         return;
34//!     }
35//! };
36//! ```
37
38// Rust 1.92 compiler bug: false positives for thiserror/miette derive macro fields
39// https://github.com/rust-lang/rust/issues/147648
40#![allow(unused_assignments)]
41
42pub mod affected;
43pub mod base;
44pub mod ci;
45pub mod config;
46pub mod contributors;
47pub mod environment;
48pub mod lockfile;
49pub mod manifest;
50pub mod module;
51pub mod owners;
52pub mod paths;
53pub mod rules;
54pub mod secrets;
55pub mod shell;
56pub mod sync;
57pub mod tasks;
58pub mod tools;
59
60// Re-export affected detection types
61pub use affected::{AffectedBy, matches_pattern};
62
63// Re-export module types for convenience
64pub use module::{Instance, InstanceKind, ModuleEvaluation};
65
66/// Version of the `cuenv-core` crate (used by task cache metadata)
67pub const VERSION: &str = env!("CARGO_PKG_VERSION");
68
69#[cfg(test)]
70pub mod test_utils;
71
72use miette::{Diagnostic, SourceSpan};
73use serde::{Deserialize, Serialize};
74use std::path::{Path, PathBuf};
75use thiserror::Error;
76
77/// Main error type for cuenv operations with enhanced diagnostics
78#[derive(Error, Debug, Diagnostic)]
79pub enum Error {
80    #[error("Configuration error: {message}")]
81    #[diagnostic(
82        code(cuenv::config::invalid),
83        help("Check your cuenv.cue configuration file for syntax errors or invalid values")
84    )]
85    Configuration {
86        #[source_code]
87        src: String,
88        #[label("invalid configuration")]
89        span: Option<SourceSpan>,
90        message: String,
91    },
92
93    #[error("FFI operation failed in {function}: {message}")]
94    #[diagnostic(code(cuenv::ffi::error))]
95    Ffi {
96        function: &'static str,
97        message: String,
98        #[help]
99        help: Option<String>,
100    },
101
102    #[error("CUE parsing failed: {message}")]
103    #[diagnostic(code(cuenv::cue::parse_error))]
104    CueParse {
105        path: Box<Path>,
106        #[source_code]
107        src: Option<String>,
108        #[label("parsing failed here")]
109        span: Option<SourceSpan>,
110        message: String,
111        suggestions: Option<Vec<String>>,
112    },
113
114    #[error("I/O {operation} failed{}", path.as_ref().map_or(String::new(), |p| format!(": {}", p.display())))]
115    #[diagnostic(
116        code(cuenv::io::error),
117        help("Check file permissions and ensure the path exists")
118    )]
119    Io {
120        #[source]
121        source: std::io::Error,
122        path: Option<Box<Path>>,
123        operation: String,
124    },
125
126    #[error("Text encoding error")]
127    #[diagnostic(
128        code(cuenv::encoding::utf8),
129        help("The file contains invalid UTF-8. Ensure your files use UTF-8 encoding.")
130    )]
131    Utf8 {
132        #[source]
133        source: std::str::Utf8Error,
134        file: Option<Box<Path>>,
135    },
136
137    #[error("Operation timed out after {seconds} seconds")]
138    #[diagnostic(
139        code(cuenv::timeout),
140        help("Try increasing the timeout or check if the operation is stuck")
141    )]
142    Timeout { seconds: u64 },
143
144    #[error("Validation failed: {message}")]
145    #[diagnostic(code(cuenv::validation::failed))]
146    Validation {
147        #[source_code]
148        src: Option<String>,
149        #[label("validation failed")]
150        span: Option<SourceSpan>,
151        message: String,
152        #[related]
153        related: Vec<Error>,
154    },
155
156    #[error("Task execution failed: {message}")]
157    #[diagnostic(code(cuenv::task::execution))]
158    Execution {
159        message: String,
160        #[help]
161        help: Option<String>,
162    },
163
164    #[error("Tool resolution failed: {message}")]
165    #[diagnostic(code(cuenv::tool::resolution))]
166    ToolResolution {
167        message: String,
168        #[help]
169        help: Option<String>,
170    },
171
172    #[error("Platform error: {message}")]
173    #[diagnostic(
174        code(cuenv::platform::error),
175        help("This platform may not be supported by the tool provider")
176    )]
177    Platform { message: String },
178
179    #[error("Task '{task_name}' failed with exit code {exit_code}")]
180    #[diagnostic(code(cuenv::task::failed))]
181    TaskFailed {
182        task_name: String,
183        exit_code: i32,
184        stdout: String,
185        stderr: String,
186        #[help]
187        help: Option<String>,
188    },
189
190    #[error("Task graph error: {message}")]
191    #[diagnostic(code(cuenv::task::graph))]
192    TaskGraph {
193        message: String,
194        #[help]
195        help: Option<String>,
196    },
197
198    #[error("Secret resolution failed: {message}")]
199    #[diagnostic(code(cuenv::secret::resolution))]
200    SecretResolution {
201        message: String,
202        #[help]
203        help: Option<String>,
204    },
205}
206
207impl Error {
208    #[must_use]
209    pub fn configuration(msg: impl Into<String>) -> Self {
210        Error::Configuration {
211            src: String::new(),
212            span: None,
213            message: msg.into(),
214        }
215    }
216
217    #[must_use]
218    pub fn configuration_with_source(
219        msg: impl Into<String>,
220        src: impl Into<String>,
221        span: Option<SourceSpan>,
222    ) -> Self {
223        Error::Configuration {
224            src: src.into(),
225            span,
226            message: msg.into(),
227        }
228    }
229
230    #[must_use]
231    pub fn ffi(function: &'static str, message: impl Into<String>) -> Self {
232        Error::Ffi {
233            function,
234            message: message.into(),
235            help: None,
236        }
237    }
238
239    #[must_use]
240    pub fn ffi_with_help(
241        function: &'static str,
242        message: impl Into<String>,
243        help: impl Into<String>,
244    ) -> Self {
245        Error::Ffi {
246            function,
247            message: message.into(),
248            help: Some(help.into()),
249        }
250    }
251
252    #[must_use]
253    pub fn cue_parse(path: &Path, message: impl Into<String>) -> Self {
254        Error::CueParse {
255            path: path.into(),
256            src: None,
257            span: None,
258            message: message.into(),
259            suggestions: None,
260        }
261    }
262
263    #[must_use]
264    pub fn cue_parse_with_source(
265        path: &Path,
266        message: impl Into<String>,
267        src: impl Into<String>,
268        span: Option<SourceSpan>,
269        suggestions: Option<Vec<String>>,
270    ) -> Self {
271        Error::CueParse {
272            path: path.into(),
273            src: Some(src.into()),
274            span,
275            message: message.into(),
276            suggestions,
277        }
278    }
279
280    #[must_use]
281    pub fn validation(msg: impl Into<String>) -> Self {
282        Error::Validation {
283            src: None,
284            span: None,
285            message: msg.into(),
286            related: Vec::new(),
287        }
288    }
289
290    #[must_use]
291    pub fn validation_with_source(
292        msg: impl Into<String>,
293        src: impl Into<String>,
294        span: Option<SourceSpan>,
295    ) -> Self {
296        Error::Validation {
297            src: Some(src.into()),
298            span,
299            message: msg.into(),
300            related: Vec::new(),
301        }
302    }
303
304    #[must_use]
305    pub fn execution(msg: impl Into<String>) -> Self {
306        Error::Execution {
307            message: msg.into(),
308            help: None,
309        }
310    }
311
312    #[must_use]
313    pub fn execution_with_help(msg: impl Into<String>, help: impl Into<String>) -> Self {
314        Error::Execution {
315            message: msg.into(),
316            help: Some(help.into()),
317        }
318    }
319
320    #[must_use]
321    pub fn tool_resolution(msg: impl Into<String>) -> Self {
322        Error::ToolResolution {
323            message: msg.into(),
324            help: None,
325        }
326    }
327
328    #[must_use]
329    pub fn tool_resolution_with_help(msg: impl Into<String>, help: impl Into<String>) -> Self {
330        Error::ToolResolution {
331            message: msg.into(),
332            help: Some(help.into()),
333        }
334    }
335
336    #[must_use]
337    pub fn platform(msg: impl Into<String>) -> Self {
338        Error::Platform {
339            message: msg.into(),
340        }
341    }
342
343    #[must_use]
344    pub fn task_failed(
345        task_name: impl Into<String>,
346        exit_code: i32,
347        stdout: impl Into<String>,
348        stderr: impl Into<String>,
349    ) -> Self {
350        Error::TaskFailed {
351            task_name: task_name.into(),
352            exit_code,
353            stdout: stdout.into(),
354            stderr: stderr.into(),
355            help: None,
356        }
357    }
358
359    #[must_use]
360    pub fn task_failed_with_help(
361        task_name: impl Into<String>,
362        exit_code: i32,
363        stdout: impl Into<String>,
364        stderr: impl Into<String>,
365        help: impl Into<String>,
366    ) -> Self {
367        Error::TaskFailed {
368            task_name: task_name.into(),
369            exit_code,
370            stdout: stdout.into(),
371            stderr: stderr.into(),
372            help: Some(help.into()),
373        }
374    }
375
376    #[must_use]
377    pub fn task_graph(message: impl Into<String>) -> Self {
378        Error::TaskGraph {
379            message: message.into(),
380            help: None,
381        }
382    }
383
384    #[must_use]
385    pub fn task_graph_with_help(message: impl Into<String>, help: impl Into<String>) -> Self {
386        Error::TaskGraph {
387            message: message.into(),
388            help: Some(help.into()),
389        }
390    }
391
392    #[must_use]
393    pub fn secret_resolution(message: impl Into<String>) -> Self {
394        Error::SecretResolution {
395            message: message.into(),
396            help: None,
397        }
398    }
399
400    #[must_use]
401    pub fn secret_resolution_with_help(
402        message: impl Into<String>,
403        help: impl Into<String>,
404    ) -> Self {
405        Error::SecretResolution {
406            message: message.into(),
407            help: Some(help.into()),
408        }
409    }
410}
411
412// Implement conversions for common error types
413impl From<std::io::Error> for Error {
414    fn from(source: std::io::Error) -> Self {
415        Error::Io {
416            source,
417            path: None,
418            operation: "unknown (unmapped error conversion)".to_string(),
419        }
420    }
421}
422
423impl From<std::str::Utf8Error> for Error {
424    fn from(source: std::str::Utf8Error) -> Self {
425        Error::Utf8 { source, file: None }
426    }
427}
428
429impl From<cuenv_hooks::Error> for Error {
430    fn from(source: cuenv_hooks::Error) -> Self {
431        Error::Execution {
432            message: source.to_string(),
433            help: None,
434        }
435    }
436}
437
438impl From<cuenv_task_graph::Error> for Error {
439    fn from(err: cuenv_task_graph::Error) -> Self {
440        let help = match &err {
441            cuenv_task_graph::Error::CycleDetected { .. } => {
442                Some("Check for circular dependencies between tasks".into())
443            }
444            cuenv_task_graph::Error::MissingDependency { task, dependency } => Some(format!(
445                "Add task '{}' or remove it from {}'s dependsOn",
446                dependency, task
447            )),
448            cuenv_task_graph::Error::MissingDependencies { missing } => {
449                let suggestions: Vec<String> = missing
450                    .iter()
451                    .map(|(task, dep)| {
452                        format!("  - Add '{}' or remove from {}'s dependsOn", dep, task)
453                    })
454                    .collect();
455                Some(format!(
456                    "Fix missing dependencies:\n{}",
457                    suggestions.join("\n")
458                ))
459            }
460            cuenv_task_graph::Error::TopologicalSortFailed { .. } => None,
461        };
462        Error::TaskGraph {
463            message: err.to_string(),
464            help,
465        }
466    }
467}
468
469/// Result type alias for cuenv operations
470pub type Result<T> = std::result::Result<T, Error>;
471
472/// Configuration limits
473pub struct Limits {
474    pub max_path_length: usize,
475    pub max_package_name_length: usize,
476    pub max_output_size: usize,
477}
478
479impl Default for Limits {
480    fn default() -> Self {
481        Self {
482            max_path_length: 4096,
483            max_package_name_length: 256,
484            max_output_size: 100 * 1024 * 1024, // 100MB
485        }
486    }
487}
488
489/// A validated directory path that must exist and be a directory
490///
491/// This newtype wrapper ensures that any instance represents a path that:
492/// - Exists on the filesystem
493/// - Is actually a directory (not a file or symlink to file)
494/// - Can be accessed for metadata reading
495///
496/// # Examples
497///
498/// ```rust
499/// use cuenv_core::PackageDir;
500/// use std::path::Path;
501///
502/// // Try to create from current directory
503/// match PackageDir::try_from(Path::new(".")) {
504///     Ok(dir) => println!("Valid directory: {}", dir.as_path().display()),
505///     Err(e) => eprintln!("Invalid directory: {}", e),
506/// }
507/// ```
508#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
509pub struct PackageDir(PathBuf);
510
511impl PackageDir {
512    /// Get the path as a reference
513    #[must_use]
514    pub fn as_path(&self) -> &Path {
515        &self.0
516    }
517
518    /// Convert into the underlying PathBuf
519    #[must_use]
520    pub fn into_path_buf(self) -> PathBuf {
521        self.0
522    }
523}
524
525impl AsRef<Path> for PackageDir {
526    fn as_ref(&self) -> &Path {
527        &self.0
528    }
529}
530
531/// Errors that can occur when validating a PackageDir
532#[derive(Error, Debug, Clone, Diagnostic)]
533pub enum PackageDirError {
534    /// The path does not exist
535    #[error("path does not exist: {0}")]
536    #[diagnostic(
537        code(cuenv::package_dir::not_found),
538        help("Make sure the directory exists and you have permission to access it")
539    )]
540    NotFound(String),
541
542    /// The path exists but is not a directory
543    #[error("path is not a directory: {0}")]
544    #[diagnostic(
545        code(cuenv::package_dir::not_directory),
546        help("The path must point to a directory, not a file")
547    )]
548    NotADirectory(String),
549
550    /// An I/O error occurred while checking the path
551    #[error("io error accessing path: {0}")]
552    #[diagnostic(
553        code(cuenv::package_dir::io_error),
554        help("Check file permissions and ensure you have access to the path")
555    )]
556    Io(String),
557}
558
559impl TryFrom<&Path> for PackageDir {
560    type Error = PackageDirError;
561
562    /// Try to create a PackageDir from a path
563    ///
564    /// # Examples
565    ///
566    /// ```rust
567    /// use cuenv_core::PackageDir;
568    /// use std::path::Path;
569    ///
570    /// match PackageDir::try_from(Path::new(".")) {
571    ///     Ok(dir) => println!("Valid directory"),
572    ///     Err(e) => eprintln!("Error: {}", e),
573    /// }
574    /// ```
575    fn try_from(input: &Path) -> std::result::Result<Self, Self::Error> {
576        match std::fs::metadata(input) {
577            Ok(meta) => {
578                if meta.is_dir() {
579                    Ok(PackageDir(input.to_path_buf()))
580                } else {
581                    Err(PackageDirError::NotADirectory(input.display().to_string()))
582                }
583            }
584            Err(e) => {
585                if e.kind() == std::io::ErrorKind::NotFound {
586                    Err(PackageDirError::NotFound(input.display().to_string()))
587                } else {
588                    Err(PackageDirError::Io(e.to_string()))
589                }
590            }
591        }
592    }
593}
594
595/// A validated CUE package name
596///
597/// Package names must follow CUE naming conventions:
598/// - 1-64 characters in length
599/// - Start with alphanumeric character (A-Z, a-z, 0-9)
600/// - Contain only alphanumeric, hyphen (-), or underscore (_) characters
601///
602/// # Examples
603///
604/// ```rust
605/// use cuenv_core::PackageName;
606///
607/// // Valid package names
608/// assert!(PackageName::try_from("my-package").is_ok());
609/// assert!(PackageName::try_from("package_123").is_ok());
610/// assert!(PackageName::try_from("app").is_ok());
611///
612/// // Invalid package names
613/// assert!(PackageName::try_from("-invalid").is_err());  // starts with hyphen
614/// assert!(PackageName::try_from("invalid.name").is_err());  // contains dot
615/// assert!(PackageName::try_from("").is_err());  // empty
616/// ```
617#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
618pub struct PackageName(String);
619
620impl PackageName {
621    /// Get the package name as a string slice
622    #[must_use]
623    pub fn as_str(&self) -> &str {
624        &self.0
625    }
626
627    /// Convert into the underlying String
628    #[must_use]
629    pub fn into_string(self) -> String {
630        self.0
631    }
632}
633
634impl AsRef<str> for PackageName {
635    fn as_ref(&self) -> &str {
636        &self.0
637    }
638}
639
640impl std::fmt::Display for PackageName {
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        write!(f, "{}", self.0)
643    }
644}
645
646/// Errors that can occur when validating a PackageName
647#[derive(Error, Debug, Clone, Diagnostic)]
648pub enum PackageNameError {
649    /// The package name is invalid
650    #[error("invalid package name: {0}")]
651    #[diagnostic(
652        code(cuenv::package_name::invalid),
653        help(
654            "Package names must be 1-64 characters, start with alphanumeric, and contain only alphanumeric, hyphen, or underscore characters"
655        )
656    )]
657    Invalid(String),
658}
659
660impl TryFrom<&str> for PackageName {
661    type Error = PackageNameError;
662
663    /// Try to create a PackageName from a string
664    ///
665    /// # Examples
666    ///
667    /// ```rust
668    /// use cuenv_core::PackageName;
669    ///
670    /// match PackageName::try_from("my-package") {
671    ///     Ok(name) => println!("Valid package name: {}", name),
672    ///     Err(e) => eprintln!("Error: {}", e),
673    /// }
674    /// ```
675    fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
676        let bytes = s.as_bytes();
677
678        // Check length bounds
679        if bytes.is_empty() || bytes.len() > 64 {
680            return Err(PackageNameError::Invalid(s.to_string()));
681        }
682
683        // Check first character must be alphanumeric
684        let first = bytes[0];
685        let is_alnum =
686            |b: u8| b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit();
687
688        if !is_alnum(first) {
689            return Err(PackageNameError::Invalid(s.to_string()));
690        }
691
692        // Check all characters are valid
693        let valid = |b: u8| is_alnum(b) || b == b'-' || b == b'_';
694        for &b in bytes {
695            if !valid(b) {
696                return Err(PackageNameError::Invalid(s.to_string()));
697            }
698        }
699
700        Ok(PackageName(s.to_string()))
701    }
702}
703
704impl TryFrom<String> for PackageName {
705    type Error = PackageNameError;
706
707    /// Try to create a PackageName from an owned String
708    fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
709        Self::try_from(s.as_str())
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use miette::SourceSpan;
717    use std::path::Path;
718
719    #[test]
720    fn test_error_configuration() {
721        let err = Error::configuration("test message");
722        assert_eq!(err.to_string(), "Configuration error: test message");
723
724        if let Error::Configuration { message, .. } = err {
725            assert_eq!(message, "test message");
726        } else {
727            panic!("Expected Configuration error");
728        }
729    }
730
731    #[test]
732    fn test_error_configuration_with_source() {
733        let src = "test source code";
734        let span = SourceSpan::from(0..4);
735        let err = Error::configuration_with_source("config error", src, Some(span));
736
737        if let Error::Configuration {
738            src: source,
739            span: s,
740            message,
741        } = err
742        {
743            assert_eq!(source, "test source code");
744            assert_eq!(s, Some(SourceSpan::from(0..4)));
745            assert_eq!(message, "config error");
746        } else {
747            panic!("Expected Configuration error");
748        }
749    }
750
751    #[test]
752    fn test_error_ffi() {
753        let err = Error::ffi("test_function", "FFI failed");
754        assert_eq!(
755            err.to_string(),
756            "FFI operation failed in test_function: FFI failed"
757        );
758
759        if let Error::Ffi {
760            function,
761            message,
762            help,
763        } = err
764        {
765            assert_eq!(function, "test_function");
766            assert_eq!(message, "FFI failed");
767            assert!(help.is_none());
768        } else {
769            panic!("Expected Ffi error");
770        }
771    }
772
773    #[test]
774    fn test_error_ffi_with_help() {
775        let err = Error::ffi_with_help("test_func", "error msg", "try this instead");
776
777        if let Error::Ffi {
778            function,
779            message,
780            help,
781        } = err
782        {
783            assert_eq!(function, "test_func");
784            assert_eq!(message, "error msg");
785            assert_eq!(help, Some("try this instead".to_string()));
786        } else {
787            panic!("Expected Ffi error");
788        }
789    }
790
791    #[test]
792    fn test_error_cue_parse() {
793        let path = Path::new("/test/path.cue");
794        let err = Error::cue_parse(path, "parsing failed");
795        assert_eq!(err.to_string(), "CUE parsing failed: parsing failed");
796
797        if let Error::CueParse {
798            path: p, message, ..
799        } = err
800        {
801            assert_eq!(p.as_ref(), Path::new("/test/path.cue"));
802            assert_eq!(message, "parsing failed");
803        } else {
804            panic!("Expected CueParse error");
805        }
806    }
807
808    #[test]
809    fn test_error_cue_parse_with_source() {
810        let path = Path::new("/test/file.cue");
811        let src = "package test";
812        let span = SourceSpan::from(0..7);
813        let suggestions = vec!["Check syntax".to_string(), "Verify imports".to_string()];
814
815        let err = Error::cue_parse_with_source(
816            path,
817            "parse error",
818            src,
819            Some(span),
820            Some(suggestions.clone()),
821        );
822
823        if let Error::CueParse {
824            path: p,
825            src: source,
826            span: s,
827            message,
828            suggestions: sugg,
829        } = err
830        {
831            assert_eq!(p.as_ref(), Path::new("/test/file.cue"));
832            assert_eq!(source, Some("package test".to_string()));
833            assert_eq!(s, Some(SourceSpan::from(0..7)));
834            assert_eq!(message, "parse error");
835            assert_eq!(sugg, Some(suggestions));
836        } else {
837            panic!("Expected CueParse error");
838        }
839    }
840
841    #[test]
842    fn test_error_validation() {
843        let err = Error::validation("validation failed");
844        assert_eq!(err.to_string(), "Validation failed: validation failed");
845
846        if let Error::Validation {
847            message, related, ..
848        } = err
849        {
850            assert_eq!(message, "validation failed");
851            assert!(related.is_empty());
852        } else {
853            panic!("Expected Validation error");
854        }
855    }
856
857    #[test]
858    fn test_error_validation_with_source() {
859        let src = "test validation source";
860        let span = SourceSpan::from(5..15);
861        let err = Error::validation_with_source("validation error", src, Some(span));
862
863        if let Error::Validation {
864            src: source,
865            span: s,
866            message,
867            ..
868        } = err
869        {
870            assert_eq!(source, Some("test validation source".to_string()));
871            assert_eq!(s, Some(SourceSpan::from(5..15)));
872            assert_eq!(message, "validation error");
873        } else {
874            panic!("Expected Validation error");
875        }
876    }
877
878    #[test]
879    fn test_error_timeout() {
880        let err = Error::Timeout { seconds: 30 };
881        assert_eq!(err.to_string(), "Operation timed out after 30 seconds");
882    }
883
884    #[test]
885    fn test_error_from_io_error() {
886        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
887        let err: Error = io_err.into();
888
889        if let Error::Io { operation, .. } = err {
890            assert_eq!(operation, "unknown (unmapped error conversion)");
891        } else {
892            panic!("Expected Io error");
893        }
894    }
895
896    #[test]
897    fn test_error_from_utf8_error() {
898        let bytes = vec![0xFF, 0xFE];
899        let utf8_err = std::str::from_utf8(&bytes).unwrap_err();
900        let err: Error = utf8_err.into();
901
902        assert!(matches!(err, Error::Utf8 { .. }));
903    }
904
905    #[test]
906    fn test_limits_default() {
907        let limits = Limits::default();
908        assert_eq!(limits.max_path_length, 4096);
909        assert_eq!(limits.max_package_name_length, 256);
910        assert_eq!(limits.max_output_size, 100 * 1024 * 1024);
911    }
912
913    #[test]
914    fn test_result_type_alias() {
915        let ok_result: Result<i32> = Ok(42);
916        assert!(ok_result.is_ok());
917        if let Ok(value) = ok_result {
918            assert_eq!(value, 42);
919        }
920
921        let err_result: Result<i32> = Err(Error::configuration("test"));
922        assert!(err_result.is_err());
923    }
924
925    #[test]
926    fn test_error_display() {
927        let errors = vec![
928            (Error::configuration("test"), "Configuration error: test"),
929            (
930                Error::ffi("func", "msg"),
931                "FFI operation failed in func: msg",
932            ),
933            (
934                Error::cue_parse(Path::new("/test"), "msg"),
935                "CUE parsing failed: msg",
936            ),
937            (Error::validation("msg"), "Validation failed: msg"),
938            (
939                Error::Timeout { seconds: 10 },
940                "Operation timed out after 10 seconds",
941            ),
942        ];
943
944        for (error, expected) in errors {
945            assert_eq!(error.to_string(), expected);
946        }
947    }
948
949    #[test]
950    fn test_error_diagnostic_codes() {
951        use miette::Diagnostic;
952
953        let config_err = Error::configuration("test");
954        assert_eq!(
955            config_err.code().unwrap().to_string(),
956            "cuenv::config::invalid"
957        );
958
959        let ffi_err = Error::ffi("func", "msg");
960        assert_eq!(ffi_err.code().unwrap().to_string(), "cuenv::ffi::error");
961
962        let cue_err = Error::cue_parse(Path::new("/test"), "msg");
963        assert_eq!(
964            cue_err.code().unwrap().to_string(),
965            "cuenv::cue::parse_error"
966        );
967
968        let validation_err = Error::validation("msg");
969        assert_eq!(
970            validation_err.code().unwrap().to_string(),
971            "cuenv::validation::failed"
972        );
973
974        let timeout_err = Error::Timeout { seconds: 5 };
975        assert_eq!(timeout_err.code().unwrap().to_string(), "cuenv::timeout");
976    }
977
978    #[test]
979    fn test_package_dir_validation() {
980        // Current directory should be valid
981        let result = PackageDir::try_from(Path::new("."));
982        assert!(result.is_ok(), "Current directory should be valid");
983
984        // Get methods should work
985        let pkg_dir = result.unwrap();
986        assert_eq!(pkg_dir.as_path(), Path::new("."));
987        assert_eq!(pkg_dir.as_ref(), Path::new("."));
988        assert_eq!(pkg_dir.into_path_buf(), PathBuf::from("."));
989
990        // Non-existent directory should fail with NotFound
991        let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
992        assert!(result.is_err());
993        match result.unwrap_err() {
994            PackageDirError::NotFound(_) => {} // Expected
995            other => panic!("Expected NotFound error, got: {:?}", other),
996        }
997
998        // Path to a file should fail with NotADirectory
999        // Create a temporary file
1000        let temp_path = std::env::temp_dir().join("cuenv_test_file");
1001        let file = std::fs::File::create(&temp_path).unwrap();
1002        drop(file);
1003
1004        let result = PackageDir::try_from(temp_path.as_path());
1005        assert!(result.is_err());
1006        match result.unwrap_err() {
1007            PackageDirError::NotADirectory(_) => {} // Expected
1008            other => panic!("Expected NotADirectory error, got: {:?}", other),
1009        }
1010
1011        // Clean up
1012        std::fs::remove_file(temp_path).ok();
1013    }
1014
1015    #[test]
1016    fn test_package_name_validation() {
1017        // Valid package names
1018        let max_len_string = "a".repeat(64);
1019        let valid_names = vec![
1020            "my-package",
1021            "package_123",
1022            "a",        // Single character
1023            "A",        // Uppercase
1024            "0package", // Starts with number
1025            "package-with-hyphens",
1026            "package_with_underscores",
1027            max_len_string.as_str(), // Max length
1028        ];
1029
1030        for name in valid_names {
1031            let result = PackageName::try_from(name);
1032            assert!(result.is_ok(), "'{}' should be valid", name);
1033
1034            // Test the String variant too
1035            let result = PackageName::try_from(name.to_string());
1036            assert!(result.is_ok(), "'{}' as String should be valid", name);
1037
1038            // Verify methods work correctly
1039            let pkg_name = result.unwrap();
1040            assert_eq!(pkg_name.as_str(), name);
1041            assert_eq!(pkg_name.as_ref(), name);
1042            assert_eq!(pkg_name.to_string(), name);
1043            assert_eq!(pkg_name.into_string(), name.to_string());
1044        }
1045
1046        // Invalid package names
1047        let too_long_string = "a".repeat(65);
1048        let invalid_names = vec![
1049            "",                       // Empty
1050            "-invalid",               // Starts with hyphen
1051            "_invalid",               // Starts with underscore
1052            "invalid.name",           // Contains dot
1053            "invalid/name",           // Contains slash
1054            "invalid:name",           // Contains colon
1055            too_long_string.as_str(), // Too long
1056            "invalid@name",           // Contains @
1057            "invalid#name",           // Contains #
1058            "invalid name",           // Contains space
1059        ];
1060
1061        for name in invalid_names {
1062            let result = PackageName::try_from(name);
1063            assert!(result.is_err(), "'{}' should be invalid", name);
1064
1065            // Verify error type is correct
1066            assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
1067        }
1068    }
1069}