Skip to main content

stefans_utils/
secret.rs

1//! Contains the `Secret<T>` struct, designed to wrap values that should never appear formatted with Display or Debug in logs or other output.
2
3use crate::prelude::{AsClone, AsCopy};
4
5/// Wraps a value that should never appear formatted with Display or Debug in logs or other output.
6///
7/// `Secret<T>` does implement `Debug`, but only outputs the inner type name, ie. `"Secret<String>"`;
8///
9/// Forwards the following core traits from its inner type:
10/// - `Clone`
11/// - `Copy`
12/// - `PartialeEq`
13/// - `Eq`
14/// - `PartialOrd`
15/// - `Ord`
16///
17/// ## Feature-flagged trait implementations
18///
19/// | Feature    | Trait         |
20/// | ---------- | ------------- |
21/// | `core`     | `Debug` (1)   |
22/// | `core`     | `Clone` (2)   |
23/// | `core`     | `Copy` (3)    |
24/// | `core`     | `PartialEq`   |
25/// | `core`     | `Eq`          |
26/// | `core`     | `PartialOrd`  |
27/// | `core`     | `Ord`         |
28/// | `core`     | `Hash`        |
29/// | `serde`    | `Serialize`   |
30/// | `serde`    | `Deserialize` |
31/// | `schemars` | `JsonSchema`  |
32/// | `sqlx`     | `Type`        |
33///
34/// All trait implementations besides `Debug` are forwarded from the inner type.
35///
36/// 1. `Debug::fmt` only produces the name of the wrapped type, ie. `"Secret<&str>"`
37/// 2. `T::Clone` enables the `expose_clone(&self) -> T` method of `Secret<T>`
38/// 3. `T::Copy` enables the `expose_copy(&self) -> T` method of `Secret<T>`
39pub struct Secret<T> {
40    value: T,
41    serialize_redacted: bool,
42}
43
44impl<T> Secret<T> {
45    pub fn new(value: T) -> Self {
46        let serialize_redacted = {
47            #[cfg(feature = "secret-serialize-redacted")]
48            {
49                true
50            }
51            #[cfg(not(feature = "secret-serialize-redacted"))]
52            {
53                false
54            }
55        };
56
57        Self {
58            value,
59            serialize_redacted,
60        }
61    }
62
63    pub fn set_serialize_redacted(&mut self, serialize_redacted: bool) {
64        self.serialize_redacted = serialize_redacted;
65    }
66
67    pub fn with_serialize_redacted(self, serialize_redacted: bool) -> Self {
68        Self {
69            value: self.value,
70            serialize_redacted,
71        }
72    }
73
74    pub fn expose(self) -> T {
75        self.value
76    }
77
78    pub fn expose_ref(&self) -> &T {
79        &self.value
80    }
81
82    pub fn expose_as_ref<R: ?Sized>(&self) -> &R
83    where
84        T: AsRef<R>,
85    {
86        self.expose_ref().as_ref()
87    }
88}
89
90impl<T: Clone> Secret<T> {
91    pub fn expose_clone(&self) -> T {
92        self.value.clone()
93    }
94
95    pub fn expose_as_clone<R>(&self) -> R
96    where
97        for<'a> &'a T: AsClone<R>,
98    {
99        (&self.value).as_clone()
100    }
101}
102
103impl<T: Copy> Secret<T> {
104    pub fn expose_copy(&self) -> T {
105        self.value
106    }
107
108    pub fn expose_as_copy<R>(&self) -> R
109    where
110        for<'a> &'a T: AsCopy<R>,
111    {
112        (&self.value).as_copy()
113    }
114}
115
116impl<T> From<T> for Secret<T> {
117    fn from(value: T) -> Self {
118        Self::new(value)
119    }
120}
121
122impl<T: Clone> Clone for Secret<T> {
123    fn clone(&self) -> Self {
124        Self::new(self.value.clone())
125    }
126}
127
128impl<T: Copy> Copy for Secret<T> {}
129
130impl<T: PartialEq> PartialEq for Secret<T> {
131    fn eq(&self, other: &Self) -> bool {
132        self.value.eq(&other.value)
133    }
134}
135
136impl<T: Eq> Eq for Secret<T> {}
137
138impl<T: PartialOrd> PartialOrd for Secret<T> {
139    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
140        self.value.partial_cmp(&other.value)
141    }
142}
143
144impl<T: Ord> Ord for Secret<T> {
145    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
146        self.value.cmp(&other.value)
147    }
148}
149
150impl<T> core::fmt::Debug for Secret<T> {
151    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
152        f.write_fmt(format_args!("Secret<{}>", core::any::type_name::<T>()))
153    }
154}
155
156impl<T: core::hash::Hash> core::hash::Hash for Secret<T> {
157    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
158        self.value.hash(state)
159    }
160}
161
162#[cfg(feature = "serde")]
163mod serde {
164    use super::Secret;
165
166    impl<T: serde::Serialize> serde::Serialize for Secret<T> {
167        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
168        where
169            S: serde::Serializer,
170        {
171            if self.serialize_redacted {
172                format!("Secret<{}>", core::any::type_name::<T>()).serialize(serializer)
173            } else {
174                self.value.serialize(serializer)
175            }
176        }
177    }
178
179    #[cfg(feature = "serde")]
180    impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Secret<T> {
181        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182        where
183            D: serde::Deserializer<'de>,
184        {
185            T::deserialize(deserializer).map(Secret::new)
186        }
187    }
188}
189
190#[cfg(feature = "schemars")]
191mod schemars {
192    use super::Secret;
193    extern crate alloc;
194
195    impl<T: schemars::JsonSchema> schemars::JsonSchema for Secret<T> {
196        fn schema_id() -> alloc::borrow::Cow<'static, str> {
197            T::schema_id()
198        }
199
200        fn schema_name() -> alloc::borrow::Cow<'static, str> {
201            T::schema_name()
202        }
203
204        fn inline_schema() -> bool {
205            T::inline_schema()
206        }
207
208        fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
209            #[cfg(feature = "secret-serialize-redacted")]
210            {
211                schemars::json_schema!({
212                    "anyOf": [
213                        {
214                            "const": format!("Secret<{}>", core::any::type_name::<T>())
215                        },
216                        T::json_schema(generator)
217                    ]
218                })
219            }
220            #[cfg(not(feature = "secret-serialize-redacted"))]
221            {
222                T::json_schema(generator)
223            }
224        }
225    }
226}
227
228#[cfg(feature = "sqlx")]
229mod sqlx_impls {
230    impl<DB: sqlx::Database, T: sqlx::Type<DB>> sqlx::Type<DB> for super::Secret<T> {
231        fn type_info() -> DB::TypeInfo {
232            T::type_info()
233        }
234
235        fn compatible(ty: &DB::TypeInfo) -> bool {
236            T::compatible(ty)
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243
244    use super::*;
245
246    #[test]
247    fn debug_should_output_type_only() {
248        let secret = Secret::new("Hello, world!");
249
250        assert_eq!(format!("{secret:#?}"), "Secret<&str>");
251    }
252
253    #[test]
254    fn serialize_redacted_should_output_type_only() {
255        let secret = Secret::new("Hello, world!").with_serialize_redacted(true);
256
257        assert_eq!(serde_json::to_string(&secret).unwrap(), "\"Secret<&str>\"");
258    }
259
260    #[test]
261    fn serialize_unredacted_should_output_full_value() {
262        let secret = Secret::new("Hello, world!").with_serialize_redacted(false);
263
264        assert_eq!(serde_json::to_string(&secret).unwrap(), "\"Hello, world!\"");
265    }
266
267    #[test]
268    fn should_be_able_to_expose_as_ref() {
269        #[derive(Debug, PartialEq)]
270        struct Inner;
271        struct Outer(Inner);
272
273        impl AsRef<Inner> for Outer {
274            fn as_ref(&self) -> &Inner {
275                &self.0
276            }
277        }
278
279        assert_eq!(&Inner, Secret::new(Outer(Inner)).expose_as_ref());
280    }
281
282    #[test]
283    fn should_be_able_to_expose_as_clone() {
284        #[derive(Clone, Debug, PartialEq)]
285        struct Inner;
286        #[derive(Clone)]
287        struct Outer(Inner);
288
289        impl AsClone<Inner> for &Outer {
290            fn as_clone(self) -> Inner {
291                self.0.clone()
292            }
293        }
294
295        let secret = Secret::new(Outer(Inner));
296
297        assert_eq!(Inner, secret.expose_as_clone());
298    }
299
300    #[test]
301    fn should_be_able_to_expose_as_copy() {
302        #[derive(Clone, Copy, Debug, PartialEq)]
303        struct Inner;
304        #[derive(Clone, Copy)]
305        struct Outer(Inner);
306
307        impl AsCopy<Inner> for &Outer {
308            fn as_copy(self) -> Inner {
309                self.0
310            }
311        }
312
313        let secret = Secret::new(Outer(Inner));
314
315        assert_eq!(Inner, secret.expose_as_copy());
316    }
317}