1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct Secret(String);
21
22impl Secret {
23 pub fn new(value: impl Into<String>) -> Self {
24 Self(value.into())
25 }
26
27 pub fn expose(&self) -> &str {
29 &self.0
30 }
31
32 pub fn is_empty(&self) -> bool {
33 self.0.is_empty()
34 }
35}
36
37impl fmt::Debug for Secret {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.write_str("Secret([redacted])")
40 }
41}
42
43impl fmt::Display for Secret {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str("[redacted]")
46 }
47}
48
49impl From<String> for Secret {
50 fn from(value: String) -> Self {
51 Self(value)
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn a_secret_never_prints_itself() {
61 let secret = Secret::new("tskey-api-example1CNTRL-secretpart");
62 assert!(!format!("{secret:?}").contains("secretpart"));
63 assert!(!format!("{secret}").contains("secretpart"));
64 assert!(!format!("{:?}", Some(secret.clone())).contains("secretpart"));
65 assert_eq!(secret.expose(), "tskey-api-example1CNTRL-secretpart");
66 }
67
68 #[test]
69 fn a_secret_is_the_plain_string_to_serde() {
70 let json = r#""tskey-api-example1CNTRL-secretpart""#;
71 let secret: Secret = serde_json::from_str(json).expect("a string is a secret");
72 assert_eq!(secret.expose(), "tskey-api-example1CNTRL-secretpart");
73 assert_eq!(serde_json::to_string(&secret).expect("it serialises"), json);
74 }
75}