1#![allow(unused_assignments)]
41
42pub mod affected;
43pub mod base;
44pub mod ci;
45pub mod config;
46pub mod contributors;
47pub mod cue;
48pub mod environment;
49pub mod lockfile;
50pub mod manifest;
51pub mod module;
52pub mod owners;
53pub mod paths;
54pub mod rules;
55pub mod secrets;
56pub mod shell;
57pub mod sync;
58pub mod tasks;
59pub mod tools;
60
61pub use affected::{AffectedBy, matches_pattern};
63
64pub use module::{Instance, InstanceKind, ModuleEvaluation};
66
67pub const VERSION: &str = env!("CARGO_PKG_VERSION");
69
70#[cfg(test)]
71pub mod test_utils;
72
73use miette::{Diagnostic, SourceSpan};
74use serde::{Deserialize, Serialize};
75use std::path::{Path, PathBuf};
76use thiserror::Error;
77
78#[derive(Error, Debug, Diagnostic)]
80pub enum Error {
81 #[error("Configuration error: {message}")]
82 #[diagnostic(
83 code(cuenv::config::invalid),
84 help("Check your cuenv.cue configuration file for syntax errors or invalid values")
85 )]
86 Configuration {
87 #[source_code]
88 src: String,
89 #[label("invalid configuration")]
90 span: Option<SourceSpan>,
91 message: String,
92 },
93
94 #[error("FFI operation failed in {function}: {message}")]
95 #[diagnostic(code(cuenv::ffi::error))]
96 Ffi {
97 function: &'static str,
98 message: String,
99 #[help]
100 help: Option<String>,
101 },
102
103 #[error("CUE parsing failed: {message}")]
104 #[diagnostic(code(cuenv::cue::parse_error))]
105 CueParse {
106 path: Box<Path>,
107 #[source_code]
108 src: Option<String>,
109 #[label("parsing failed here")]
110 span: Option<SourceSpan>,
111 message: String,
112 suggestions: Option<Vec<String>>,
113 },
114
115 #[error("I/O {operation} failed{}", path.as_ref().map_or(String::new(), |p| format!(": {}", p.display())))]
116 #[diagnostic(
117 code(cuenv::io::error),
118 help("Check file permissions and ensure the path exists")
119 )]
120 Io {
121 #[source]
122 source: std::io::Error,
123 path: Option<Box<Path>>,
124 operation: String,
125 },
126
127 #[error("Text encoding error")]
128 #[diagnostic(
129 code(cuenv::encoding::utf8),
130 help("The file contains invalid UTF-8. Ensure your files use UTF-8 encoding.")
131 )]
132 Utf8 {
133 #[source]
134 source: std::str::Utf8Error,
135 file: Option<Box<Path>>,
136 },
137
138 #[error("Operation timed out after {seconds} seconds")]
139 #[diagnostic(
140 code(cuenv::timeout),
141 help("Try increasing the timeout or check if the operation is stuck")
142 )]
143 Timeout { seconds: u64 },
144
145 #[error("Validation failed: {message}")]
146 #[diagnostic(code(cuenv::validation::failed))]
147 Validation {
148 #[source_code]
149 src: Option<String>,
150 #[label("validation failed")]
151 span: Option<SourceSpan>,
152 message: String,
153 #[related]
154 related: Vec<Error>,
155 },
156
157 #[error("Task execution failed: {message}")]
158 #[diagnostic(code(cuenv::task::execution))]
159 Execution {
160 message: String,
161 #[help]
162 help: Option<String>,
163 },
164
165 #[error("Tool resolution failed: {message}")]
166 #[diagnostic(code(cuenv::tool::resolution))]
167 ToolResolution {
168 message: String,
169 #[help]
170 help: Option<String>,
171 },
172
173 #[error("Platform error: {message}")]
174 #[diagnostic(
175 code(cuenv::platform::error),
176 help("This platform may not be supported by the tool provider")
177 )]
178 Platform { message: String },
179
180 #[error("Task '{task_name}' failed with exit code {exit_code}")]
181 #[diagnostic(code(cuenv::task::failed))]
182 TaskFailed {
183 task_name: String,
184 exit_code: i32,
185 stdout: String,
186 stderr: String,
187 #[help]
188 help: Option<String>,
189 },
190
191 #[error("Task graph error: {message}")]
192 #[diagnostic(code(cuenv::task::graph))]
193 TaskGraph {
194 message: String,
195 #[help]
196 help: Option<String>,
197 },
198
199 #[error("Secret resolution failed: {message}")]
200 #[diagnostic(code(cuenv::secret::resolution))]
201 SecretResolution {
202 message: String,
203 #[help]
204 help: Option<String>,
205 },
206}
207
208impl Error {
209 #[must_use]
210 pub fn configuration(msg: impl Into<String>) -> Self {
211 Error::Configuration {
212 src: String::new(),
213 span: None,
214 message: msg.into(),
215 }
216 }
217
218 #[must_use]
219 pub fn configuration_with_source(
220 msg: impl Into<String>,
221 src: impl Into<String>,
222 span: Option<SourceSpan>,
223 ) -> Self {
224 Error::Configuration {
225 src: src.into(),
226 span,
227 message: msg.into(),
228 }
229 }
230
231 #[must_use]
232 pub fn ffi(function: &'static str, message: impl Into<String>) -> Self {
233 Error::Ffi {
234 function,
235 message: message.into(),
236 help: None,
237 }
238 }
239
240 #[must_use]
241 pub fn ffi_with_help(
242 function: &'static str,
243 message: impl Into<String>,
244 help: impl Into<String>,
245 ) -> Self {
246 Error::Ffi {
247 function,
248 message: message.into(),
249 help: Some(help.into()),
250 }
251 }
252
253 #[must_use]
254 pub fn cue_parse(path: &Path, message: impl Into<String>) -> Self {
255 Error::CueParse {
256 path: path.into(),
257 src: None,
258 span: None,
259 message: message.into(),
260 suggestions: None,
261 }
262 }
263
264 #[must_use]
265 pub fn cue_parse_with_source(
266 path: &Path,
267 message: impl Into<String>,
268 src: impl Into<String>,
269 span: Option<SourceSpan>,
270 suggestions: Option<Vec<String>>,
271 ) -> Self {
272 Error::CueParse {
273 path: path.into(),
274 src: Some(src.into()),
275 span,
276 message: message.into(),
277 suggestions,
278 }
279 }
280
281 #[must_use]
282 pub fn validation(msg: impl Into<String>) -> Self {
283 Error::Validation {
284 src: None,
285 span: None,
286 message: msg.into(),
287 related: Vec::new(),
288 }
289 }
290
291 #[must_use]
292 pub fn validation_with_source(
293 msg: impl Into<String>,
294 src: impl Into<String>,
295 span: Option<SourceSpan>,
296 ) -> Self {
297 Error::Validation {
298 src: Some(src.into()),
299 span,
300 message: msg.into(),
301 related: Vec::new(),
302 }
303 }
304
305 #[must_use]
306 pub fn execution(msg: impl Into<String>) -> Self {
307 Error::Execution {
308 message: msg.into(),
309 help: None,
310 }
311 }
312
313 #[must_use]
314 pub fn execution_with_help(msg: impl Into<String>, help: impl Into<String>) -> Self {
315 Error::Execution {
316 message: msg.into(),
317 help: Some(help.into()),
318 }
319 }
320
321 #[must_use]
322 pub fn tool_resolution(msg: impl Into<String>) -> Self {
323 Error::ToolResolution {
324 message: msg.into(),
325 help: None,
326 }
327 }
328
329 #[must_use]
330 pub fn tool_resolution_with_help(msg: impl Into<String>, help: impl Into<String>) -> Self {
331 Error::ToolResolution {
332 message: msg.into(),
333 help: Some(help.into()),
334 }
335 }
336
337 #[must_use]
338 pub fn platform(msg: impl Into<String>) -> Self {
339 Error::Platform {
340 message: msg.into(),
341 }
342 }
343
344 #[must_use]
345 pub fn task_failed(
346 task_name: impl Into<String>,
347 exit_code: i32,
348 stdout: impl Into<String>,
349 stderr: impl Into<String>,
350 ) -> Self {
351 Error::TaskFailed {
352 task_name: task_name.into(),
353 exit_code,
354 stdout: stdout.into(),
355 stderr: stderr.into(),
356 help: None,
357 }
358 }
359
360 #[must_use]
361 pub fn task_failed_with_help(
362 task_name: impl Into<String>,
363 exit_code: i32,
364 stdout: impl Into<String>,
365 stderr: impl Into<String>,
366 help: impl Into<String>,
367 ) -> Self {
368 Error::TaskFailed {
369 task_name: task_name.into(),
370 exit_code,
371 stdout: stdout.into(),
372 stderr: stderr.into(),
373 help: Some(help.into()),
374 }
375 }
376
377 #[must_use]
378 pub fn task_graph(message: impl Into<String>) -> Self {
379 Error::TaskGraph {
380 message: message.into(),
381 help: None,
382 }
383 }
384
385 #[must_use]
386 pub fn task_graph_with_help(message: impl Into<String>, help: impl Into<String>) -> Self {
387 Error::TaskGraph {
388 message: message.into(),
389 help: Some(help.into()),
390 }
391 }
392
393 #[must_use]
394 pub fn secret_resolution(message: impl Into<String>) -> Self {
395 Error::SecretResolution {
396 message: message.into(),
397 help: None,
398 }
399 }
400
401 #[must_use]
402 pub fn secret_resolution_with_help(
403 message: impl Into<String>,
404 help: impl Into<String>,
405 ) -> Self {
406 Error::SecretResolution {
407 message: message.into(),
408 help: Some(help.into()),
409 }
410 }
411}
412
413impl From<std::io::Error> for Error {
415 fn from(source: std::io::Error) -> Self {
416 Error::Io {
417 source,
418 path: None,
419 operation: "unknown (unmapped error conversion)".to_string(),
420 }
421 }
422}
423
424impl From<std::str::Utf8Error> for Error {
425 fn from(source: std::str::Utf8Error) -> Self {
426 Error::Utf8 { source, file: None }
427 }
428}
429
430impl From<cuenv_hooks::Error> for Error {
431 fn from(source: cuenv_hooks::Error) -> Self {
432 Error::Execution {
433 message: source.to_string(),
434 help: None,
435 }
436 }
437}
438
439impl From<cuenv_task_graph::Error> for Error {
440 fn from(err: cuenv_task_graph::Error) -> Self {
441 let help = match &err {
442 cuenv_task_graph::Error::CycleDetected { .. } => {
443 Some("Check for circular dependencies between tasks".into())
444 }
445 cuenv_task_graph::Error::MissingDependency { task, dependency } => Some(format!(
446 "Add task '{}' or remove it from {}'s dependsOn",
447 dependency, task
448 )),
449 cuenv_task_graph::Error::MissingDependencies { missing } => {
450 let suggestions: Vec<String> = missing
451 .iter()
452 .map(|(task, dep)| {
453 format!(" - Add '{}' or remove from {}'s dependsOn", dep, task)
454 })
455 .collect();
456 Some(format!(
457 "Fix missing dependencies:\n{}",
458 suggestions.join("\n")
459 ))
460 }
461 cuenv_task_graph::Error::TopologicalSortFailed { .. } => None,
462 };
463 Error::TaskGraph {
464 message: err.to_string(),
465 help,
466 }
467 }
468}
469
470pub type Result<T> = std::result::Result<T, Error>;
472
473#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
475pub enum DryRun {
476 #[default]
478 No,
479 Yes,
481}
482
483impl DryRun {
484 #[must_use]
486 pub const fn is_dry_run(self) -> bool {
487 matches!(self, Self::Yes)
488 }
489}
490
491impl From<bool> for DryRun {
492 fn from(v: bool) -> Self {
493 if v { Self::Yes } else { Self::No }
494 }
495}
496
497#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
499pub enum OutputCapture {
500 #[default]
502 Capture,
503 Stream,
505}
506
507impl OutputCapture {
508 #[must_use]
510 pub const fn should_capture(self) -> bool {
511 matches!(self, Self::Capture)
512 }
513}
514
515impl From<bool> for OutputCapture {
516 fn from(v: bool) -> Self {
517 if v { Self::Capture } else { Self::Stream }
518 }
519}
520
521pub struct Limits {
523 pub max_path_length: usize,
524 pub max_package_name_length: usize,
525 pub max_output_size: usize,
526}
527
528impl Default for Limits {
529 fn default() -> Self {
530 Self {
531 max_path_length: 4096,
532 max_package_name_length: 256,
533 max_output_size: 100 * 1024 * 1024, }
535 }
536}
537
538#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
558pub struct PackageDir(PathBuf);
559
560impl PackageDir {
561 #[must_use]
563 pub fn as_path(&self) -> &Path {
564 &self.0
565 }
566
567 #[must_use]
569 pub fn into_path_buf(self) -> PathBuf {
570 self.0
571 }
572}
573
574impl AsRef<Path> for PackageDir {
575 fn as_ref(&self) -> &Path {
576 &self.0
577 }
578}
579
580#[derive(Error, Debug, Clone, Diagnostic)]
582pub enum PackageDirError {
583 #[error("path does not exist: {0}")]
585 #[diagnostic(
586 code(cuenv::package_dir::not_found),
587 help("Make sure the directory exists and you have permission to access it")
588 )]
589 NotFound(String),
590
591 #[error("path is not a directory: {0}")]
593 #[diagnostic(
594 code(cuenv::package_dir::not_directory),
595 help("The path must point to a directory, not a file")
596 )]
597 NotADirectory(String),
598
599 #[error("io error accessing path: {0}")]
601 #[diagnostic(
602 code(cuenv::package_dir::io_error),
603 help("Check file permissions and ensure you have access to the path")
604 )]
605 Io(String),
606}
607
608impl TryFrom<&Path> for PackageDir {
609 type Error = PackageDirError;
610
611 fn try_from(input: &Path) -> std::result::Result<Self, Self::Error> {
625 match std::fs::metadata(input) {
626 Ok(meta) => {
627 if meta.is_dir() {
628 Ok(PackageDir(input.to_path_buf()))
629 } else {
630 Err(PackageDirError::NotADirectory(input.display().to_string()))
631 }
632 }
633 Err(e) => {
634 if e.kind() == std::io::ErrorKind::NotFound {
635 Err(PackageDirError::NotFound(input.display().to_string()))
636 } else {
637 Err(PackageDirError::Io(e.to_string()))
638 }
639 }
640 }
641 }
642}
643
644#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
667pub struct PackageName(String);
668
669impl PackageName {
670 #[must_use]
672 pub fn as_str(&self) -> &str {
673 &self.0
674 }
675
676 #[must_use]
678 pub fn into_string(self) -> String {
679 self.0
680 }
681}
682
683impl AsRef<str> for PackageName {
684 fn as_ref(&self) -> &str {
685 &self.0
686 }
687}
688
689impl std::fmt::Display for PackageName {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 write!(f, "{}", self.0)
692 }
693}
694
695#[derive(Error, Debug, Clone, Diagnostic)]
697pub enum PackageNameError {
698 #[error("invalid package name: {0}")]
700 #[diagnostic(
701 code(cuenv::package_name::invalid),
702 help(
703 "Package names must be 1-64 characters, start with alphanumeric, and contain only alphanumeric, hyphen, or underscore characters"
704 )
705 )]
706 Invalid(String),
707}
708
709impl TryFrom<&str> for PackageName {
710 type Error = PackageNameError;
711
712 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
725 let bytes = s.as_bytes();
726
727 if bytes.is_empty() || bytes.len() > 64 {
729 return Err(PackageNameError::Invalid(s.to_string()));
730 }
731
732 let first = bytes[0];
734 let is_alnum =
735 |b: u8| b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit();
736
737 if !is_alnum(first) {
738 return Err(PackageNameError::Invalid(s.to_string()));
739 }
740
741 let valid = |b: u8| is_alnum(b) || b == b'-' || b == b'_';
743 for &b in bytes {
744 if !valid(b) {
745 return Err(PackageNameError::Invalid(s.to_string()));
746 }
747 }
748
749 Ok(PackageName(s.to_string()))
750 }
751}
752
753impl TryFrom<String> for PackageName {
754 type Error = PackageNameError;
755
756 fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
758 Self::try_from(s.as_str())
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use miette::SourceSpan;
766 use std::path::Path;
767
768 #[test]
769 fn test_error_configuration() {
770 let err = Error::configuration("test message");
771 assert_eq!(err.to_string(), "Configuration error: test message");
772
773 if let Error::Configuration { message, .. } = err {
774 assert_eq!(message, "test message");
775 } else {
776 panic!("Expected Configuration error");
777 }
778 }
779
780 #[test]
781 fn test_error_configuration_with_source() {
782 let src = "test source code";
783 let span = SourceSpan::from(0..4);
784 let err = Error::configuration_with_source("config error", src, Some(span));
785
786 if let Error::Configuration {
787 src: source,
788 span: s,
789 message,
790 } = err
791 {
792 assert_eq!(source, "test source code");
793 assert_eq!(s, Some(SourceSpan::from(0..4)));
794 assert_eq!(message, "config error");
795 } else {
796 panic!("Expected Configuration error");
797 }
798 }
799
800 #[test]
801 fn test_error_ffi() {
802 let err = Error::ffi("test_function", "FFI failed");
803 assert_eq!(
804 err.to_string(),
805 "FFI operation failed in test_function: FFI failed"
806 );
807
808 if let Error::Ffi {
809 function,
810 message,
811 help,
812 } = err
813 {
814 assert_eq!(function, "test_function");
815 assert_eq!(message, "FFI failed");
816 assert!(help.is_none());
817 } else {
818 panic!("Expected Ffi error");
819 }
820 }
821
822 #[test]
823 fn test_error_ffi_with_help() {
824 let err = Error::ffi_with_help("test_func", "error msg", "try this instead");
825
826 if let Error::Ffi {
827 function,
828 message,
829 help,
830 } = err
831 {
832 assert_eq!(function, "test_func");
833 assert_eq!(message, "error msg");
834 assert_eq!(help, Some("try this instead".to_string()));
835 } else {
836 panic!("Expected Ffi error");
837 }
838 }
839
840 #[test]
841 fn test_error_cue_parse() {
842 let path = Path::new("/test/path.cue");
843 let err = Error::cue_parse(path, "parsing failed");
844 assert_eq!(err.to_string(), "CUE parsing failed: parsing failed");
845
846 if let Error::CueParse {
847 path: p, message, ..
848 } = err
849 {
850 assert_eq!(p.as_ref(), Path::new("/test/path.cue"));
851 assert_eq!(message, "parsing failed");
852 } else {
853 panic!("Expected CueParse error");
854 }
855 }
856
857 #[test]
858 fn test_error_cue_parse_with_source() {
859 let path = Path::new("/test/file.cue");
860 let src = "package test";
861 let span = SourceSpan::from(0..7);
862 let suggestions = vec!["Check syntax".to_string(), "Verify imports".to_string()];
863
864 let err = Error::cue_parse_with_source(
865 path,
866 "parse error",
867 src,
868 Some(span),
869 Some(suggestions.clone()),
870 );
871
872 if let Error::CueParse {
873 path: p,
874 src: source,
875 span: s,
876 message,
877 suggestions: sugg,
878 } = err
879 {
880 assert_eq!(p.as_ref(), Path::new("/test/file.cue"));
881 assert_eq!(source, Some("package test".to_string()));
882 assert_eq!(s, Some(SourceSpan::from(0..7)));
883 assert_eq!(message, "parse error");
884 assert_eq!(sugg, Some(suggestions));
885 } else {
886 panic!("Expected CueParse error");
887 }
888 }
889
890 #[test]
891 fn test_error_validation() {
892 let err = Error::validation("validation failed");
893 assert_eq!(err.to_string(), "Validation failed: validation failed");
894
895 if let Error::Validation {
896 message, related, ..
897 } = err
898 {
899 assert_eq!(message, "validation failed");
900 assert!(related.is_empty());
901 } else {
902 panic!("Expected Validation error");
903 }
904 }
905
906 #[test]
907 fn test_error_validation_with_source() {
908 let src = "test validation source";
909 let span = SourceSpan::from(5..15);
910 let err = Error::validation_with_source("validation error", src, Some(span));
911
912 if let Error::Validation {
913 src: source,
914 span: s,
915 message,
916 ..
917 } = err
918 {
919 assert_eq!(source, Some("test validation source".to_string()));
920 assert_eq!(s, Some(SourceSpan::from(5..15)));
921 assert_eq!(message, "validation error");
922 } else {
923 panic!("Expected Validation error");
924 }
925 }
926
927 #[test]
928 fn test_error_timeout() {
929 let err = Error::Timeout { seconds: 30 };
930 assert_eq!(err.to_string(), "Operation timed out after 30 seconds");
931 }
932
933 #[test]
934 fn test_error_from_io_error() {
935 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
936 let err: Error = io_err.into();
937
938 if let Error::Io { operation, .. } = err {
939 assert_eq!(operation, "unknown (unmapped error conversion)");
940 } else {
941 panic!("Expected Io error");
942 }
943 }
944
945 #[test]
946 fn test_error_from_utf8_error() {
947 let bytes = vec![0xFF, 0xFE];
948 let utf8_err = std::str::from_utf8(&bytes).unwrap_err();
949 let err: Error = utf8_err.into();
950
951 assert!(matches!(err, Error::Utf8 { .. }));
952 }
953
954 #[test]
955 fn test_limits_default() {
956 let limits = Limits::default();
957 assert_eq!(limits.max_path_length, 4096);
958 assert_eq!(limits.max_package_name_length, 256);
959 assert_eq!(limits.max_output_size, 100 * 1024 * 1024);
960 }
961
962 #[test]
963 fn test_result_type_alias() {
964 let ok_result: Result<i32> = Ok(42);
965 assert!(ok_result.is_ok());
966 if let Ok(value) = ok_result {
967 assert_eq!(value, 42);
968 }
969
970 let err_result: Result<i32> = Err(Error::configuration("test"));
971 assert!(err_result.is_err());
972 }
973
974 #[test]
975 fn test_error_display() {
976 let errors = vec![
977 (Error::configuration("test"), "Configuration error: test"),
978 (
979 Error::ffi("func", "msg"),
980 "FFI operation failed in func: msg",
981 ),
982 (
983 Error::cue_parse(Path::new("/test"), "msg"),
984 "CUE parsing failed: msg",
985 ),
986 (Error::validation("msg"), "Validation failed: msg"),
987 (
988 Error::Timeout { seconds: 10 },
989 "Operation timed out after 10 seconds",
990 ),
991 ];
992
993 for (error, expected) in errors {
994 assert_eq!(error.to_string(), expected);
995 }
996 }
997
998 #[test]
999 fn test_error_diagnostic_codes() {
1000 use miette::Diagnostic;
1001
1002 let config_err = Error::configuration("test");
1003 assert_eq!(
1004 config_err.code().unwrap().to_string(),
1005 "cuenv::config::invalid"
1006 );
1007
1008 let ffi_err = Error::ffi("func", "msg");
1009 assert_eq!(ffi_err.code().unwrap().to_string(), "cuenv::ffi::error");
1010
1011 let cue_err = Error::cue_parse(Path::new("/test"), "msg");
1012 assert_eq!(
1013 cue_err.code().unwrap().to_string(),
1014 "cuenv::cue::parse_error"
1015 );
1016
1017 let validation_err = Error::validation("msg");
1018 assert_eq!(
1019 validation_err.code().unwrap().to_string(),
1020 "cuenv::validation::failed"
1021 );
1022
1023 let timeout_err = Error::Timeout { seconds: 5 };
1024 assert_eq!(timeout_err.code().unwrap().to_string(), "cuenv::timeout");
1025 }
1026
1027 #[test]
1028 fn test_package_dir_validation() {
1029 let result = PackageDir::try_from(Path::new("."));
1031 assert!(result.is_ok(), "Current directory should be valid");
1032
1033 let pkg_dir = result.unwrap();
1035 assert_eq!(pkg_dir.as_path(), Path::new("."));
1036 assert_eq!(pkg_dir.as_ref(), Path::new("."));
1037 assert_eq!(pkg_dir.into_path_buf(), PathBuf::from("."));
1038
1039 let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
1041 assert!(result.is_err());
1042 match result.unwrap_err() {
1043 PackageDirError::NotFound(_) => {} other => panic!("Expected NotFound error, got: {:?}", other),
1045 }
1046
1047 let temp_path = std::env::temp_dir().join("cuenv_test_file");
1050 let file = std::fs::File::create(&temp_path).unwrap();
1051 drop(file);
1052
1053 let result = PackageDir::try_from(temp_path.as_path());
1054 assert!(result.is_err());
1055 match result.unwrap_err() {
1056 PackageDirError::NotADirectory(_) => {} other => panic!("Expected NotADirectory error, got: {:?}", other),
1058 }
1059
1060 std::fs::remove_file(temp_path).ok();
1062 }
1063
1064 #[test]
1065 fn test_package_name_validation() {
1066 let max_len_string = "a".repeat(64);
1068 let valid_names = vec![
1069 "my-package",
1070 "package_123",
1071 "a", "A", "0package", "package-with-hyphens",
1075 "package_with_underscores",
1076 max_len_string.as_str(), ];
1078
1079 for name in valid_names {
1080 let result = PackageName::try_from(name);
1081 assert!(result.is_ok(), "'{}' should be valid", name);
1082
1083 let result = PackageName::try_from(name.to_string());
1085 assert!(result.is_ok(), "'{}' as String should be valid", name);
1086
1087 let pkg_name = result.unwrap();
1089 assert_eq!(pkg_name.as_str(), name);
1090 assert_eq!(pkg_name.as_ref(), name);
1091 assert_eq!(pkg_name.to_string(), name);
1092 assert_eq!(pkg_name.into_string(), name.to_string());
1093 }
1094
1095 let too_long_string = "a".repeat(65);
1097 let invalid_names = vec![
1098 "", "-invalid", "_invalid", "invalid.name", "invalid/name", "invalid:name", too_long_string.as_str(), "invalid@name", "invalid#name", "invalid name", ];
1109
1110 for name in invalid_names {
1111 let result = PackageName::try_from(name);
1112 assert!(result.is_err(), "'{}' should be invalid", name);
1113
1114 assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
1116 }
1117 }
1118}