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