use core::fmt;
use core::str::FromStr;
pub struct Redacted<T>(T);
impl<T> Redacted<T> {
pub const fn new(secret: T) -> Self {
Self(secret)
}
pub fn expose_secret(&self) -> &T {
&self.0
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> fmt::Debug for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[redacted]")
}
}
impl<T> fmt::Display for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[redacted]")
}
}
impl<T> From<T> for Redacted<T> {
fn from(secret: T) -> Self {
Self(secret)
}
}
impl<T: FromStr> FromStr for Redacted<T> {
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
T::from_str(s).map(Self)
}
}
impl<T: Clone> Clone for Redacted<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: Default> Default for Redacted<T> {
fn default() -> Self {
Self(T::default())
}
}
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Redacted<T> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
T::deserialize(deserializer).map(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_and_display_never_reveal_the_secret() {
let s = Redacted::new("postgres://user:hunter2@db/app".to_string());
assert_eq!(format!("{s:?}"), "[redacted]");
assert_eq!(format!("{s}"), "[redacted]");
assert!(!format!("{s:?} {s}").contains("hunter2"));
}
#[test]
fn the_value_is_reachable_only_through_the_named_accessors() {
let s = Redacted::new(42_u16);
assert_eq!(*s.expose_secret(), 42);
assert_eq!(s.into_inner(), 42);
}
#[test]
fn from_str_wraps_so_it_is_a_drop_in_config_field() {
let port: Redacted<u16> = "8080".parse().unwrap();
assert_eq!(*port.expose_secret(), 8080);
assert!("not-a-number".parse::<Redacted<u16>>().is_err());
}
#[test]
fn a_secret_field_stays_hidden_inside_a_derived_debug() {
#[derive(Debug)]
#[allow(dead_code)] struct Config {
dsn: Redacted<String>,
port: u16,
}
let out = format!(
"{:?}",
Config {
dsn: Redacted::new("top-secret-dsn".to_string()),
port: 8080,
}
);
assert!(out.contains("[redacted]"));
assert!(!out.contains("top-secret-dsn"));
assert!(out.contains("8080"), "non-secret fields still print");
}
}