use std::fmt;
use secrecy::{ExposeSecret, SecretString};
#[derive(Clone)]
pub struct ApiSecret(SecretString);
impl ApiSecret {
pub fn new(key: impl Into<String>) -> Self {
Self(SecretString::from(key.into()))
}
pub fn expose(&self) -> &str {
self.0.expose_secret()
}
}
impl fmt::Debug for ApiSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[REDACTED]")
}
}
impl fmt::Display for ApiSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[REDACTED]")
}
}
impl Default for ApiSecret {
fn default() -> Self {
Self(SecretString::from(String::new()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_and_display_are_redacted() {
let s = ApiSecret::new("abcdefghij.0123456789abcdef");
assert_eq!(format!("{s:?}"), "[REDACTED]");
assert_eq!(format!("{s}"), "[REDACTED]");
}
#[test]
fn expose_returns_plaintext() {
let s = ApiSecret::new("abcdefghij.0123456789abcdef");
assert_eq!(s.expose(), "abcdefghij.0123456789abcdef");
}
#[test]
fn clone_does_not_leak_and_remains_exposable() {
let s = ApiSecret::new("abcdefghij.0123456789abcdef");
let cloned = s.clone();
assert_eq!(format!("{cloned:?}"), "[REDACTED]");
assert_eq!(cloned.expose(), "abcdefghij.0123456789abcdef");
}
}