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