1use serde::{Deserialize, Serialize};
5use std::fmt;
6
7const REDACTED: &str = "<redacted>";
9
10#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct SecretString(String);
24
25impl SecretString {
26 pub fn new(value: impl Into<String>) -> Self {
28 Self(value.into())
29 }
30
31 pub fn is_blank(&self) -> bool {
33 self.0.trim().is_empty()
34 }
35
36 pub fn expose(&self) -> &str {
38 &self.0
39 }
40
41 pub fn masked(&self) -> Self {
47 Self(REDACTED.to_owned())
48 }
49}
50
51impl fmt::Debug for SecretString {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(REDACTED)
54 }
55}
56
57impl fmt::Display for SecretString {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str(REDACTED)
60 }
61}
62
63impl From<String> for SecretString {
64 fn from(value: String) -> Self {
65 Self(value)
66 }
67}
68
69impl From<&str> for SecretString {
70 fn from(value: &str) -> Self {
71 Self(value.to_owned())
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::SecretString;
78
79 #[test]
80 fn debug_and_display_redact_the_value() {
81 let secret = SecretString::new("super-secret-value");
82
83 assert_eq!(format!("{secret:?}"), "<redacted>");
84 assert_eq!(format!("{secret}"), "<redacted>");
85 assert_eq!(secret.expose(), "super-secret-value");
86 }
87
88 #[test]
89 fn blank_detection_ignores_surrounding_whitespace() {
90 assert!(SecretString::new(" \t\n").is_blank());
91 assert!(!SecretString::new(" secret ").is_blank());
92 }
93
94 #[test]
95 fn serde_round_trips_the_raw_value() {
96 let secret = SecretString::new("super-secret-value");
97
98 let encoded = serde_json::to_string(&secret).expect("serialize secret");
99 assert_eq!(encoded, "\"super-secret-value\"");
100
101 let decoded: SecretString = serde_json::from_str(&encoded).expect("deserialize secret");
102 assert_eq!(decoded, secret);
103 }
104}