use keyring::Entry;
#[derive(Debug, thiserror::Error)]
pub enum KeychainError {
#[error("Keychain entry not found: service={service}, key={key}")]
NotFound {
service: String,
key: String,
},
#[error("Keychain error: {0}")]
Backend(#[from] keyring::Error),
}
pub fn get(service: &str, key: &str) -> Result<String, KeychainError> {
let entry = Entry::new(service, key)?;
entry.get_password().map_err(|e| match e {
keyring::Error::NoEntry => KeychainError::NotFound {
service: service.to_string(),
key: key.to_string(),
},
other => KeychainError::Backend(other),
})
}
pub fn set(service: &str, key: &str, value: &str) -> Result<(), KeychainError> {
let entry = Entry::new(service, key)?;
entry.set_password(value)?;
Ok(())
}
pub fn delete(service: &str, key: &str) -> Result<(), KeychainError> {
let entry = Entry::new(service, key)?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(KeychainError::Backend(e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_found_error_includes_service_and_key() {
let err = KeychainError::NotFound {
service: "test-svc".to_string(),
key: "test-key".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("test-svc"), "missing service: {msg}");
assert!(msg.contains("test-key"), "missing key: {msg}");
}
#[test]
fn backend_error_display() {
let err = KeychainError::Backend(keyring::Error::NoEntry);
let msg = err.to_string();
assert!(msg.contains("Keychain error"), "unexpected: {msg}");
}
}