Skip to main content

rust_tg_bot_ext/
defaults.rs

1//! Default parameter values for bot methods.
2//!
3//! Ported from `python-telegram-bot/src/telegram/ext/_defaults.py`.
4//!
5//! [`Defaults`] gathers all user-defined default values that the [`ExtBot`](super::ext_bot::ExtBot)
6//! and handlers consult when an explicit value is not provided by the caller.
7//!
8//! Once constructed, every field is immutable (no public setters).
9//!
10//! # Construction
11//!
12//! Use the builder pattern via [`Defaults::builder()`]:
13//!
14//! ```
15//! # use rust_tg_bot_ext::defaults::Defaults;
16//! let defaults = Defaults::builder()
17//!     .parse_mode("HTML")
18//!     .disable_notification(true)
19//!     .build();
20//!
21//! assert_eq!(defaults.parse_mode(), Some("HTML"));
22//! ```
23
24use std::collections::HashMap;
25
26use serde_json::Value;
27
28use rust_tg_bot_raw::types::link_preview_options::LinkPreviewOptions;
29
30/// Convenience struct to gather all parameters with a (user defined) default value.
31///
32/// Fields that are `None` indicate "no default was set" and will not be injected into API
33/// calls.
34#[derive(Debug, Clone)]
35pub struct Defaults {
36    parse_mode: Option<String>,
37    disable_notification: Option<bool>,
38    allow_sending_without_reply: Option<bool>,
39    protect_content: Option<bool>,
40    block: bool,
41    link_preview_options: Option<LinkPreviewOptions>,
42    do_quote: Option<bool>,
43    /// Pre-computed map of non-`None` defaults keyed by the API parameter name.  Used by
44    /// `ExtBot` to merge defaults into outgoing requests.
45    api_defaults: HashMap<String, Value>,
46}
47
48impl Defaults {
49    /// Creates a new `Defaults` instance.
50    ///
51    /// Only the values that are explicitly `Some(...)` will be injected into API calls.
52    /// `block` defaults to `true` when `None` is passed.
53    ///
54    /// Prefer [`Defaults::builder()`] for public construction -- this avoids long
55    /// `None, None, None` argument lists.
56    #[must_use]
57    pub(crate) fn new(
58        parse_mode: Option<String>,
59        disable_notification: Option<bool>,
60        allow_sending_without_reply: Option<bool>,
61        protect_content: Option<bool>,
62        block: Option<bool>,
63        link_preview_options: Option<LinkPreviewOptions>,
64        do_quote: Option<bool>,
65    ) -> Self {
66        let block = block.unwrap_or(true);
67
68        let mut api_defaults = HashMap::new();
69
70        if let Some(ref pm) = parse_mode {
71            let v = Value::String(pm.clone());
72            api_defaults.insert("parse_mode".into(), v.clone());
73            api_defaults.insert("explanation_parse_mode".into(), v.clone());
74            api_defaults.insert("text_parse_mode".into(), v.clone());
75            api_defaults.insert("question_parse_mode".into(), v);
76        }
77        if let Some(dn) = disable_notification {
78            api_defaults.insert("disable_notification".into(), Value::Bool(dn));
79        }
80        if let Some(aswr) = allow_sending_without_reply {
81            api_defaults.insert("allow_sending_without_reply".into(), Value::Bool(aswr));
82        }
83        if let Some(pc) = protect_content {
84            api_defaults.insert("protect_content".into(), Value::Bool(pc));
85        }
86        if let Some(dq) = do_quote {
87            api_defaults.insert("do_quote".into(), Value::Bool(dq));
88        }
89        if let Some(ref lpo) = link_preview_options {
90            if let Ok(v) = serde_json::to_value(lpo) {
91                api_defaults.insert("link_preview_options".into(), v);
92            }
93        }
94
95        Self {
96            parse_mode,
97            disable_notification,
98            allow_sending_without_reply,
99            protect_content,
100            block,
101            link_preview_options,
102            do_quote,
103            api_defaults,
104        }
105    }
106
107    /// Returns a new [`DefaultsBuilder`] for ergonomic construction.
108    ///
109    /// # Example
110    ///
111    /// ```
112    /// # use rust_tg_bot_ext::defaults::Defaults;
113    /// let defaults = Defaults::builder()
114    ///     .parse_mode("HTML")
115    ///     .protect_content(true)
116    ///     .build();
117    /// ```
118    #[must_use]
119    pub fn builder() -> DefaultsBuilder {
120        DefaultsBuilder::new()
121    }
122
123    // -- Read-only accessors (mirrors Python @property with no setter) --
124
125    /// Send Markdown or HTML -- if you want Telegram apps to show bold, italic, fixed-width text
126    /// or URLs in your bot's message.
127    #[must_use]
128    pub fn parse_mode(&self) -> Option<&str> {
129        self.parse_mode.as_deref()
130    }
131
132    /// Alias for [`parse_mode`](Self::parse_mode), used for the corresponding parameter of
133    /// `Bot::send_poll`.
134    #[must_use]
135    pub fn explanation_parse_mode(&self) -> Option<&str> {
136        self.parse_mode.as_deref()
137    }
138
139    /// Alias for [`parse_mode`](Self::parse_mode), used for `InputPollOption` and
140    /// `Bot::send_gift`.
141    #[must_use]
142    pub fn text_parse_mode(&self) -> Option<&str> {
143        self.parse_mode.as_deref()
144    }
145
146    /// Alias for [`parse_mode`](Self::parse_mode), used for `Bot::send_poll`.
147    #[must_use]
148    pub fn question_parse_mode(&self) -> Option<&str> {
149        self.parse_mode.as_deref()
150    }
151
152    /// Alias for [`parse_mode`](Self::parse_mode), used for `ReplyParameters`.
153    #[must_use]
154    pub fn quote_parse_mode(&self) -> Option<&str> {
155        self.parse_mode.as_deref()
156    }
157
158    /// Sends the message silently.
159    #[must_use]
160    pub fn disable_notification(&self) -> Option<bool> {
161        self.disable_notification
162    }
163
164    /// Pass `true` if the message should be sent even if the specified replied-to message is not
165    /// found.
166    #[must_use]
167    pub fn allow_sending_without_reply(&self) -> Option<bool> {
168        self.allow_sending_without_reply
169    }
170
171    /// Default setting for `BaseHandler.block` and error handlers.
172    #[must_use]
173    pub fn block(&self) -> bool {
174        self.block
175    }
176
177    /// Protects the contents of the sent message from forwarding and saving.
178    #[must_use]
179    pub fn protect_content(&self) -> Option<bool> {
180        self.protect_content
181    }
182
183    /// Link preview generation options for all outgoing messages.
184    #[must_use]
185    pub fn link_preview_options(&self) -> Option<&LinkPreviewOptions> {
186        self.link_preview_options.as_ref()
187    }
188
189    /// Whether the bot should quote the replied-to message.
190    #[must_use]
191    pub fn do_quote(&self) -> Option<bool> {
192        self.do_quote
193    }
194
195    /// Pre-computed mapping of non-`None` defaults keyed by API parameter name.
196    #[must_use]
197    pub fn api_defaults(&self) -> &HashMap<String, Value> {
198        &self.api_defaults
199    }
200}
201
202impl PartialEq for Defaults {
203    fn eq(&self, other: &Self) -> bool {
204        self.parse_mode == other.parse_mode
205            && self.disable_notification == other.disable_notification
206            && self.allow_sending_without_reply == other.allow_sending_without_reply
207            && self.protect_content == other.protect_content
208            && self.block == other.block
209            && self.link_preview_options == other.link_preview_options
210            && self.do_quote == other.do_quote
211    }
212}
213
214impl Eq for Defaults {}
215
216impl std::hash::Hash for Defaults {
217    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
218        self.parse_mode.hash(state);
219        self.disable_notification.hash(state);
220        self.allow_sending_without_reply.hash(state);
221        self.protect_content.hash(state);
222        self.block.hash(state);
223        self.do_quote.hash(state);
224        // LinkPreviewOptions does not implement Hash, so we hash its JSON representation.
225        if let Some(ref lpo) = self.link_preview_options {
226            if let Ok(v) = serde_json::to_string(lpo) {
227                v.hash(state);
228            }
229        }
230    }
231}
232
233// ---------------------------------------------------------------------------
234// DefaultsBuilder
235// ---------------------------------------------------------------------------
236
237/// Builder for [`Defaults`].
238///
239/// Provides ergonomic construction without `None, None, None` argument lists.
240///
241/// # Example
242///
243/// ```
244/// # use rust_tg_bot_ext::defaults::Defaults;
245/// let defaults = Defaults::builder()
246///     .parse_mode("HTML")
247///     .disable_notification(true)
248///     .do_quote(true)
249///     .build();
250///
251/// assert_eq!(defaults.parse_mode(), Some("HTML"));
252/// assert_eq!(defaults.disable_notification(), Some(true));
253/// assert!(defaults.block()); // defaults to true
254/// ```
255#[derive(Debug)]
256pub struct DefaultsBuilder {
257    parse_mode: Option<String>,
258    disable_notification: Option<bool>,
259    allow_sending_without_reply: Option<bool>,
260    protect_content: Option<bool>,
261    block: Option<bool>,
262    link_preview_options: Option<LinkPreviewOptions>,
263    do_quote: Option<bool>,
264}
265
266impl Default for DefaultsBuilder {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl DefaultsBuilder {
273    /// Creates a new builder with all fields unset.
274    #[must_use]
275    pub fn new() -> Self {
276        Self {
277            parse_mode: None,
278            disable_notification: None,
279            allow_sending_without_reply: None,
280            protect_content: None,
281            block: None,
282            link_preview_options: None,
283            do_quote: None,
284        }
285    }
286
287    /// Sets the default parse mode (e.g. `"HTML"`, `"MarkdownV2"`).
288    #[must_use]
289    pub fn parse_mode(mut self, mode: impl Into<String>) -> Self {
290        self.parse_mode = Some(mode.into());
291        self
292    }
293
294    /// Sets whether messages are sent silently by default.
295    #[must_use]
296    pub fn disable_notification(mut self, value: bool) -> Self {
297        self.disable_notification = Some(value);
298        self
299    }
300
301    /// Sets whether messages should be sent even if the replied-to message is not found.
302    #[must_use]
303    pub fn allow_sending_without_reply(mut self, value: bool) -> Self {
304        self.allow_sending_without_reply = Some(value);
305        self
306    }
307
308    /// Sets whether message contents are protected from forwarding and saving.
309    #[must_use]
310    pub fn protect_content(mut self, value: bool) -> Self {
311        self.protect_content = Some(value);
312        self
313    }
314
315    /// Sets the default `block` value for handlers. Defaults to `true` if not set.
316    #[must_use]
317    pub fn block(mut self, value: bool) -> Self {
318        self.block = Some(value);
319        self
320    }
321
322    /// Sets the default link preview options.
323    #[must_use]
324    pub fn link_preview_options(mut self, options: LinkPreviewOptions) -> Self {
325        self.link_preview_options = Some(options);
326        self
327    }
328
329    /// Sets whether the bot should quote the replied-to message.
330    #[must_use]
331    pub fn do_quote(mut self, value: bool) -> Self {
332        self.do_quote = Some(value);
333        self
334    }
335
336    /// Builds the [`Defaults`] instance.
337    #[must_use]
338    pub fn build(self) -> Defaults {
339        Defaults::new(
340            self.parse_mode,
341            self.disable_notification,
342            self.allow_sending_without_reply,
343            self.protect_content,
344            self.block,
345            self.link_preview_options,
346            self.do_quote,
347        )
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn defaults_immutable_accessors() {
357        let d = Defaults::builder()
358            .parse_mode("HTML")
359            .disable_notification(true)
360            .allow_sending_without_reply(false)
361            .do_quote(true)
362            .build();
363
364        assert_eq!(d.parse_mode(), Some("HTML"));
365        assert_eq!(d.explanation_parse_mode(), Some("HTML"));
366        assert_eq!(d.text_parse_mode(), Some("HTML"));
367        assert_eq!(d.question_parse_mode(), Some("HTML"));
368        assert_eq!(d.disable_notification(), Some(true));
369        assert_eq!(d.allow_sending_without_reply(), Some(false));
370        assert!(d.block());
371        assert_eq!(d.protect_content(), None);
372        assert_eq!(d.do_quote(), Some(true));
373    }
374
375    #[test]
376    fn defaults_api_defaults_map() {
377        let d = Defaults::builder()
378            .parse_mode("MarkdownV2")
379            .protect_content(true)
380            .build();
381
382        let m = d.api_defaults();
383        assert!(m.contains_key("parse_mode"));
384        assert!(m.contains_key("explanation_parse_mode"));
385        assert!(m.contains_key("protect_content"));
386        assert!(!m.contains_key("disable_notification"));
387    }
388
389    #[test]
390    fn defaults_equality() {
391        let a = Defaults::builder().parse_mode("HTML").build();
392        let b = Defaults::builder().parse_mode("HTML").build();
393        let c = Defaults::builder().build();
394        assert_eq!(a, b);
395        assert_ne!(a, c);
396    }
397
398    #[test]
399    fn block_defaults_to_true() {
400        let d = Defaults::builder().build();
401        assert!(d.block());
402    }
403
404    #[test]
405    fn block_can_be_set_to_false() {
406        let d = Defaults::builder().block(false).build();
407        assert!(!d.block());
408    }
409
410    #[test]
411    fn builder_default_trait() {
412        let b = DefaultsBuilder::default();
413        let d = b.build();
414        assert!(d.parse_mode().is_none());
415        assert!(d.block());
416    }
417}