#![allow(unused_assignments)]
pub mod affected;
pub mod base;
pub mod ci;
pub mod config;
pub mod contributors;
pub mod cue;
pub mod environment;
pub mod http;
pub mod lockfile;
pub mod manifest;
pub mod module;
pub mod owners;
pub mod paths;
pub mod rules;
pub mod runtime;
pub mod secrets;
pub mod shell;
pub mod sync;
pub mod tasks;
pub mod tools;
pub use affected::{AffectedBy, matches_pattern};
pub use module::{Instance, InstanceKind, ModuleEvaluation};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
pub mod test_utils;
use miette::{Diagnostic, SourceSpan};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]
pub enum Error {
#[error("Configuration error: {message}")]
#[diagnostic(
code(cuenv::config::invalid),
help("Check your cuenv.cue configuration file for syntax errors or invalid values")
)]
Configuration {
#[source_code]
src: String,
#[label("invalid configuration")]
span: Option<SourceSpan>,
message: String,
},
#[error("FFI operation failed in {function}: {message}")]
#[diagnostic(code(cuenv::ffi::error))]
Ffi {
function: &'static str,
message: String,
#[help]
help: Option<String>,
},
#[error("CUE parsing failed: {message}")]
#[diagnostic(code(cuenv::cue::parse_error))]
CueParse {
path: Box<Path>,
#[source_code]
src: Option<String>,
#[label("parsing failed here")]
span: Option<SourceSpan>,
message: String,
suggestions: Option<Vec<String>>,
},
#[error("I/O {operation} failed{}", path.as_ref().map_or(String::new(), |p| format!(": {}", p.display())))]
#[diagnostic(
code(cuenv::io::error),
help("Check file permissions and ensure the path exists")
)]
Io {
#[source]
source: std::io::Error,
path: Option<Box<Path>>,
operation: String,
},
#[error("Text encoding error")]
#[diagnostic(
code(cuenv::encoding::utf8),
help("The file contains invalid UTF-8. Ensure your files use UTF-8 encoding.")
)]
Utf8 {
#[source]
source: std::str::Utf8Error,
file: Option<Box<Path>>,
},
#[error("Operation timed out after {seconds} seconds")]
#[diagnostic(
code(cuenv::timeout),
help("Try increasing the timeout or check if the operation is stuck")
)]
Timeout { seconds: u64 },
#[error("Validation failed: {message}")]
#[diagnostic(code(cuenv::validation::failed))]
Validation {
#[source_code]
src: Option<String>,
#[label("validation failed")]
span: Option<SourceSpan>,
message: String,
#[related]
related: Vec<Error>,
},
#[error("Task execution failed: {message}")]
#[diagnostic(code(cuenv::task::execution))]
Execution {
message: String,
#[help]
help: Option<String>,
},
#[error("Tool resolution failed: {message}")]
#[diagnostic(code(cuenv::tool::resolution))]
ToolResolution {
message: String,
#[help]
help: Option<String>,
},
#[error("Platform error: {message}")]
#[diagnostic(
code(cuenv::platform::error),
help("This platform may not be supported by the tool provider")
)]
Platform { message: String },
#[error("Task '{task_name}' failed with exit code {exit_code}")]
#[diagnostic(code(cuenv::task::failed))]
TaskFailed {
task_name: String,
exit_code: i32,
stdout: String,
stderr: String,
#[help]
help: Option<String>,
},
#[error("Task graph error: {message}")]
#[diagnostic(code(cuenv::task::graph))]
TaskGraph {
message: String,
#[help]
help: Option<String>,
},
#[error("Secret resolution failed: {message}")]
#[diagnostic(code(cuenv::secret::resolution))]
SecretResolution {
message: String,
#[help]
help: Option<String>,
},
}
impl Error {
#[must_use]
pub fn configuration(msg: impl Into<String>) -> Self {
Error::Configuration {
src: String::new(),
span: None,
message: msg.into(),
}
}
#[must_use]
pub fn configuration_with_source(
msg: impl Into<String>,
src: impl Into<String>,
span: Option<SourceSpan>,
) -> Self {
Error::Configuration {
src: src.into(),
span,
message: msg.into(),
}
}
#[must_use]
pub fn ffi(function: &'static str, message: impl Into<String>) -> Self {
Error::Ffi {
function,
message: message.into(),
help: None,
}
}
#[must_use]
pub fn ffi_with_help(
function: &'static str,
message: impl Into<String>,
help: impl Into<String>,
) -> Self {
Error::Ffi {
function,
message: message.into(),
help: Some(help.into()),
}
}
#[must_use]
pub fn cue_parse(path: &Path, message: impl Into<String>) -> Self {
Error::CueParse {
path: path.into(),
src: None,
span: None,
message: message.into(),
suggestions: None,
}
}
#[must_use]
pub fn cue_parse_with_source(
path: &Path,
message: impl Into<String>,
src: impl Into<String>,
span: Option<SourceSpan>,
suggestions: Option<Vec<String>>,
) -> Self {
Error::CueParse {
path: path.into(),
src: Some(src.into()),
span,
message: message.into(),
suggestions,
}
}
#[must_use]
pub fn validation(msg: impl Into<String>) -> Self {
Error::Validation {
src: None,
span: None,
message: msg.into(),
related: Vec::new(),
}
}
#[must_use]
pub fn validation_with_source(
msg: impl Into<String>,
src: impl Into<String>,
span: Option<SourceSpan>,
) -> Self {
Error::Validation {
src: Some(src.into()),
span,
message: msg.into(),
related: Vec::new(),
}
}
#[must_use]
pub fn execution(msg: impl Into<String>) -> Self {
Error::Execution {
message: msg.into(),
help: None,
}
}
#[must_use]
pub fn execution_with_help(msg: impl Into<String>, help: impl Into<String>) -> Self {
Error::Execution {
message: msg.into(),
help: Some(help.into()),
}
}
#[must_use]
pub fn tool_resolution(msg: impl Into<String>) -> Self {
Error::ToolResolution {
message: msg.into(),
help: None,
}
}
#[must_use]
pub fn tool_resolution_with_help(msg: impl Into<String>, help: impl Into<String>) -> Self {
Error::ToolResolution {
message: msg.into(),
help: Some(help.into()),
}
}
#[must_use]
pub fn platform(msg: impl Into<String>) -> Self {
Error::Platform {
message: msg.into(),
}
}
#[must_use]
pub fn task_failed(
task_name: impl Into<String>,
exit_code: i32,
stdout: impl Into<String>,
stderr: impl Into<String>,
) -> Self {
Error::TaskFailed {
task_name: task_name.into(),
exit_code,
stdout: stdout.into(),
stderr: stderr.into(),
help: None,
}
}
#[must_use]
pub fn task_failed_with_help(
task_name: impl Into<String>,
exit_code: i32,
stdout: impl Into<String>,
stderr: impl Into<String>,
help: impl Into<String>,
) -> Self {
Error::TaskFailed {
task_name: task_name.into(),
exit_code,
stdout: stdout.into(),
stderr: stderr.into(),
help: Some(help.into()),
}
}
#[must_use]
pub fn task_graph(message: impl Into<String>) -> Self {
Error::TaskGraph {
message: message.into(),
help: None,
}
}
#[must_use]
pub fn task_graph_with_help(message: impl Into<String>, help: impl Into<String>) -> Self {
Error::TaskGraph {
message: message.into(),
help: Some(help.into()),
}
}
#[must_use]
pub fn secret_resolution(message: impl Into<String>) -> Self {
Error::SecretResolution {
message: message.into(),
help: None,
}
}
#[must_use]
pub fn secret_resolution_with_help(
message: impl Into<String>,
help: impl Into<String>,
) -> Self {
Error::SecretResolution {
message: message.into(),
help: Some(help.into()),
}
}
}
impl From<std::io::Error> for Error {
fn from(source: std::io::Error) -> Self {
Error::Io {
source,
path: None,
operation: "unknown (unmapped error conversion)".to_string(),
}
}
}
impl From<std::str::Utf8Error> for Error {
fn from(source: std::str::Utf8Error) -> Self {
Error::Utf8 { source, file: None }
}
}
impl From<cuenv_hooks::Error> for Error {
fn from(source: cuenv_hooks::Error) -> Self {
Error::Execution {
message: source.to_string(),
help: None,
}
}
}
impl From<cuenv_task_graph::Error> for Error {
fn from(err: cuenv_task_graph::Error) -> Self {
let help = match &err {
cuenv_task_graph::Error::CycleDetected { .. } => {
Some("Check for circular dependencies between tasks".into())
}
cuenv_task_graph::Error::MissingDependency { task, dependency } => Some(format!(
"Add task '{}' or remove it from {}'s dependsOn",
dependency, task
)),
cuenv_task_graph::Error::MissingDependencies { missing } => {
let suggestions: Vec<String> = missing
.iter()
.map(|(task, dep)| {
format!(" - Add '{}' or remove from {}'s dependsOn", dep, task)
})
.collect();
Some(format!(
"Fix missing dependencies:\n{}",
suggestions.join("\n")
))
}
cuenv_task_graph::Error::TopologicalSortFailed { .. } => None,
cuenv_task_graph::Error::DuplicateNodeName {
name,
existing_kind,
new_kind,
} => Some(format!(
"Rename the {new_kind} '{name}' to avoid collision with the existing {existing_kind}"
)),
};
Error::TaskGraph {
message: err.to_string(),
help,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum DryRun {
#[default]
No,
Yes,
}
impl DryRun {
#[must_use]
pub const fn is_dry_run(self) -> bool {
matches!(self, Self::Yes)
}
}
impl From<bool> for DryRun {
fn from(v: bool) -> Self {
if v { Self::Yes } else { Self::No }
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum OutputCapture {
#[default]
Capture,
Stream,
}
impl OutputCapture {
#[must_use]
pub const fn should_capture(self) -> bool {
matches!(self, Self::Capture)
}
}
impl From<bool> for OutputCapture {
fn from(v: bool) -> Self {
if v { Self::Capture } else { Self::Stream }
}
}
pub struct Limits {
pub max_path_length: usize,
pub max_package_name_length: usize,
pub max_output_size: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_path_length: 4096,
max_package_name_length: 256,
max_output_size: 100 * 1024 * 1024, }
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PackageDir(PathBuf);
impl PackageDir {
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
#[must_use]
pub fn into_path_buf(self) -> PathBuf {
self.0
}
}
impl AsRef<Path> for PackageDir {
fn as_ref(&self) -> &Path {
&self.0
}
}
#[derive(Error, Debug, Clone, Diagnostic)]
pub enum PackageDirError {
#[error("path does not exist: {0}")]
#[diagnostic(
code(cuenv::package_dir::not_found),
help("Make sure the directory exists and you have permission to access it")
)]
NotFound(String),
#[error("path is not a directory: {0}")]
#[diagnostic(
code(cuenv::package_dir::not_directory),
help("The path must point to a directory, not a file")
)]
NotADirectory(String),
#[error("io error accessing path: {0}")]
#[diagnostic(
code(cuenv::package_dir::io_error),
help("Check file permissions and ensure you have access to the path")
)]
Io(String),
}
impl TryFrom<&Path> for PackageDir {
type Error = PackageDirError;
fn try_from(input: &Path) -> std::result::Result<Self, Self::Error> {
match std::fs::metadata(input) {
Ok(meta) => {
if meta.is_dir() {
Ok(PackageDir(input.to_path_buf()))
} else {
Err(PackageDirError::NotADirectory(input.display().to_string()))
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Err(PackageDirError::NotFound(input.display().to_string()))
} else {
Err(PackageDirError::Io(e.to_string()))
}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PackageName(String);
impl PackageName {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for PackageName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for PackageName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Error, Debug, Clone, Diagnostic)]
pub enum PackageNameError {
#[error("invalid package name: {0}")]
#[diagnostic(
code(cuenv::package_name::invalid),
help(
"Package names must be 1-64 characters, start with alphanumeric, and contain only alphanumeric, hyphen, or underscore characters"
)
)]
Invalid(String),
}
impl TryFrom<&str> for PackageName {
type Error = PackageNameError;
fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
let bytes = s.as_bytes();
if bytes.is_empty() || bytes.len() > 64 {
return Err(PackageNameError::Invalid(s.to_string()));
}
let first = bytes[0];
let is_alnum =
|b: u8| b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit();
if !is_alnum(first) {
return Err(PackageNameError::Invalid(s.to_string()));
}
let valid = |b: u8| is_alnum(b) || b == b'-' || b == b'_';
for &b in bytes {
if !valid(b) {
return Err(PackageNameError::Invalid(s.to_string()));
}
}
Ok(PackageName(s.to_string()))
}
}
impl TryFrom<String> for PackageName {
type Error = PackageNameError;
fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
Self::try_from(s.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use miette::SourceSpan;
use std::path::Path;
#[test]
fn test_error_configuration() {
let err = Error::configuration("test message");
assert_eq!(err.to_string(), "Configuration error: test message");
if let Error::Configuration { message, .. } = err {
assert_eq!(message, "test message");
} else {
panic!("Expected Configuration error");
}
}
#[test]
fn test_error_configuration_with_source() {
let src = "test source code";
let span = SourceSpan::from(0..4);
let err = Error::configuration_with_source("config error", src, Some(span));
if let Error::Configuration {
src: source,
span: s,
message,
} = err
{
assert_eq!(source, "test source code");
assert_eq!(s, Some(SourceSpan::from(0..4)));
assert_eq!(message, "config error");
} else {
panic!("Expected Configuration error");
}
}
#[test]
fn test_error_ffi() {
let err = Error::ffi("test_function", "FFI failed");
assert_eq!(
err.to_string(),
"FFI operation failed in test_function: FFI failed"
);
if let Error::Ffi {
function,
message,
help,
} = err
{
assert_eq!(function, "test_function");
assert_eq!(message, "FFI failed");
assert!(help.is_none());
} else {
panic!("Expected Ffi error");
}
}
#[test]
fn test_error_ffi_with_help() {
let err = Error::ffi_with_help("test_func", "error msg", "try this instead");
if let Error::Ffi {
function,
message,
help,
} = err
{
assert_eq!(function, "test_func");
assert_eq!(message, "error msg");
assert_eq!(help, Some("try this instead".to_string()));
} else {
panic!("Expected Ffi error");
}
}
#[test]
fn test_error_cue_parse() {
let path = Path::new("/test/path.cue");
let err = Error::cue_parse(path, "parsing failed");
assert_eq!(err.to_string(), "CUE parsing failed: parsing failed");
if let Error::CueParse {
path: p, message, ..
} = err
{
assert_eq!(p.as_ref(), Path::new("/test/path.cue"));
assert_eq!(message, "parsing failed");
} else {
panic!("Expected CueParse error");
}
}
#[test]
fn test_error_cue_parse_with_source() {
let path = Path::new("/test/file.cue");
let src = "package test";
let span = SourceSpan::from(0..7);
let suggestions = vec!["Check syntax".to_string(), "Verify imports".to_string()];
let err = Error::cue_parse_with_source(
path,
"parse error",
src,
Some(span),
Some(suggestions.clone()),
);
if let Error::CueParse {
path: p,
src: source,
span: s,
message,
suggestions: sugg,
} = err
{
assert_eq!(p.as_ref(), Path::new("/test/file.cue"));
assert_eq!(source, Some("package test".to_string()));
assert_eq!(s, Some(SourceSpan::from(0..7)));
assert_eq!(message, "parse error");
assert_eq!(sugg, Some(suggestions));
} else {
panic!("Expected CueParse error");
}
}
#[test]
fn test_error_validation() {
let err = Error::validation("validation failed");
assert_eq!(err.to_string(), "Validation failed: validation failed");
if let Error::Validation {
message, related, ..
} = err
{
assert_eq!(message, "validation failed");
assert!(related.is_empty());
} else {
panic!("Expected Validation error");
}
}
#[test]
fn test_error_validation_with_source() {
let src = "test validation source";
let span = SourceSpan::from(5..15);
let err = Error::validation_with_source("validation error", src, Some(span));
if let Error::Validation {
src: source,
span: s,
message,
..
} = err
{
assert_eq!(source, Some("test validation source".to_string()));
assert_eq!(s, Some(SourceSpan::from(5..15)));
assert_eq!(message, "validation error");
} else {
panic!("Expected Validation error");
}
}
#[test]
fn test_error_timeout() {
let err = Error::Timeout { seconds: 30 };
assert_eq!(err.to_string(), "Operation timed out after 30 seconds");
}
#[test]
fn test_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
if let Error::Io { operation, .. } = err {
assert_eq!(operation, "unknown (unmapped error conversion)");
} else {
panic!("Expected Io error");
}
}
#[test]
fn test_error_from_utf8_error() {
let bytes = vec![0xFF, 0xFE];
let utf8_err = std::str::from_utf8(&bytes).unwrap_err();
let err: Error = utf8_err.into();
assert!(matches!(err, Error::Utf8 { .. }));
}
#[test]
fn test_limits_default() {
let limits = Limits::default();
assert_eq!(limits.max_path_length, 4096);
assert_eq!(limits.max_package_name_length, 256);
assert_eq!(limits.max_output_size, 100 * 1024 * 1024);
}
#[test]
fn test_result_type_alias() {
let ok_result: Result<i32> = Ok(42);
assert!(ok_result.is_ok());
if let Ok(value) = ok_result {
assert_eq!(value, 42);
}
let err_result: Result<i32> = Err(Error::configuration("test"));
assert!(err_result.is_err());
}
#[test]
fn test_error_display() {
let errors = vec![
(Error::configuration("test"), "Configuration error: test"),
(
Error::ffi("func", "msg"),
"FFI operation failed in func: msg",
),
(
Error::cue_parse(Path::new("/test"), "msg"),
"CUE parsing failed: msg",
),
(Error::validation("msg"), "Validation failed: msg"),
(
Error::Timeout { seconds: 10 },
"Operation timed out after 10 seconds",
),
];
for (error, expected) in errors {
assert_eq!(error.to_string(), expected);
}
}
#[test]
fn test_error_diagnostic_codes() {
use miette::Diagnostic;
let config_err = Error::configuration("test");
assert_eq!(
config_err.code().unwrap().to_string(),
"cuenv::config::invalid"
);
let ffi_err = Error::ffi("func", "msg");
assert_eq!(ffi_err.code().unwrap().to_string(), "cuenv::ffi::error");
let cue_err = Error::cue_parse(Path::new("/test"), "msg");
assert_eq!(
cue_err.code().unwrap().to_string(),
"cuenv::cue::parse_error"
);
let validation_err = Error::validation("msg");
assert_eq!(
validation_err.code().unwrap().to_string(),
"cuenv::validation::failed"
);
let timeout_err = Error::Timeout { seconds: 5 };
assert_eq!(timeout_err.code().unwrap().to_string(), "cuenv::timeout");
}
#[test]
fn test_package_dir_validation() {
let result = PackageDir::try_from(Path::new("."));
assert!(result.is_ok(), "Current directory should be valid");
let pkg_dir = result.unwrap();
assert_eq!(pkg_dir.as_path(), Path::new("."));
assert_eq!(pkg_dir.as_ref(), Path::new("."));
assert_eq!(pkg_dir.into_path_buf(), PathBuf::from("."));
let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
assert!(result.is_err());
match result.unwrap_err() {
PackageDirError::NotFound(_) => {} other => panic!("Expected NotFound error, got: {:?}", other),
}
let temp_path = std::env::temp_dir().join("cuenv_test_file");
let file = std::fs::File::create(&temp_path).unwrap();
drop(file);
let result = PackageDir::try_from(temp_path.as_path());
assert!(result.is_err());
match result.unwrap_err() {
PackageDirError::NotADirectory(_) => {} other => panic!("Expected NotADirectory error, got: {:?}", other),
}
std::fs::remove_file(temp_path).ok();
}
#[test]
fn test_package_name_validation() {
let max_len_string = "a".repeat(64);
let valid_names = vec![
"my-package",
"package_123",
"a", "A", "0package", "package-with-hyphens",
"package_with_underscores",
max_len_string.as_str(), ];
for name in valid_names {
let result = PackageName::try_from(name);
assert!(result.is_ok(), "'{}' should be valid", name);
let result = PackageName::try_from(name.to_string());
assert!(result.is_ok(), "'{}' as String should be valid", name);
let pkg_name = result.unwrap();
assert_eq!(pkg_name.as_str(), name);
assert_eq!(pkg_name.as_ref(), name);
assert_eq!(pkg_name.to_string(), name);
assert_eq!(pkg_name.into_string(), name.to_string());
}
let too_long_string = "a".repeat(65);
let invalid_names = vec![
"", "-invalid", "_invalid", "invalid.name", "invalid/name", "invalid:name", too_long_string.as_str(), "invalid@name", "invalid#name", "invalid name", ];
for name in invalid_names {
let result = PackageName::try_from(name);
assert!(result.is_err(), "'{}' should be invalid", name);
assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
}
}
}