use keyring::Entry;
use polyoxide_venue::{Class, Classify};
#[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),
}
impl Classify for KeychainError {
fn class(&self) -> Class {
match self {
Self::NotFound { .. } | Self::Backend(_) => Class::InvalidRequest,
}
}
}
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}");
}
#[test]
fn every_variant_classifies() {
let rows = [
KeychainError::NotFound {
service: "svc".into(),
key: "key".into(),
},
KeychainError::Backend(keyring::Error::NoEntry),
];
for err in rows {
assert_eq!(err.class(), Class::InvalidRequest, "{err:?}");
assert!(err.is_fault(), "{err:?}");
assert_eq!(err.retry_after(), None, "{err:?}");
assert!(!err.is_retriable(), "{err:?}");
}
}
}