Skip to main content

gotify/
config.rs

1use std::fmt;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6/// Default [`GotifyConfig::request_timeout`] when not otherwise specified.
7pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
8
9/// Gotify server connection config.
10///
11/// `Debug` redacts [`client_token`](Self::client_token) and
12/// [`app_token`](Self::app_token) so this can't leak into logs or traces
13/// through an incidental `{:?}` — only each token's length is shown.
14#[derive(Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct GotifyConfig {
17    /// Server base URL, e.g. `https://gotify.example.com` (`GOTIFY_URL`).
18    pub url: String,
19    /// Client token for management operations: messages, applications,
20    /// clients, current user (`GOTIFY_CLIENT_TOKEN`). Create one under
21    /// **Clients** in the Gotify web UI. Empty means unconfigured — calls
22    /// that need it return [`crate::GotifyError::MissingClientToken`].
23    pub client_token: String,
24    /// App token for sending messages (`GOTIFY_APP_TOKEN`). Distinct from
25    /// `client_token` — create one under **Applications** in the Gotify web
26    /// UI. Empty means unconfigured — [`crate::GotifyClient::send_message`]
27    /// returns [`crate::GotifyError::MissingAppToken`].
28    pub app_token: String,
29    /// Per-request timeout, applied to the pooled `reqwest::Client` at
30    /// construction. Defaults to [`DEFAULT_REQUEST_TIMEOUT`].
31    #[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
63/// (De)serializes a [`Duration`] as whole seconds. `std::time::Duration` has
64/// no built-in `serde` support, and pulling in `serde_with`/`humantime-serde`
65/// for one field would be a heavier dependency than this crate's single
66/// duration field justifies.
67mod 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}