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