1pub mod config;
39pub mod environment;
40pub mod hooks;
41pub mod manifest;
42pub mod secrets;
43pub mod shell;
44pub mod tasks;
45
46#[cfg(test)]
47mod schema_tests;
48
49use miette::{Diagnostic, SourceSpan};
50use serde::{Deserialize, Serialize};
51use std::path::{Path, PathBuf};
52use thiserror::Error;
53
54#[derive(Error, Debug, Diagnostic)]
56pub enum Error {
57 #[error("Configuration error")]
58 #[diagnostic(
59 code(cuenv::config::invalid),
60 help("Check your cuenv.cue configuration file for syntax errors or invalid values")
61 )]
62 Configuration {
63 #[source_code]
64 src: String,
65 #[label("invalid configuration")]
66 span: Option<SourceSpan>,
67 message: String,
68 },
69
70 #[error("FFI operation failed")]
71 #[diagnostic(code(cuenv::ffi::error))]
72 Ffi {
73 function: &'static str,
74 message: String,
75 #[help]
76 help: Option<String>,
77 },
78
79 #[error("CUE parsing failed")]
80 #[diagnostic(code(cuenv::cue::parse_error))]
81 CueParse {
82 path: Box<Path>,
83 #[source_code]
84 src: Option<String>,
85 #[label("parsing failed here")]
86 span: Option<SourceSpan>,
87 message: String,
88 suggestions: Option<Vec<String>>,
89 },
90
91 #[error("I/O operation failed")]
92 #[diagnostic(
93 code(cuenv::io::error),
94 help("Check file permissions and ensure the path exists")
95 )]
96 Io {
97 #[source]
98 source: std::io::Error,
99 path: Option<Box<Path>>,
100 operation: String,
101 },
102
103 #[error("Text encoding error")]
104 #[diagnostic(
105 code(cuenv::encoding::utf8),
106 help("The file contains invalid UTF-8. Ensure your files use UTF-8 encoding.")
107 )]
108 Utf8 {
109 #[source]
110 source: std::str::Utf8Error,
111 file: Option<Box<Path>>,
112 },
113
114 #[error("Operation timed out after {seconds} seconds")]
115 #[diagnostic(
116 code(cuenv::timeout),
117 help("Try increasing the timeout or check if the operation is stuck")
118 )]
119 Timeout { seconds: u64 },
120
121 #[error("Validation failed")]
122 #[diagnostic(code(cuenv::validation::failed))]
123 Validation {
124 #[source_code]
125 src: Option<String>,
126 #[label("validation failed")]
127 span: Option<SourceSpan>,
128 message: String,
129 #[related]
130 related: Vec<Error>,
131 },
132}
133
134impl Error {
135 pub fn configuration(msg: impl Into<String>) -> Self {
136 Error::Configuration {
137 src: String::new(),
138 span: None,
139 message: msg.into(),
140 }
141 }
142
143 pub fn configuration_with_source(
144 msg: impl Into<String>,
145 src: impl Into<String>,
146 span: Option<SourceSpan>,
147 ) -> Self {
148 Error::Configuration {
149 src: src.into(),
150 span,
151 message: msg.into(),
152 }
153 }
154
155 pub fn ffi(function: &'static str, message: impl Into<String>) -> Self {
156 Error::Ffi {
157 function,
158 message: message.into(),
159 help: None,
160 }
161 }
162
163 pub fn ffi_with_help(
164 function: &'static str,
165 message: impl Into<String>,
166 help: impl Into<String>,
167 ) -> Self {
168 Error::Ffi {
169 function,
170 message: message.into(),
171 help: Some(help.into()),
172 }
173 }
174
175 pub fn cue_parse(path: &Path, message: impl Into<String>) -> Self {
176 Error::CueParse {
177 path: path.into(),
178 src: None,
179 span: None,
180 message: message.into(),
181 suggestions: None,
182 }
183 }
184
185 pub fn cue_parse_with_source(
186 path: &Path,
187 message: impl Into<String>,
188 src: impl Into<String>,
189 span: Option<SourceSpan>,
190 suggestions: Option<Vec<String>>,
191 ) -> Self {
192 Error::CueParse {
193 path: path.into(),
194 src: Some(src.into()),
195 span,
196 message: message.into(),
197 suggestions,
198 }
199 }
200
201 pub fn validation(msg: impl Into<String>) -> Self {
202 Error::Validation {
203 src: None,
204 span: None,
205 message: msg.into(),
206 related: Vec::new(),
207 }
208 }
209
210 pub fn validation_with_source(
211 msg: impl Into<String>,
212 src: impl Into<String>,
213 span: Option<SourceSpan>,
214 ) -> Self {
215 Error::Validation {
216 src: Some(src.into()),
217 span,
218 message: msg.into(),
219 related: Vec::new(),
220 }
221 }
222}
223
224impl From<std::io::Error> for Error {
226 fn from(source: std::io::Error) -> Self {
227 Error::Io {
228 source,
229 path: None,
230 operation: "unknown".to_string(),
231 }
232 }
233}
234
235impl From<std::str::Utf8Error> for Error {
236 fn from(source: std::str::Utf8Error) -> Self {
237 Error::Utf8 { source, file: None }
238 }
239}
240
241pub type Result<T> = std::result::Result<T, Error>;
243
244pub struct Limits {
246 pub max_path_length: usize,
247 pub max_package_name_length: usize,
248 pub max_output_size: usize,
249}
250
251impl Default for Limits {
252 fn default() -> Self {
253 Self {
254 max_path_length: 4096,
255 max_package_name_length: 256,
256 max_output_size: 100 * 1024 * 1024, }
258 }
259}
260
261#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
281pub struct PackageDir(PathBuf);
282
283impl PackageDir {
284 #[must_use]
286 pub fn as_path(&self) -> &Path {
287 &self.0
288 }
289
290 #[must_use]
292 pub fn into_path_buf(self) -> PathBuf {
293 self.0
294 }
295}
296
297impl AsRef<Path> for PackageDir {
298 fn as_ref(&self) -> &Path {
299 &self.0
300 }
301}
302
303#[derive(Error, Debug, Clone, Diagnostic)]
305pub enum PackageDirError {
306 #[error("path does not exist: {0}")]
308 #[diagnostic(
309 code(cuenv::package_dir::not_found),
310 help("Make sure the directory exists and you have permission to access it")
311 )]
312 NotFound(String),
313
314 #[error("path is not a directory: {0}")]
316 #[diagnostic(
317 code(cuenv::package_dir::not_directory),
318 help("The path must point to a directory, not a file")
319 )]
320 NotADirectory(String),
321
322 #[error("io error accessing path: {0}")]
324 #[diagnostic(
325 code(cuenv::package_dir::io_error),
326 help("Check file permissions and ensure you have access to the path")
327 )]
328 Io(String),
329}
330
331impl TryFrom<&Path> for PackageDir {
332 type Error = PackageDirError;
333
334 fn try_from(input: &Path) -> std::result::Result<Self, Self::Error> {
348 match std::fs::metadata(input) {
349 Ok(meta) => {
350 if meta.is_dir() {
351 Ok(PackageDir(input.to_path_buf()))
352 } else {
353 Err(PackageDirError::NotADirectory(input.display().to_string()))
354 }
355 }
356 Err(e) => {
357 if e.kind() == std::io::ErrorKind::NotFound {
358 Err(PackageDirError::NotFound(input.display().to_string()))
359 } else {
360 Err(PackageDirError::Io(e.to_string()))
361 }
362 }
363 }
364 }
365}
366
367#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
390pub struct PackageName(String);
391
392impl PackageName {
393 #[must_use]
395 pub fn as_str(&self) -> &str {
396 &self.0
397 }
398
399 #[must_use]
401 pub fn into_string(self) -> String {
402 self.0
403 }
404}
405
406impl AsRef<str> for PackageName {
407 fn as_ref(&self) -> &str {
408 &self.0
409 }
410}
411
412impl std::fmt::Display for PackageName {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 write!(f, "{}", self.0)
415 }
416}
417
418#[derive(Error, Debug, Clone, Diagnostic)]
420pub enum PackageNameError {
421 #[error("invalid package name: {0}")]
423 #[diagnostic(
424 code(cuenv::package_name::invalid),
425 help(
426 "Package names must be 1-64 characters, start with alphanumeric, and contain only alphanumeric, hyphen, or underscore characters"
427 )
428 )]
429 Invalid(String),
430}
431
432impl TryFrom<&str> for PackageName {
433 type Error = PackageNameError;
434
435 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
448 let bytes = s.as_bytes();
449
450 if bytes.is_empty() || bytes.len() > 64 {
452 return Err(PackageNameError::Invalid(s.to_string()));
453 }
454
455 let first = bytes[0];
457 let is_alnum =
458 |b: u8| b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit();
459
460 if !is_alnum(first) {
461 return Err(PackageNameError::Invalid(s.to_string()));
462 }
463
464 let valid = |b: u8| is_alnum(b) || b == b'-' || b == b'_';
466 for &b in bytes {
467 if !valid(b) {
468 return Err(PackageNameError::Invalid(s.to_string()));
469 }
470 }
471
472 Ok(PackageName(s.to_string()))
473 }
474}
475
476impl TryFrom<String> for PackageName {
477 type Error = PackageNameError;
478
479 fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
481 Self::try_from(s.as_str())
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use miette::SourceSpan;
489 use std::path::Path;
490
491 #[test]
492 fn test_error_configuration() {
493 let err = Error::configuration("test message");
494 assert_eq!(err.to_string(), "Configuration error");
495
496 if let Error::Configuration { message, .. } = err {
497 assert_eq!(message, "test message");
498 } else {
499 panic!("Expected Configuration error");
500 }
501 }
502
503 #[test]
504 fn test_error_configuration_with_source() {
505 let src = "test source code";
506 let span = SourceSpan::from(0..4);
507 let err = Error::configuration_with_source("config error", src, Some(span));
508
509 if let Error::Configuration {
510 src: source,
511 span: s,
512 message,
513 } = err
514 {
515 assert_eq!(source, "test source code");
516 assert_eq!(s, Some(SourceSpan::from(0..4)));
517 assert_eq!(message, "config error");
518 } else {
519 panic!("Expected Configuration error");
520 }
521 }
522
523 #[test]
524 fn test_error_ffi() {
525 let err = Error::ffi("test_function", "FFI failed");
526 assert_eq!(err.to_string(), "FFI operation failed");
527
528 if let Error::Ffi {
529 function,
530 message,
531 help,
532 } = err
533 {
534 assert_eq!(function, "test_function");
535 assert_eq!(message, "FFI failed");
536 assert!(help.is_none());
537 } else {
538 panic!("Expected Ffi error");
539 }
540 }
541
542 #[test]
543 fn test_error_ffi_with_help() {
544 let err = Error::ffi_with_help("test_func", "error msg", "try this instead");
545
546 if let Error::Ffi {
547 function,
548 message,
549 help,
550 } = err
551 {
552 assert_eq!(function, "test_func");
553 assert_eq!(message, "error msg");
554 assert_eq!(help, Some("try this instead".to_string()));
555 } else {
556 panic!("Expected Ffi error");
557 }
558 }
559
560 #[test]
561 fn test_error_cue_parse() {
562 let path = Path::new("/test/path.cue");
563 let err = Error::cue_parse(path, "parsing failed");
564 assert_eq!(err.to_string(), "CUE parsing failed");
565
566 if let Error::CueParse {
567 path: p, message, ..
568 } = err
569 {
570 assert_eq!(p.as_ref(), Path::new("/test/path.cue"));
571 assert_eq!(message, "parsing failed");
572 } else {
573 panic!("Expected CueParse error");
574 }
575 }
576
577 #[test]
578 fn test_error_cue_parse_with_source() {
579 let path = Path::new("/test/file.cue");
580 let src = "package test";
581 let span = SourceSpan::from(0..7);
582 let suggestions = vec!["Check syntax".to_string(), "Verify imports".to_string()];
583
584 let err = Error::cue_parse_with_source(
585 path,
586 "parse error",
587 src,
588 Some(span),
589 Some(suggestions.clone()),
590 );
591
592 if let Error::CueParse {
593 path: p,
594 src: source,
595 span: s,
596 message,
597 suggestions: sugg,
598 } = err
599 {
600 assert_eq!(p.as_ref(), Path::new("/test/file.cue"));
601 assert_eq!(source, Some("package test".to_string()));
602 assert_eq!(s, Some(SourceSpan::from(0..7)));
603 assert_eq!(message, "parse error");
604 assert_eq!(sugg, Some(suggestions));
605 } else {
606 panic!("Expected CueParse error");
607 }
608 }
609
610 #[test]
611 fn test_error_validation() {
612 let err = Error::validation("validation failed");
613 assert_eq!(err.to_string(), "Validation failed");
614
615 if let Error::Validation {
616 message, related, ..
617 } = err
618 {
619 assert_eq!(message, "validation failed");
620 assert!(related.is_empty());
621 } else {
622 panic!("Expected Validation error");
623 }
624 }
625
626 #[test]
627 fn test_error_validation_with_source() {
628 let src = "test validation source";
629 let span = SourceSpan::from(5..15);
630 let err = Error::validation_with_source("validation error", src, Some(span));
631
632 if let Error::Validation {
633 src: source,
634 span: s,
635 message,
636 ..
637 } = err
638 {
639 assert_eq!(source, Some("test validation source".to_string()));
640 assert_eq!(s, Some(SourceSpan::from(5..15)));
641 assert_eq!(message, "validation error");
642 } else {
643 panic!("Expected Validation error");
644 }
645 }
646
647 #[test]
648 fn test_error_timeout() {
649 let err = Error::Timeout { seconds: 30 };
650 assert_eq!(err.to_string(), "Operation timed out after 30 seconds");
651 }
652
653 #[test]
654 fn test_error_from_io_error() {
655 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
656 let err: Error = io_err.into();
657
658 if let Error::Io { operation, .. } = err {
659 assert_eq!(operation, "unknown");
660 } else {
661 panic!("Expected Io error");
662 }
663 }
664
665 #[test]
666 fn test_error_from_utf8_error() {
667 let bytes = vec![0xFF, 0xFE];
668 let utf8_err = std::str::from_utf8(&bytes).unwrap_err();
669 let err: Error = utf8_err.into();
670
671 assert!(matches!(err, Error::Utf8 { .. }));
672 }
673
674 #[test]
675 fn test_limits_default() {
676 let limits = Limits::default();
677 assert_eq!(limits.max_path_length, 4096);
678 assert_eq!(limits.max_package_name_length, 256);
679 assert_eq!(limits.max_output_size, 100 * 1024 * 1024);
680 }
681
682 #[test]
683 fn test_result_type_alias() {
684 let ok_result: Result<i32> = Ok(42);
685 assert!(ok_result.is_ok());
686 if let Ok(value) = ok_result {
687 assert_eq!(value, 42);
688 }
689
690 let err_result: Result<i32> = Err(Error::configuration("test"));
691 assert!(err_result.is_err());
692 }
693
694 #[test]
695 fn test_error_display() {
696 let errors = vec![
697 (Error::configuration("test"), "Configuration error"),
698 (Error::ffi("func", "msg"), "FFI operation failed"),
699 (
700 Error::cue_parse(Path::new("/test"), "msg"),
701 "CUE parsing failed",
702 ),
703 (Error::validation("msg"), "Validation failed"),
704 (
705 Error::Timeout { seconds: 10 },
706 "Operation timed out after 10 seconds",
707 ),
708 ];
709
710 for (error, expected) in errors {
711 assert_eq!(error.to_string(), expected);
712 }
713 }
714
715 #[test]
716 fn test_error_diagnostic_codes() {
717 use miette::Diagnostic;
718
719 let config_err = Error::configuration("test");
720 assert_eq!(
721 config_err.code().unwrap().to_string(),
722 "cuenv::config::invalid"
723 );
724
725 let ffi_err = Error::ffi("func", "msg");
726 assert_eq!(ffi_err.code().unwrap().to_string(), "cuenv::ffi::error");
727
728 let cue_err = Error::cue_parse(Path::new("/test"), "msg");
729 assert_eq!(
730 cue_err.code().unwrap().to_string(),
731 "cuenv::cue::parse_error"
732 );
733
734 let validation_err = Error::validation("msg");
735 assert_eq!(
736 validation_err.code().unwrap().to_string(),
737 "cuenv::validation::failed"
738 );
739
740 let timeout_err = Error::Timeout { seconds: 5 };
741 assert_eq!(timeout_err.code().unwrap().to_string(), "cuenv::timeout");
742 }
743
744 #[test]
745 fn test_package_dir_validation() {
746 let result = PackageDir::try_from(Path::new("."));
748 assert!(result.is_ok(), "Current directory should be valid");
749
750 let pkg_dir = result.unwrap();
752 assert_eq!(pkg_dir.as_path(), Path::new("."));
753 assert_eq!(pkg_dir.as_ref(), Path::new("."));
754 assert_eq!(pkg_dir.into_path_buf(), PathBuf::from("."));
755
756 let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
758 assert!(result.is_err());
759 match result.unwrap_err() {
760 PackageDirError::NotFound(_) => {} other => panic!("Expected NotFound error, got: {:?}", other),
762 }
763
764 let temp_path = std::env::temp_dir().join("cuenv_test_file");
767 let file = std::fs::File::create(&temp_path).unwrap();
768 drop(file);
769
770 let result = PackageDir::try_from(temp_path.as_path());
771 assert!(result.is_err());
772 match result.unwrap_err() {
773 PackageDirError::NotADirectory(_) => {} other => panic!("Expected NotADirectory error, got: {:?}", other),
775 }
776
777 std::fs::remove_file(temp_path).ok();
779 }
780
781 #[test]
782 fn test_package_name_validation() {
783 let max_len_string = "a".repeat(64);
785 let valid_names = vec![
786 "my-package",
787 "package_123",
788 "a", "A", "0package", "package-with-hyphens",
792 "package_with_underscores",
793 max_len_string.as_str(), ];
795
796 for name in valid_names {
797 let result = PackageName::try_from(name);
798 assert!(result.is_ok(), "'{}' should be valid", name);
799
800 let result = PackageName::try_from(name.to_string());
802 assert!(result.is_ok(), "'{}' as String should be valid", name);
803
804 let pkg_name = result.unwrap();
806 assert_eq!(pkg_name.as_str(), name);
807 assert_eq!(pkg_name.as_ref(), name);
808 assert_eq!(pkg_name.to_string(), name);
809 assert_eq!(pkg_name.into_string(), name.to_string());
810 }
811
812 let too_long_string = "a".repeat(65);
814 let invalid_names = vec![
815 "", "-invalid", "_invalid", "invalid.name", "invalid/name", "invalid:name", too_long_string.as_str(), "invalid@name", "invalid#name", "invalid name", ];
826
827 for name in invalid_names {
828 let result = PackageName::try_from(name);
829 assert!(result.is_err(), "'{}' should be invalid", name);
830
831 assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
833 }
834 }
835}