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