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.value.as_ref()
87    }
88}
89
90impl<T: AsRef<str>> Secret<T> {
91    pub fn expose_as_str(&self) -> &str {
92        self.value.as_ref()
93    }
94}
95
96impl<T: ToString> Secret<T> {
97    pub fn expose_to_string(&self) -> String {
98        self.value.to_string()
99    }
100}
101
102impl<T: Clone> Secret<T> {
103    pub fn expose_clone(&self) -> T {
104        self.value.clone()
105    }
106
107    pub fn expose_as_clone<R>(&self) -> R
108    where
109        for<'a> &'a T: AsClone<R>,
110    {
111        (&self.value).as_clone()
112    }
113}
114
115impl<T: Copy> Secret<T> {
116    pub fn expose_copy(&self) -> T {
117        self.value
118    }
119
120    pub fn expose_as_copy<R>(&self) -> R
121    where
122        for<'a> &'a T: AsCopy<R>,
123    {
124        (&self.value).as_copy()
125    }
126}
127
128impl<T> From<T> for Secret<T> {
129    fn from(value: T) -> Self {
130        Self::new(value)
131    }
132}
133
134impl<T: Clone> Clone for Secret<T> {
135    fn clone(&self) -> Self {
136        Self::new(self.value.clone())
137    }
138}
139
140impl<T: Copy> Copy for Secret<T> {}
141
142impl<T: PartialEq> PartialEq for Secret<T> {
143    fn eq(&self, other: &Self) -> bool {
144        self.value.eq(&other.value)
145    }
146}
147
148impl<T: Eq> Eq for Secret<T> {}
149
150impl<T: PartialOrd> PartialOrd for Secret<T> {
151    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
152        self.value.partial_cmp(&other.value)
153    }
154}
155
156impl<T: Ord> Ord for Secret<T> {
157    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
158        self.value.cmp(&other.value)
159    }
160}
161
162impl<T> core::fmt::Debug for Secret<T> {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        f.write_fmt(format_args!("Secret<{}>", core::any::type_name::<T>()))
165    }
166}
167
168impl<T: core::hash::Hash> core::hash::Hash for Secret<T> {
169    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
170        self.value.hash(state)
171    }
172}
173
174#[cfg(feature = "serde")]
175mod serde {
176    use super::Secret;
177
178    impl<T: serde::Serialize> serde::Serialize for Secret<T> {
179        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
180        where
181            S: serde::Serializer,
182        {
183            if self.serialize_redacted {
184                format!("Secret<{}>", core::any::type_name::<T>()).serialize(serializer)
185            } else {
186                self.value.serialize(serializer)
187            }
188        }
189    }
190
191    #[cfg(feature = "serde")]
192    impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Secret<T> {
193        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
194        where
195            D: serde::Deserializer<'de>,
196        {
197            T::deserialize(deserializer).map(Secret::new)
198        }
199    }
200}
201
202#[cfg(feature = "schemars")]
203mod schemars {
204    use super::Secret;
205    extern crate alloc;
206
207    impl<T: schemars::JsonSchema> schemars::JsonSchema for Secret<T> {
208        fn schema_id() -> alloc::borrow::Cow<'static, str> {
209            T::schema_id()
210        }
211
212        fn schema_name() -> alloc::borrow::Cow<'static, str> {
213            T::schema_name()
214        }
215
216        fn inline_schema() -> bool {
217            T::inline_schema()
218        }
219
220        fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
221            #[cfg(feature = "secret-serialize-redacted")]
222            {
223                schemars::json_schema!({
224                    "anyOf": [
225                        {
226                            "const": format!("Secret<{}>", core::any::type_name::<T>())
227                        },
228                        T::json_schema(generator)
229                    ]
230                })
231            }
232            #[cfg(not(feature = "secret-serialize-redacted"))]
233            {
234                T::json_schema(generator)
235            }
236        }
237    }
238}
239
240#[cfg(feature = "sqlx")]
241mod sqlx_impls {
242    impl<DB: sqlx::Database, T: sqlx::Type<DB>> sqlx::Type<DB> for super::Secret<T> {
243        fn type_info() -> DB::TypeInfo {
244            T::type_info()
245        }
246
247        fn compatible(ty: &DB::TypeInfo) -> bool {
248            T::compatible(ty)
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255
256    use super::*;
257
258    #[test]
259    fn debug_should_output_type_only() {
260        let secret = Secret::new("Hello, world!");
261
262        assert_eq!(format!("{secret:#?}"), "Secret<&str>");
263    }
264
265    #[test]
266    fn serialize_redacted_should_output_type_only() {
267        let secret = Secret::new("Hello, world!").with_serialize_redacted(true);
268
269        assert_eq!(serde_json::to_string(&secret).unwrap(), "\"Secret<&str>\"");
270    }
271
272    #[test]
273    fn serialize_unredacted_should_output_full_value() {
274        let secret = Secret::new("Hello, world!").with_serialize_redacted(false);
275
276        assert_eq!(serde_json::to_string(&secret).unwrap(), "\"Hello, world!\"");
277    }
278
279    #[test]
280    fn should_be_able_to_expose_as_ref() {
281        #[derive(Debug, PartialEq)]
282        struct Inner;
283        struct Outer(Inner);
284
285        impl AsRef<Inner> for Outer {
286            fn as_ref(&self) -> &Inner {
287                &self.0
288            }
289        }
290
291        assert_eq!(&Inner, Secret::new(Outer(Inner)).expose_as_ref());
292    }
293
294    #[test]
295    fn should_be_able_to_expose_as_clone() {
296        #[derive(Clone, Debug, PartialEq)]
297        struct Inner;
298        #[derive(Clone)]
299        struct Outer(Inner);
300
301        impl AsClone<Inner> for &Outer {
302            fn as_clone(self) -> Inner {
303                self.0.clone()
304            }
305        }
306
307        let secret = Secret::new(Outer(Inner));
308
309        assert_eq!(Inner, secret.expose_as_clone());
310    }
311
312    #[test]
313    fn should_be_able_to_expose_as_copy() {
314        #[derive(Clone, Copy, Debug, PartialEq)]
315        struct Inner;
316        #[derive(Clone, Copy)]
317        struct Outer(Inner);
318
319        impl AsCopy<Inner> for &Outer {
320            fn as_copy(self) -> Inner {
321                self.0
322            }
323        }
324
325        let secret = Secret::new(Outer(Inner));
326
327        assert_eq!(Inner, secret.expose_as_copy());
328    }
329
330    #[test]
331    fn should_be_able_to_expose_to_string_for_anything_implementing_to_string() {
332        struct X;
333
334        impl std::fmt::Display for X {
335            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336                f.write_str("check")
337            }
338        }
339
340        assert_eq!(Secret::new(X).expose_to_string(), "check");
341    }
342
343    #[test]
344    fn should_be_able_to_expose_as_str_for_anything_implementing_as_ref_str() {
345        assert_eq!(Secret::new(String::from("check")).expose_as_str(), "check")
346    }
347}