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