1use std::fmt;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
8
9#[derive(Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct GotifyConfig {
17 pub url: String,
19 pub client_token: String,
24 pub app_token: String,
29 #[serde(with = "duration_secs")]
32 pub request_timeout: Duration,
33}
34
35impl Default for GotifyConfig {
36 fn default() -> Self {
37 Self {
38 url: String::new(),
39 client_token: String::new(),
40 app_token: String::new(),
41 request_timeout: DEFAULT_REQUEST_TIMEOUT,
42 }
43 }
44}
45
46impl fmt::Debug for GotifyConfig {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.debug_struct("GotifyConfig")
49 .field("url", &self.url)
50 .field(
51 "client_token",
52 &format_args!("<redacted, {} bytes>", self.client_token.len()),
53 )
54 .field(
55 "app_token",
56 &format_args!("<redacted, {} bytes>", self.app_token.len()),
57 )
58 .field("request_timeout", &self.request_timeout)
59 .finish()
60 }
61}
62
63mod duration_secs {
68 use std::time::Duration;
69
70 use serde::{Deserialize, Deserializer, Serializer};
71
72 pub fn serialize<S: Serializer>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
73 serializer.serialize_u64(value.as_secs())
74 }
75
76 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
77 Ok(Duration::from_secs(u64::deserialize(deserializer)?))
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn debug_redacts_both_tokens() {
87 let cfg = GotifyConfig {
88 client_token: "super-secret-client".to_string(),
89 app_token: "super-secret-app".to_string(),
90 ..GotifyConfig::default()
91 };
92
93 let debug = format!("{cfg:?}");
94
95 assert!(!debug.contains("super-secret-client"));
96 assert!(!debug.contains("super-secret-app"));
97 assert!(debug.contains("redacted"));
98 }
99
100 #[test]
101 fn request_timeout_serializes_as_whole_seconds() {
102 let cfg = GotifyConfig {
103 request_timeout: Duration::from_secs(90),
104 ..GotifyConfig::default()
105 };
106
107 let json = serde_json::to_value(&cfg).unwrap();
108
109 assert_eq!(json["request_timeout"], serde_json::json!(90));
110 }
111
112 #[test]
113 fn request_timeout_round_trips_through_json() {
114 let cfg = GotifyConfig {
115 request_timeout: Duration::from_secs(90),
116 ..GotifyConfig::default()
117 };
118
119 let json = serde_json::to_string(&cfg).unwrap();
120 let restored: GotifyConfig = serde_json::from_str(&json).unwrap();
121
122 assert_eq!(restored.request_timeout, Duration::from_secs(90));
123 }
124}