1use core::fmt;
4
5pub struct Sensitive<T> {
33 value: T,
34}
35
36impl<T> Sensitive<T> {
37 #[must_use]
39 pub fn new(value: T) -> Self {
40 Self { value }
41 }
42
43 #[must_use]
45 pub fn expose(&self) -> &T {
46 &self.value
47 }
48
49 #[must_use]
52 pub fn into_inner(self) -> T {
53 self.value
54 }
55}
56
57impl<T> From<T> for Sensitive<T> {
58 fn from(value: T) -> Self {
59 Self::new(value)
60 }
61}
62
63impl<T> fmt::Debug for Sensitive<T> {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str("[REDACTED]")
66 }
67}
68
69impl<T> fmt::Display for Sensitive<T> {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str("[REDACTED]")
72 }
73}
74
75impl<'de, T> serde::Deserialize<'de> for Sensitive<T>
76where
77 T: serde::Deserialize<'de>,
78{
79 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80 where
81 D: serde::Deserializer<'de>,
82 {
83 T::deserialize(deserializer).map(Sensitive::new)
84 }
85}
86
87#[cfg(feature = "serde")]
89impl<T> serde::Serialize for Sensitive<T>
90where
91 T: serde::Serialize,
92{
93 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94 where
95 S: serde::Serializer,
96 {
97 self.value.serialize(serializer)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 #![allow(clippy::unwrap_used, clippy::expect_used)]
104 use super::*;
105
106 #[test]
107 fn debug_and_display_redact() {
108 let secret = Sensitive::new("hunter2".to_owned());
109 assert_eq!(format!("{secret:?}"), "[REDACTED]");
110 assert_eq!(format!("{secret}"), "[REDACTED]");
111 assert!(!format!("{secret:?}").contains("hunter2"));
112 }
113
114 #[test]
115 fn access_is_explicit() {
116 let secret = Sensitive::from(42_u32);
117 assert_eq!(*secret.expose(), 42);
118 assert_eq!(secret.into_inner(), 42);
119 }
120
121 #[test]
122 fn deserializes_and_redacts_through_containers() {
123 #[derive(serde::Deserialize, Debug)]
124 struct Container {
125 token: Sensitive<String>,
126 }
127
128 let container: Container =
129 toml::from_str("token = \"top-secret\"").expect("deserialize container");
130 assert_eq!(container.token.expose(), "top-secret");
131 let rendered = format!("{container:?}");
132 assert!(rendered.contains("[REDACTED]"), "{rendered}");
133 assert!(!rendered.contains("top-secret"), "{rendered}");
134 }
135
136 #[cfg(feature = "serde")]
137 #[test]
138 fn serde_roundtrip_preserves_value() {
139 #[derive(serde::Serialize, serde::Deserialize)]
140 struct Wrapper {
141 token: Sensitive<String>,
142 }
143
144 let original = Wrapper {
145 token: Sensitive::new("rotate-me".to_owned()),
146 };
147 let encoded = toml::to_string(&original).expect("serialize");
148 let decoded: Wrapper = toml::from_str(&encoded).expect("deserialize");
149 assert_eq!(decoded.token.expose(), "rotate-me");
150 assert_eq!(format!("{:?}", decoded.token), "[REDACTED]");
151 }
152}