1pub 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
48pub 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#[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
251impl 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
268pub type Result<T> = std::result::Result<T, Error>;
270
271pub 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, }
285 }
286}
287
288#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
308pub struct PackageDir(PathBuf);
309
310impl PackageDir {
311 #[must_use]
313 pub fn as_path(&self) -> &Path {
314 &self.0
315 }
316
317 #[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#[derive(Error, Debug, Clone, Diagnostic)]
332pub enum PackageDirError {
333 #[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 #[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 #[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 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#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
417pub struct PackageName(String);
418
419impl PackageName {
420 #[must_use]
422 pub fn as_str(&self) -> &str {
423 &self.0
424 }
425
426 #[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#[derive(Error, Debug, Clone, Diagnostic)]
447pub enum PackageNameError {
448 #[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 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
475 let bytes = s.as_bytes();
476
477 if bytes.is_empty() || bytes.len() > 64 {
479 return Err(PackageNameError::Invalid(s.to_string()));
480 }
481
482 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 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 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 let result = PackageDir::try_from(Path::new("."));
781 assert!(result.is_ok(), "Current directory should be valid");
782
783 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 let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
791 assert!(result.is_err());
792 match result.unwrap_err() {
793 PackageDirError::NotFound(_) => {} other => panic!("Expected NotFound error, got: {:?}", other),
795 }
796
797 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(_) => {} other => panic!("Expected NotADirectory error, got: {:?}", other),
808 }
809
810 std::fs::remove_file(temp_path).ok();
812 }
813
814 #[test]
815 fn test_package_name_validation() {
816 let max_len_string = "a".repeat(64);
818 let valid_names = vec![
819 "my-package",
820 "package_123",
821 "a", "A", "0package", "package-with-hyphens",
825 "package_with_underscores",
826 max_len_string.as_str(), ];
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 let result = PackageName::try_from(name.to_string());
835 assert!(result.is_ok(), "'{}' as String should be valid", name);
836
837 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 let too_long_string = "a".repeat(65);
847 let invalid_names = vec![
848 "", "-invalid", "_invalid", "invalid.name", "invalid/name", "invalid:name", too_long_string.as_str(), "invalid@name", "invalid#name", "invalid name", ];
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 assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
866 }
867 }
868}