Skip to main content

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