1use 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#[derive(Debug)]
27pub struct NoToken;
28
29#[derive(Debug)]
31pub struct HasToken;
32
33pub 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 #[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 #[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
120impl<S> ApplicationBuilder<S> {
122 #[must_use]
124 pub fn request(mut self, request: Arc<dyn BaseRequest>) -> Self {
125 self.request = Some(request);
126 self
127 }
128
129 #[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 #[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 #[must_use]
145 pub fn defaults(mut self, defaults: Defaults) -> Self {
146 self.defaults = Some(defaults);
147 self
148 }
149
150 #[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 #[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 #[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 #[must_use]
178 pub fn context_types(mut self, ct: ContextTypes) -> Self {
179 self.context_types = Some(ct);
180 self
181 }
182
183 #[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 #[must_use]
192 pub fn post_init(mut self, hook: LifecycleHook) -> Self {
193 self.post_init = Some(hook);
194 self
195 }
196
197 #[must_use]
199 pub fn post_stop(mut self, hook: LifecycleHook) -> Self {
200 self.post_stop = Some(hook);
201 self
202 }
203
204 #[must_use]
206 pub fn post_shutdown(mut self, hook: LifecycleHook) -> Self {
207 self.post_shutdown = Some(hook);
208 self
209 }
210
211 #[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 #[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 #[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 #[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}