1use std::fmt;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10#[derive(Clone, Default)]
18pub struct SecretString(String);
19
20impl SecretString {
21 pub fn new(value: impl Into<String>) -> Self {
23 Self(value.into())
24 }
25
26 pub fn expose(&self) -> &str {
29 &self.0
30 }
31
32 pub fn is_empty(&self) -> bool {
34 self.0.is_empty()
35 }
36}
37
38impl fmt::Debug for SecretString {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 if self.0.is_empty() {
41 write!(f, "\"\"")
42 } else {
43 write!(f, "\"[REDACTED]\"")
44 }
45 }
46}
47
48impl fmt::Display for SecretString {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 if self.0.is_empty() {
51 write!(f, "")
52 } else {
53 write!(f, "[REDACTED]")
54 }
55 }
56}
57
58impl Serialize for SecretString {
59 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
60 serializer.serialize_str("")
62 }
63}
64
65impl<'de> Deserialize<'de> for SecretString {
66 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
67 let s = String::deserialize(deserializer)?;
68 Ok(SecretString(s))
69 }
70}
71
72impl From<String> for SecretString {
73 fn from(s: String) -> Self {
74 SecretString(s)
75 }
76}
77
78impl From<&str> for SecretString {
79 fn from(s: &str) -> Self {
80 SecretString(s.to_string())
81 }
82}
83
84impl PartialEq for SecretString {
85 fn eq(&self, other: &Self) -> bool {
86 self.0 == other.0
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn debug_redacts_non_empty() {
96 let s = SecretString::new("my-secret-key");
97 assert_eq!(format!("{:?}", s), "\"[REDACTED]\"");
98 }
99
100 #[test]
101 fn debug_shows_empty_for_empty() {
102 let s = SecretString::default();
103 assert_eq!(format!("{:?}", s), "\"\"");
104 }
105
106 #[test]
107 fn display_redacts_non_empty() {
108 let s = SecretString::new("secret");
109 assert_eq!(format!("{}", s), "[REDACTED]");
110 }
111
112 #[test]
113 fn display_empty_for_empty() {
114 let s = SecretString::default();
115 assert_eq!(format!("{}", s), "");
116 }
117
118 #[test]
119 fn expose_returns_actual_value() {
120 let s = SecretString::new("actual-secret");
121 assert_eq!(s.expose(), "actual-secret");
122 }
123
124 #[test]
125 fn is_empty_works() {
126 assert!(SecretString::default().is_empty());
127 assert!(!SecretString::new("x").is_empty());
128 }
129
130 #[test]
131 fn serialize_emits_empty_string() {
132 let s = SecretString::new("my-api-key");
133 let json = serde_json::to_string(&s).unwrap();
134 assert_eq!(json, "\"\"");
135 assert!(!json.contains("my-api-key"));
136 }
137
138 #[test]
139 fn deserialize_accepts_plain_string() {
140 let s: SecretString = serde_json::from_str("\"my-api-key\"").unwrap();
141 assert_eq!(s.expose(), "my-api-key");
142 }
143
144 #[test]
145 fn from_string() {
146 let s: SecretString = "test".into();
147 assert_eq!(s.expose(), "test");
148 }
149
150 #[test]
151 fn from_owned_string() {
152 let s: SecretString = String::from("test").into();
153 assert_eq!(s.expose(), "test");
154 }
155
156 #[test]
157 fn equality() {
158 let a = SecretString::new("same");
159 let b = SecretString::new("same");
160 let c = SecretString::new("different");
161 assert_eq!(a, b);
162 assert_ne!(a, c);
163 }
164}