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