1pub 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
49pub 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#[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
255impl 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
272pub type Result<T> = std::result::Result<T, Error>;
274
275pub 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, }
289 }
290}
291
292#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
312pub struct PackageDir(PathBuf);
313
314impl PackageDir {
315 #[must_use]
317 pub fn as_path(&self) -> &Path {
318 &self.0
319 }
320
321 #[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#[derive(Error, Debug, Clone, Diagnostic)]
336pub enum PackageDirError {
337 #[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 #[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 #[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 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#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
421pub struct PackageName(String);
422
423impl PackageName {
424 #[must_use]
426 pub fn as_str(&self) -> &str {
427 &self.0
428 }
429
430 #[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#[derive(Error, Debug, Clone, Diagnostic)]
451pub enum PackageNameError {
452 #[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 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
479 let bytes = s.as_bytes();
480
481 if bytes.is_empty() || bytes.len() > 64 {
483 return Err(PackageNameError::Invalid(s.to_string()));
484 }
485
486 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 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 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 let result = PackageDir::try_from(Path::new("."));
785 assert!(result.is_ok(), "Current directory should be valid");
786
787 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 let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
795 assert!(result.is_err());
796 match result.unwrap_err() {
797 PackageDirError::NotFound(_) => {} other => panic!("Expected NotFound error, got: {:?}", other),
799 }
800
801 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(_) => {} other => panic!("Expected NotADirectory error, got: {:?}", other),
812 }
813
814 std::fs::remove_file(temp_path).ok();
816 }
817
818 #[test]
819 fn test_package_name_validation() {
820 let max_len_string = "a".repeat(64);
822 let valid_names = vec![
823 "my-package",
824 "package_123",
825 "a", "A", "0package", "package-with-hyphens",
829 "package_with_underscores",
830 max_len_string.as_str(), ];
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 let result = PackageName::try_from(name.to_string());
839 assert!(result.is_ok(), "'{}' as String should be valid", name);
840
841 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 let too_long_string = "a".repeat(65);
851 let invalid_names = vec![
852 "", "-invalid", "_invalid", "invalid.name", "invalid/name", "invalid:name", too_long_string.as_str(), "invalid@name", "invalid#name", "invalid name", ];
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 assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
870 }
871 }
872}