1use std::fmt;
7
8#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct Secret<T> {
19 inner: T,
20}
21
22impl<T> Secret<T> {
23 pub fn new(value: T) -> Self {
25 Self { inner: value }
26 }
27
28 pub fn expose(&self) -> &T {
30 &self.inner
31 }
32
33 pub fn expose_owned(self) -> T {
35 self.inner
36 }
37
38 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Secret<U> {
40 Secret::new(f(self.inner))
41 }
42}
43
44impl<T> fmt::Debug for Secret<T> {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.write_str("Secret([REDACTED])")
47 }
48}
49
50impl fmt::Display for Secret<String> {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 let s = &self.inner;
53 if s.len() > 8 {
54 write!(f, "{}...{}", &s[..4], &s[s.len() - 4..])
55 } else {
56 f.write_str("[REDACTED]")
57 }
58 }
59}
60
61impl serde::Serialize for Secret<String> {
62 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
63 s.serialize_str("[REDACTED]")
65 }
66}
67
68impl<'de> serde::Deserialize<'de> for Secret<String> {
69 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
70 Ok(Self::new(String::deserialize(d)?))
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn debug_masks_value() {
80 let s = Secret::new("sk-ant-12345678".to_string());
81 assert_eq!(format!("{s:?}"), "Secret([REDACTED])");
82 }
83
84 #[test]
85 fn display_short_value_redacted() {
86 let s = Secret::new("short".to_string());
87 assert_eq!(format!("{s}"), "[REDACTED]");
88 }
89
90 #[test]
91 fn display_long_value_partial() {
92 let s = Secret::new("sk-ant-12345678".to_string());
93 assert_eq!(format!("{s}"), "sk-a...5678");
94 }
95
96 #[test]
97 fn expose_returns_inner() {
98 let s = Secret::new("secret-key".to_string());
99 assert_eq!(s.expose(), "secret-key");
100 }
101
102 #[test]
103 fn expose_owned_consumes() {
104 let s = Secret::new("my-key".to_string());
105 let inner = s.expose_owned();
106 assert_eq!(inner, "my-key");
107 }
108
109 #[test]
110 fn map_transforms() {
111 let s = Secret::new("key".to_string());
112 let mapped = s.map(|v| v.len());
113 assert_eq!(*mapped.expose(), 3);
114 }
115
116 #[test]
117 fn serde_roundtrip() {
118 let s = Secret::new("sk-test-123".to_string());
119 let json = serde_json::to_string(&s).unwrap();
120 assert!(json.contains("[REDACTED]"));
122 assert!(!json.contains("sk-test-123"));
123 let input_json = r#""sk-test-123""#;
125 let back: Secret<String> = serde_json::from_str(input_json).unwrap();
126 assert_eq!(back.expose(), "sk-test-123");
127 }
128}