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