Skip to main content

rust_tg_bot_ext/
builder.rs

1//! Typestate builder for [`Application`](crate::application::Application).
2
3use std::marker::PhantomData;
4use std::sync::Arc;
5
6use rust_tg_bot_raw::bot::Bot;
7use rust_tg_bot_raw::request::base::BaseRequest;
8
9#[cfg(feature = "persistence")]
10use crate::application::DynPersistence;
11use crate::application::{Application, ApplicationConfig, LifecycleHook};
12use crate::context_types::ContextTypes;
13use crate::defaults::Defaults;
14use crate::ext_bot::ExtBot;
15#[cfg(feature = "job-queue")]
16use crate::job_queue::JobQueue;
17#[cfg(feature = "rate-limiter")]
18use crate::rate_limiter::{DynRateLimiter, RateLimitedRequest};
19use crate::update_processor;
20
21// ---------------------------------------------------------------------------
22// Typestate markers
23// ---------------------------------------------------------------------------
24
25/// Typestate marker indicating no bot token has been provided yet.
26#[derive(Debug)]
27pub struct NoToken;
28
29/// Typestate marker indicating a bot token has been set.
30#[derive(Debug)]
31pub struct HasToken;
32
33// ---------------------------------------------------------------------------
34// Builder
35// ---------------------------------------------------------------------------
36/// Typestate builder for constructing an [`Application`](crate::application::Application).
37///
38/// The builder enforces at compile time that a bot token is set before
39/// [`build`](ApplicationBuilder::build) can be called, using the
40/// [`NoToken`] / [`HasToken`] marker types.
41pub struct ApplicationBuilder<State = NoToken> {
42    token: Option<String>,
43    request: Option<Arc<dyn BaseRequest>>,
44    base_url: Option<String>,
45    base_file_url: Option<String>,
46    defaults: Option<Defaults>,
47    arbitrary_callback_data: Option<usize>,
48    #[cfg(feature = "rate-limiter")]
49    rate_limiter: Option<Arc<dyn DynRateLimiter>>,
50    #[cfg(not(feature = "rate-limiter"))]
51    rate_limiter: Option<()>,
52    context_types: Option<ContextTypes>,
53    concurrent_updates: usize,
54    post_init: Option<LifecycleHook>,
55    post_stop: Option<LifecycleHook>,
56    post_shutdown: Option<LifecycleHook>,
57    #[cfg(feature = "persistence")]
58    persistence: Option<Box<dyn DynPersistence>>,
59    #[cfg(feature = "job-queue")]
60    job_queue: Option<Arc<JobQueue>>,
61    _marker: PhantomData<State>,
62}
63
64impl Default for ApplicationBuilder<NoToken> {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl ApplicationBuilder<NoToken> {
71    /// Create a new builder with no token and default settings.
72    #[must_use]
73    pub fn new() -> Self {
74        Self {
75            token: None,
76            request: None,
77            base_url: None,
78            base_file_url: None,
79            defaults: None,
80            arbitrary_callback_data: None,
81            rate_limiter: None,
82            context_types: None,
83            concurrent_updates: 1,
84            post_init: None,
85            post_stop: None,
86            post_shutdown: None,
87            #[cfg(feature = "persistence")]
88            persistence: None,
89            #[cfg(feature = "job-queue")]
90            job_queue: None,
91            _marker: PhantomData,
92        }
93    }
94
95    /// Set the bot token. Transitions the builder to the [`HasToken`] state.
96    #[must_use]
97    pub fn token(self, token: impl Into<String>) -> ApplicationBuilder<HasToken> {
98        ApplicationBuilder {
99            token: Some(token.into()),
100            request: self.request,
101            base_url: self.base_url,
102            base_file_url: self.base_file_url,
103            defaults: self.defaults,
104            arbitrary_callback_data: self.arbitrary_callback_data,
105            rate_limiter: self.rate_limiter,
106            context_types: self.context_types,
107            concurrent_updates: self.concurrent_updates,
108            post_init: self.post_init,
109            post_stop: self.post_stop,
110            post_shutdown: self.post_shutdown,
111            #[cfg(feature = "persistence")]
112            persistence: self.persistence,
113            #[cfg(feature = "job-queue")]
114            job_queue: self.job_queue,
115            _marker: PhantomData,
116        }
117    }
118}
119
120// Methods available in *any* state.
121impl<S> ApplicationBuilder<S> {
122    /// Set a custom HTTP request backend for the bot.
123    #[must_use]
124    pub fn request(mut self, request: Arc<dyn BaseRequest>) -> Self {
125        self.request = Some(request);
126        self
127    }
128
129    /// Override the base URL for Telegram Bot API requests.
130    #[must_use]
131    pub fn base_url(mut self, url: impl Into<String>) -> Self {
132        self.base_url = Some(url.into());
133        self
134    }
135
136    /// Override the base URL for downloading files from Telegram.
137    #[must_use]
138    pub fn base_file_url(mut self, url: impl Into<String>) -> Self {
139        self.base_file_url = Some(url.into());
140        self
141    }
142
143    /// Set default values (e.g. parse_mode) applied to every API call.
144    #[must_use]
145    pub fn defaults(mut self, defaults: Defaults) -> Self {
146        self.defaults = Some(defaults);
147        self
148    }
149
150    /// Enable the arbitrary callback data cache with the given maximum size.
151    #[must_use]
152    pub fn arbitrary_callback_data(mut self, maxsize: usize) -> Self {
153        self.arbitrary_callback_data = Some(maxsize);
154        self
155    }
156
157    /// Sets the rate limiter for the application.
158    ///
159    /// When set, all API calls will be throttled through the provided limiter.
160    /// Requires the `rate-limiter` feature.
161    #[cfg(feature = "rate-limiter")]
162    #[must_use]
163    pub fn rate_limiter(mut self, rl: Arc<dyn DynRateLimiter>) -> Self {
164        self.rate_limiter = Some(rl);
165        self
166    }
167
168    /// Sets the rate-limiter placeholder (feature disabled).
169    #[cfg(not(feature = "rate-limiter"))]
170    #[must_use]
171    pub fn rate_limiter(mut self, _rl: ()) -> Self {
172        self.rate_limiter = Some(());
173        self
174    }
175
176    /// Set custom context types for the application.
177    #[must_use]
178    pub fn context_types(mut self, ct: ContextTypes) -> Self {
179        self.context_types = Some(ct);
180        self
181    }
182
183    /// Set the maximum number of concurrent update processing tasks. Minimum is 1.
184    #[must_use]
185    pub fn concurrent_updates(mut self, n: usize) -> Self {
186        self.concurrent_updates = if n == 0 { 1 } else { n };
187        self
188    }
189
190    /// Register a hook to run after the application has been initialized.
191    #[must_use]
192    pub fn post_init(mut self, hook: LifecycleHook) -> Self {
193        self.post_init = Some(hook);
194        self
195    }
196
197    /// Register a hook to run after the application has stopped.
198    #[must_use]
199    pub fn post_stop(mut self, hook: LifecycleHook) -> Self {
200        self.post_stop = Some(hook);
201        self
202    }
203
204    /// Register a hook to run after the application has been fully shut down.
205    #[must_use]
206    pub fn post_shutdown(mut self, hook: LifecycleHook) -> Self {
207        self.post_shutdown = Some(hook);
208        self
209    }
210
211    /// Sets the persistence backend.
212    ///
213    /// Requires the `persistence` feature.
214    #[cfg(feature = "persistence")]
215    #[must_use]
216    pub fn persistence(mut self, p: Box<dyn DynPersistence>) -> Self {
217        self.persistence = Some(p);
218        self
219    }
220
221    /// Sets the job queue.
222    ///
223    /// Requires the `job-queue` feature.
224    #[cfg(feature = "job-queue")]
225    #[must_use]
226    pub fn job_queue(mut self, jq: Arc<JobQueue>) -> Self {
227        self.job_queue = Some(jq);
228        self
229    }
230}
231
232impl ApplicationBuilder<HasToken> {
233    /// Consume the builder and construct the [`Application`](crate::application::Application).
234    #[must_use]
235    pub fn build(self) -> Arc<Application> {
236        let token = self.token.expect("HasToken state guarantees a token");
237
238        let request: Arc<dyn BaseRequest> = self.request.unwrap_or_else(|| {
239            Arc::new(
240                rust_tg_bot_raw::request::reqwest_impl::ReqwestRequest::new()
241                    .expect("Failed to create default ReqwestRequest"),
242            )
243        });
244
245        // Wrap the request backend in a RateLimitedRequest if a limiter is set.
246        #[cfg(feature = "rate-limiter")]
247        let (effective_request, rate_limiter) = if let Some(ref rl) = self.rate_limiter {
248            let wrapped: Arc<dyn BaseRequest> =
249                Arc::new(RateLimitedRequest::new(request, rl.clone()));
250            (wrapped, self.rate_limiter)
251        } else {
252            (request, None)
253        };
254
255        #[cfg(not(feature = "rate-limiter"))]
256        let (effective_request, rate_limiter) = (request, self.rate_limiter);
257
258        let bot_raw = Bot::new(&token, effective_request);
259
260        let ext_bot = Arc::new(ExtBot::new(
261            bot_raw,
262            self.defaults,
263            self.arbitrary_callback_data,
264            rate_limiter,
265        ));
266
267        let context_types = self.context_types.unwrap_or_default();
268
269        let update_processor = Arc::new(
270            update_processor::simple_processor(self.concurrent_updates)
271                .expect("concurrent_updates validated by builder"),
272        );
273
274        let mut config = ApplicationConfig::new(ext_bot, context_types, update_processor);
275        config.post_init = self.post_init;
276        config.post_stop = self.post_stop;
277        config.post_shutdown = self.post_shutdown;
278        #[cfg(feature = "persistence")]
279        {
280            config.persistence = self.persistence;
281        }
282        #[cfg(feature = "job-queue")]
283        {
284            config.job_queue = self.job_queue;
285        }
286
287        Application::new(config)
288    }
289}
290
291impl<S> std::fmt::Debug for ApplicationBuilder<S> {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        f.debug_struct("ApplicationBuilder")
294            .field("has_token", &self.token.is_some())
295            .field("concurrent_updates", &self.concurrent_updates)
296            .field("has_rate_limiter", &self.rate_limiter.is_some())
297            .finish()
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::ext_bot::test_support::mock_request;
305
306    #[test]
307    fn builder_typestate_enforces_token() {
308        let app = ApplicationBuilder::new()
309            .token("test_token")
310            .request(mock_request())
311            .build();
312        assert_eq!(app.bot().token(), "test_token");
313    }
314
315    #[test]
316    fn builder_with_defaults() {
317        let defaults = Defaults::builder().parse_mode("HTML").build();
318        let app = ApplicationBuilder::new()
319            .token("t")
320            .request(mock_request())
321            .defaults(defaults)
322            .build();
323        assert_eq!(app.bot().defaults().unwrap().parse_mode(), Some("HTML"));
324    }
325
326    #[test]
327    fn builder_concurrent_updates() {
328        let app = ApplicationBuilder::new()
329            .token("t")
330            .request(mock_request())
331            .concurrent_updates(8)
332            .build();
333        assert_eq!(app.concurrent_updates(), 8);
334    }
335
336    #[test]
337    fn builder_zero_concurrent_updates_defaults_to_one() {
338        let app = ApplicationBuilder::new()
339            .token("t")
340            .request(mock_request())
341            .concurrent_updates(0)
342            .build();
343        assert_eq!(app.concurrent_updates(), 1);
344    }
345
346    #[test]
347    fn builder_arbitrary_callback_data() {
348        let app = ApplicationBuilder::new()
349            .token("t")
350            .request(mock_request())
351            .arbitrary_callback_data(512)
352            .build();
353        assert!(app.bot().has_callback_data_cache());
354    }
355
356    #[test]
357    fn builder_custom_context_types() {
358        let ct = ContextTypes::default();
359        let app = ApplicationBuilder::new()
360            .token("t")
361            .request(mock_request())
362            .context_types(ct)
363            .build();
364        assert_eq!(app.bot().token(), "t");
365    }
366
367    #[test]
368    fn builder_with_lifecycle_hooks() {
369        let hook: LifecycleHook = Arc::new(|_app| Box::pin(async {}));
370        let app = ApplicationBuilder::new()
371            .token("t")
372            .request(mock_request())
373            .post_init(hook.clone())
374            .post_stop(hook.clone())
375            .post_shutdown(hook)
376            .build();
377        assert_eq!(app.bot().token(), "t");
378    }
379
380    #[cfg(feature = "job-queue")]
381    #[test]
382    fn builder_with_job_queue() {
383        let jq = Arc::new(JobQueue::new());
384        let app = ApplicationBuilder::new()
385            .token("t")
386            .request(mock_request())
387            .job_queue(jq)
388            .build();
389        assert!(app.job_queue().is_some());
390    }
391
392    #[cfg(feature = "rate-limiter")]
393    #[test]
394    fn builder_with_rate_limiter() {
395        use crate::rate_limiter::NoRateLimiter;
396
397        let limiter: Arc<dyn DynRateLimiter> = Arc::new(NoRateLimiter);
398        let app = ApplicationBuilder::new()
399            .token("rl_app")
400            .request(mock_request())
401            .rate_limiter(limiter)
402            .build();
403        assert_eq!(app.bot().token(), "rl_app");
404        assert!(app.bot().has_rate_limiter());
405    }
406
407    #[test]
408    fn builder_debug() {
409        let b = ApplicationBuilder::new();
410        let s = format!("{b:?}");
411        assert!(s.contains("ApplicationBuilder"));
412        assert!(s.contains("has_token"));
413    }
414
415    #[test]
416    fn default_builder_is_no_token() {
417        let b = ApplicationBuilder::default();
418        let _b2 = b.token("tok");
419    }
420}