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