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