1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Deserializer};
3
4#[derive(Clone)]
5pub struct SecretValue {
6 value: SecretString,
7}
8
9impl SecretValue {
10 pub fn new(value: String) -> Self {
11 Self {
12 value: SecretString::from(value),
13 }
14 }
15
16 pub fn from_utf8_lossy_trimmed(bytes: Vec<u8>) -> Self {
17 let value = match String::from_utf8(bytes) {
18 Ok(value) => value,
19 Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
20 };
21 Self::new_trimmed(value)
22 }
23
24 pub fn expose(&self) -> &str {
25 self.value.expose_secret()
26 }
27
28 fn new_trimmed(mut value: String) -> Self {
29 while value.ends_with(['\r', '\n']) {
30 value.pop();
31 }
32 Self::new(value)
33 }
34}
35
36impl<'de> Deserialize<'de> for SecretValue {
37 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38 where
39 D: Deserializer<'de>,
40 {
41 String::deserialize(deserializer).map(Self::new)
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn exposes_secret_value_when_explicitly_requested() {
51 let secret = SecretValue::new("secret-token".to_owned());
52
53 assert_eq!(secret.expose(), "secret-token");
54 }
55
56 #[test]
57 fn builds_secret_value_from_trimmed_utf8_output() {
58 let secret = SecretValue::from_utf8_lossy_trimmed(b"secret-token\r\n".to_vec());
59
60 assert_eq!(secret.expose(), "secret-token");
61 }
62}