use std::fmt;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Secret<T> {
inner: T,
}
impl<T> Secret<T> {
pub fn new(value: T) -> Self {
Self { inner: value }
}
pub fn expose(&self) -> &T {
&self.inner
}
pub fn expose_owned(self) -> T {
self.inner
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Secret<U> {
Secret::new(f(self.inner))
}
}
impl<T> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret([REDACTED])")
}
}
impl fmt::Display for Secret<String> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = &self.inner;
if s.len() > 8 {
write!(f, "{}...{}", &s[..4], &s[s.len() - 4..])
} else {
f.write_str("[REDACTED]")
}
}
}
impl serde::Serialize for Secret<String> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.inner)
}
}
impl<'de> serde::Deserialize<'de> for Secret<String> {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Ok(Self::new(String::deserialize(d)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_masks_value() {
let s = Secret::new("sk-ant-12345678".to_string());
assert_eq!(format!("{s:?}"), "Secret([REDACTED])");
}
#[test]
fn display_short_value_redacted() {
let s = Secret::new("short".to_string());
assert_eq!(format!("{s}"), "[REDACTED]");
}
#[test]
fn display_long_value_partial() {
let s = Secret::new("sk-ant-12345678".to_string());
assert_eq!(format!("{s}"), "sk-a...5678");
}
#[test]
fn expose_returns_inner() {
let s = Secret::new("secret-key".to_string());
assert_eq!(s.expose(), "secret-key");
}
#[test]
fn expose_owned_consumes() {
let s = Secret::new("my-key".to_string());
let inner = s.expose_owned();
assert_eq!(inner, "my-key");
}
#[test]
fn map_transforms() {
let s = Secret::new("key".to_string());
let mapped = s.map(|v| v.len());
assert_eq!(*mapped.expose(), 3);
}
#[test]
fn serde_roundtrip() {
let s = Secret::new("sk-test-123".to_string());
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("sk-test-123"));
let back: Secret<String> = serde_json::from_str(&json).unwrap();
assert_eq!(back.expose(), "sk-test-123");
}
}