Replace map_err string wrapping with typed error variants
Added From impls for serde_yaml_ng::Error and FromUtf8Error so ?
handles the conversion. Replaced map_err(Error::Io) with bare ? since
Io already has #[from]. Added comments on SecretResolve explaining
why it still uses a string (OnceLock + non-Clone io::Error) and
noting once_cell_try as the fix path.
Assisted-by: GLM-5.1 via pi
diff --git a/src/ci.rs b/src/ci.rs
index ac95be7..709e36e 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -653,20 +653,19 @@ async fn dispatch_mirror(quire: &crate::Quire, repo: crate::quire::Repo, event:
}
}
+/// Write a serializable value to a YAML file atomically (temp file + rename).
fn write_yaml<T: serde::Serialize>(path: &Path, value: &T) -> Result<()> {
let tmp_path = path.with_extension("yml.tmp");
let f = fs_err::File::create(&tmp_path)?;
- serde_yaml_ng::to_writer(std::io::BufWriter::new(f), value)
- .map_err(|e| crate::Error::Io(std::io::Error::other(format!("yaml write error: {e}"))))?;
+ serde_yaml_ng::to_writer(std::io::BufWriter::new(f), value)?;
fs_err::rename(&tmp_path, path)?;
Ok(())
}
-/// Read a value from a YAML file.
+/// Read a deserializable value from a YAML file.
fn read_yaml<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
let f = fs_err::File::open(path)?;
- serde_yaml_ng::from_reader(std::io::BufReader::new(f))
- .map_err(|e| crate::Error::Io(std::io::Error::other(format!("yaml read error: {e}"))))
+ Ok(serde_yaml_ng::from_reader(std::io::BufReader::new(f))?)
}
#[cfg(test)]
diff --git a/src/error.rs b/src/error.rs
index 70fe5bc..7956c64 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -10,6 +10,8 @@ pub enum Error {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
+ // Stored as a string because `OnceLock` in `SecretString::reveal` caches
+ // the error and `std::io::Error` is not `Clone`. See `secret.rs` for details.
#[error("secret resolution failed: {0}")]
SecretResolve(String),
@@ -29,6 +31,12 @@ pub enum Error {
#[error("git error: {0}")]
Git(String),
+ #[error(transparent)]
+ Yaml(#[from] serde_yaml_ng::Error),
+
+ #[error(transparent)]
+ Utf8(#[from] std::string::FromUtf8Error),
+
#[allow(dead_code)]
#[error("event socket error: {0}")]
EventSocket(String),
diff --git a/src/quire.rs b/src/quire.rs
index d98b454..e553e80 100644
--- a/src/quire.rs
+++ b/src/quire.rs
@@ -170,8 +170,7 @@ impl Repo {
.git(&["show", &format!("{sha}:.quire/ci.fnl")])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
- .output()
- .map_err(|e| crate::Error::Git(format!("failed to read ci.fnl: {e}")))?;
+ .output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -180,8 +179,7 @@ impl Repo {
)));
}
- String::from_utf8(output.stdout)
- .map_err(|e| crate::Error::Git(format!("ci.fnl is not valid UTF-8: {e}")))
+ Ok(String::from_utf8(output.stdout)?)
}
/// Push `main` to the configured mirror, injecting the GitHub token via
@@ -208,8 +206,7 @@ impl Repo {
.env("GIT_CONFIG_VALUE_0", github_auth_header(token))
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
- .status()
- .map_err(crate::Error::Io)?;
+ .status()?;
if !status.success() {
return Err(crate::Error::Git(format!("push to {} failed", mirror.url)));
@@ -233,30 +230,21 @@ impl Repo {
.git(&["rev-parse", "--verify", "HEAD"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
- .status()
- .map_err(crate::Error::Io)?
+ .status()?
.success();
if !has_head {
return Ok(RepoConfig::default());
}
- let output = self
- .git(&["show", "HEAD:.quire/config.fnl"])
- .output()
- .map_err(crate::Error::Io)?;
+ let output = self.git(&["show", "HEAD:.quire/config.fnl"]).output()?;
if !output.status.success() {
// HEAD exists but the file doesn't — not an error.
return Ok(RepoConfig::default());
}
- let source = String::from_utf8(output.stdout).map_err(|e| {
- crate::Error::Io(std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- format!("config is not valid UTF-8: {e}"),
- ))
- })?;
+ let source = String::from_utf8(output.stdout)?;
let fennel = Fennel::new()?;
Ok(fennel.load_string(&source, "HEAD:.quire/config.fnl")?)
diff --git a/src/secret.rs b/src/secret.rs
index e439673..3ac5a1a 100644
--- a/src/secret.rs
+++ b/src/secret.rs
@@ -40,8 +40,13 @@ impl SecretString {
/// The resolved secret value.
///
/// For the file variant, reads from disk on first call and caches the
- /// result. Returns a typed error if the file is missing or unreadable.
- /// Errors are also cached — subsequent calls return the same error.
+ /// result. Errors are also cached — subsequent calls return the same error.
+ ///
+ /// The error is stored as a formatted string inside `OnceLock` because
+ /// `std::io::Error` is not `Clone`, and `OnceLock::get_or_init` requires
+ /// the closure output to be `Sized` + ownable. Once `once_cell_try`
+ /// stabilizes (allowing `OnceLock::get_or_try_init` with a separate error
+ /// type), we can store a structured error instead of a string.
pub fn reveal(&self) -> crate::Result<&str> {
match &self.0 {
SecretSource::Plain(s) => Ok(s.as_str()),