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