Skip to main content

atproto_tap/
config.rs

1//! Configuration for TAP stream connections.
2//!
3//! This module provides the [`TapConfig`] struct for configuring TAP stream
4//! connections, including hostname, authentication, and reconnection behavior.
5
6use std::time::Duration;
7
8/// Configuration for a TAP stream connection.
9///
10/// Use [`TapConfig::builder()`] for ergonomic construction with defaults.
11///
12/// # Example
13///
14/// ```
15/// use atproto_tap::TapConfig;
16/// use std::time::Duration;
17///
18/// let config = TapConfig::builder()
19///     .hostname("localhost:2480")
20///     .admin_password("secret")
21///     .send_acks(true)
22///     .max_reconnect_attempts(Some(10))
23///     .build();
24/// ```
25#[derive(Debug, Clone)]
26pub struct TapConfig {
27    /// TAP service hostname (e.g., "localhost:2480").
28    ///
29    /// The WebSocket URL is constructed as `ws://{hostname}/channel`.
30    pub hostname: String,
31
32    /// Optional admin password for authentication.
33    ///
34    /// If set, HTTP Basic Auth is used with username "admin".
35    pub admin_password: Option<String>,
36
37    /// Whether to send acknowledgments for received messages.
38    ///
39    /// Default: `true`. Set to `false` if the TAP service has acks disabled.
40    pub send_acks: bool,
41
42    /// User-Agent header value for WebSocket connections.
43    pub user_agent: String,
44
45    /// Maximum reconnection attempts before giving up.
46    ///
47    /// `None` means unlimited reconnection attempts (default).
48    pub max_reconnect_attempts: Option<u32>,
49
50    /// Initial delay before first reconnection attempt.
51    ///
52    /// Default: 1 second.
53    pub initial_reconnect_delay: Duration,
54
55    /// Maximum delay between reconnection attempts.
56    ///
57    /// Default: 60 seconds.
58    pub max_reconnect_delay: Duration,
59
60    /// Multiplier for exponential backoff between reconnections.
61    ///
62    /// Default: 2.0 (doubles the delay each attempt).
63    pub reconnect_backoff_multiplier: f64,
64
65    /// Size of the internal channel buffer between the WebSocket reader and consumer.
66    ///
67    /// A larger buffer absorbs more burst latency when the consumer has occasional
68    /// slow processing. Default: 32.
69    pub channel_buffer_size: usize,
70}
71
72impl Default for TapConfig {
73    fn default() -> Self {
74        Self {
75            hostname: "localhost:2480".to_string(),
76            admin_password: None,
77            send_acks: true,
78            user_agent: format!("atproto-tap/{}", env!("CARGO_PKG_VERSION")),
79            max_reconnect_attempts: None,
80            initial_reconnect_delay: Duration::from_secs(1),
81            max_reconnect_delay: Duration::from_secs(60),
82            reconnect_backoff_multiplier: 2.0,
83            channel_buffer_size: 32,
84        }
85    }
86}
87
88impl TapConfig {
89    /// Create a new configuration builder with defaults.
90    pub fn builder() -> TapConfigBuilder {
91        TapConfigBuilder::default()
92    }
93
94    /// Create a minimal configuration for the given hostname.
95    pub fn new(hostname: impl Into<String>) -> Self {
96        Self {
97            hostname: hostname.into(),
98            ..Default::default()
99        }
100    }
101
102    /// Returns the WebSocket URL for the TAP channel.
103    pub fn ws_url(&self) -> String {
104        format!("ws://{}/channel", self.hostname)
105    }
106
107    /// Returns the HTTP base URL for the TAP management API.
108    pub fn http_base_url(&self) -> String {
109        format!("http://{}", self.hostname)
110    }
111}
112
113/// Builder for [`TapConfig`].
114#[derive(Debug, Clone, Default)]
115pub struct TapConfigBuilder {
116    config: TapConfig,
117}
118
119impl TapConfigBuilder {
120    /// Set the TAP service hostname.
121    pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
122        self.config.hostname = hostname.into();
123        self
124    }
125
126    /// Set the admin password for authentication.
127    pub fn admin_password(mut self, password: impl Into<String>) -> Self {
128        self.config.admin_password = Some(password.into());
129        self
130    }
131
132    /// Set whether to send acknowledgments.
133    pub fn send_acks(mut self, send_acks: bool) -> Self {
134        self.config.send_acks = send_acks;
135        self
136    }
137
138    /// Set the User-Agent header value.
139    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
140        self.config.user_agent = user_agent.into();
141        self
142    }
143
144    /// Set the maximum reconnection attempts.
145    ///
146    /// `None` means unlimited attempts.
147    pub fn max_reconnect_attempts(mut self, max: Option<u32>) -> Self {
148        self.config.max_reconnect_attempts = max;
149        self
150    }
151
152    /// Set the initial reconnection delay.
153    pub fn initial_reconnect_delay(mut self, delay: Duration) -> Self {
154        self.config.initial_reconnect_delay = delay;
155        self
156    }
157
158    /// Set the maximum reconnection delay.
159    pub fn max_reconnect_delay(mut self, delay: Duration) -> Self {
160        self.config.max_reconnect_delay = delay;
161        self
162    }
163
164    /// Set the reconnection backoff multiplier.
165    pub fn reconnect_backoff_multiplier(mut self, multiplier: f64) -> Self {
166        self.config.reconnect_backoff_multiplier = multiplier;
167        self
168    }
169
170    /// Set the internal channel buffer size.
171    pub fn channel_buffer_size(mut self, size: usize) -> Self {
172        self.config.channel_buffer_size = size;
173        self
174    }
175
176    /// Build the configuration.
177    pub fn build(self) -> TapConfig {
178        self.config
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_default_config() {
188        let config = TapConfig::default();
189        assert_eq!(config.hostname, "localhost:2480");
190        assert!(config.admin_password.is_none());
191        assert!(config.send_acks);
192        assert!(config.max_reconnect_attempts.is_none());
193        assert_eq!(config.initial_reconnect_delay, Duration::from_secs(1));
194        assert_eq!(config.max_reconnect_delay, Duration::from_secs(60));
195        assert!((config.reconnect_backoff_multiplier - 2.0).abs() < f64::EPSILON);
196        assert_eq!(config.channel_buffer_size, 32);
197    }
198
199    #[test]
200    fn test_builder() {
201        let config = TapConfig::builder()
202            .hostname("tap.example.com:2480")
203            .admin_password("secret123")
204            .send_acks(false)
205            .max_reconnect_attempts(Some(5))
206            .initial_reconnect_delay(Duration::from_millis(500))
207            .max_reconnect_delay(Duration::from_secs(30))
208            .reconnect_backoff_multiplier(1.5)
209            .build();
210
211        assert_eq!(config.hostname, "tap.example.com:2480");
212        assert_eq!(config.admin_password, Some("secret123".to_string()));
213        assert!(!config.send_acks);
214        assert_eq!(config.max_reconnect_attempts, Some(5));
215        assert_eq!(config.initial_reconnect_delay, Duration::from_millis(500));
216        assert_eq!(config.max_reconnect_delay, Duration::from_secs(30));
217        assert!((config.reconnect_backoff_multiplier - 1.5).abs() < f64::EPSILON);
218    }
219
220    #[test]
221    fn test_channel_buffer_size() {
222        let config = TapConfig::builder().channel_buffer_size(128).build();
223        assert_eq!(config.channel_buffer_size, 128);
224    }
225
226    #[test]
227    fn test_ws_url() {
228        let config = TapConfig::new("localhost:2480");
229        assert_eq!(config.ws_url(), "ws://localhost:2480/channel");
230
231        let config = TapConfig::new("tap.example.com:8080");
232        assert_eq!(config.ws_url(), "ws://tap.example.com:8080/channel");
233    }
234
235    #[test]
236    fn test_http_base_url() {
237        let config = TapConfig::new("localhost:2480");
238        assert_eq!(config.http_base_url(), "http://localhost:2480");
239    }
240}