1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
use miette::Diagnostic;
use crate::ci::Error as CiError;
use quire_core::fennel::FennelError;
use quire_core::secret;
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum Error {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Repo(#[from] RepoNameError),
#[error("repository not found: {0}")]
RepoNotFound(String),
#[error(transparent)]
#[diagnostic(transparent)]
Fennel(#[from] Box<FennelError>),
#[error(transparent)]
#[diagnostic(transparent)]
Ci(#[from] CiError),
#[error(transparent)]
Secret(#[from] secret::Error),
#[error(transparent)]
Sql(#[from] rusqlite::Error),
#[error(transparent)]
Yaml(#[from] serde_yaml_ng::Error),
#[error(transparent)]
Utf8(#[from] std::string::FromUtf8Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum RepoNameError {
#[error("repository name cannot be empty")]
Empty,
#[error("repository name must end in .git: {0}")]
MissingGitSuffix(String),
#[error("repository name allows at most one level of grouping: {0}")]
TooManySegments(String),
#[error("path is not under repos directory: {0}")]
PathOutsideBase(String),
#[error("invalid repository name: {0}")]
Invalid(String),
}
impl From<FennelError> for Error {
fn from(err: FennelError) -> Self {
Error::Fennel(Box::new(err))
}
}
#[cfg(test)]
mod tests {
use super::*;
use quire_core::fennel::FennelError;
#[test]
fn from_fennel_error() {
let fennel_err = FennelError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"test.fnl",
));
let err: Error = fennel_err.into();
assert!(err.to_string().contains("test.fnl"));
}
#[test]
fn fennel_eval_display_is_self_contained() {
// FennelError::Eval's Display should carry both the source
// name and the underlying lua error text — without needing to
// walk the `#[source]` chain — so plain `%err` is enough for
// tracing/Sentry. The chain is still preserved structurally.
let f = quire_core::fennel::Fennel::new().expect("Fennel::new");
let result: std::result::Result<i32, _> =
f.load_string("(this is not valid", "bad.fnl", |_| {});
let fennel_err = result.unwrap_err();
let plain = fennel_err.to_string();
assert!(
plain.starts_with("bad.fnl: "),
"Display should start with the source name: {plain:?}"
);
assert!(
plain.len() > "bad.fnl: ".len(),
"Display should include the underlying error detail: {plain:?}"
);
// The `#[source]` chain is still walkable.
let err: &dyn std::error::Error = &fennel_err;
assert!(err.source().is_some(), "source chain should be preserved");
}
#[test]
fn from_pipeline_error() {
let source = "(ci.job :a [] (fn [_] nil))";
let pipeline_err = crate::ci::PipelineError {
src: miette::NamedSource::new("ci.fnl", source.to_string()),
diagnostics: vec![crate::ci::Diagnostic::Definition(
crate::ci::DefinitionError::EmptyInputs {
job_id: "a".to_string(),
span: miette::SourceSpan::from((0, 0)),
},
)],
};
let ci_err = crate::ci::Error::from(pipeline_err);
let err: Error = ci_err.into();
assert!(err.to_string().contains("ci.fnl has errors"));
}
}