vkteams_bot/api/
types.rs

1//! API types
2use crate::error::{ApiError, BotError, Result};
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use std::borrow::Cow;
5use std::fmt::*;
6use std::time::Duration;
7#[cfg(feature = "templates")]
8use tera::{Context, Tera};
9use tracing::debug;
10
11/// Environment variable name for bot API URL
12pub const VKTEAMS_BOT_API_URL: &str = "VKTEAMS_BOT_API_URL";
13/// Environment variable name for bot API token
14pub const VKTEAMS_BOT_API_TOKEN: &str = "VKTEAMS_BOT_API_TOKEN";
15/// Environment variable name for bot Proxy URL
16pub const VKTEAMS_PROXY: &str = "VKTEAMS_PROXY";
17/// Timeout for long polling
18pub const POLL_TIME: u64 = 30;
19/// Global timeout for [`reqwest::Client`]
20/// [`reqwest::Client`]: https://docs.rs/reqwest/latest/reqwest/struct.Client.html
21pub const POLL_DURATION: &Duration = &Duration::from_secs(POLL_TIME + 10);
22
23pub const SERVICE_NAME: &str = "BOT";
24/// Supported API versions
25#[derive(Debug)]
26pub enum APIVersionUrl {
27    /// default V1
28    V1,
29}
30/// Supported API HTTP methods
31#[derive(Debug, Default)]
32pub enum HTTPMethod {
33    #[default]
34    GET,
35    POST,
36}
37
38#[derive(Debug, Default)]
39pub enum HTTPBody {
40    // JSON,
41    MultiPart(MultipartName),
42    #[default]
43    None,
44}
45/// Bot request trait
46pub trait BotRequest {
47    type Args;
48
49    const METHOD: &'static str;
50    const HTTP_METHOD: HTTPMethod = HTTPMethod::GET;
51    type RequestType: Serialize + Debug + Default;
52    type ResponseType: Serialize + DeserializeOwned + Debug + Default;
53    fn get_multipart(&self) -> &MultipartName;
54    fn new(args: Self::Args) -> Self;
55    fn get_chat_id(&self) -> Option<&ChatId>;
56}
57/// API event id type
58pub type EventId = u32;
59/// Message text struct
60#[derive(Serialize, Clone, Debug)]
61pub enum MessageTextFormat {
62    /// Plain text
63    Plain(String),
64    /// Bold text
65    Bold(String),
66    /// Italic text
67    Italic(String),
68    /// Underline text
69    Underline(String),
70    /// Strikethrough text
71    Strikethrough(String),
72    /// Inline URL
73    Link(String, String),
74    /// Inline mention of a user
75    Mention(ChatId),
76    /// Code formatted text
77    Code(String),
78    /// Pre-formatted fixed-width test block
79    Pre(String, Option<String>),
80    /// Ordered list
81    OrderedList(Vec<String>),
82    /// Unordered list
83    UnOrderedList(Vec<String>),
84    /// Quote text
85    Quote(String),
86    None,
87}
88/// Message text parse struct
89#[derive(Default, Clone, Debug)]
90pub struct MessageTextParser {
91    /// Array of text formats
92    pub text: Vec<MessageTextFormat>,
93    // Context for templates
94    #[cfg(feature = "templates")]
95    pub ctx: Context,
96    // Template name
97    #[cfg(feature = "templates")]
98    pub name: String,
99    // Tera template engine
100    #[cfg(feature = "templates")]
101    pub tmpl: Tera,
102    /// ## Parse mode
103    /// - `HTML` - HTML
104    /// - `MarkdownV2` - Markdown
105    pub parse_mode: ParseMode,
106}
107/// Keyboard for send message methods
108/// One of variants must be set:
109/// - {`text`: String,`url`: String,`style`: [`ButtonStyle`]} - simple buttons
110/// - {`text`: String,`callback_data`: String,`style`: [`ButtonStyle`]} - buttons with callback
111#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
112#[serde(rename_all = "camelCase")]
113pub struct ButtonKeyboard {
114    pub text: String, // formatting is not supported
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub url: Option<String>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub callback_data: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub style: Option<ButtonStyle>,
121}
122#[derive(Serialize, Deserialize, Clone, Debug)]
123/// Array of keyboard buttons
124pub struct Keyboard {
125    pub buttons: Vec<Vec<ButtonKeyboard>>,
126}
127/// Keyboard buttons style
128#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub enum ButtonStyle {
131    Primary,
132    Attention,
133    #[default]
134    Base,
135}
136/// Message text format parse mode
137#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
138pub enum ParseMode {
139    MarkdownV2,
140    #[default]
141    HTML,
142    #[cfg(feature = "templates")]
143    Template,
144}
145/// Event message
146#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
147#[serde(rename_all = "camelCase")]
148pub struct EventMessage {
149    pub event_id: EventId,
150    #[serde(flatten)]
151    pub event_type: EventType,
152}
153/// Event types
154#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
155#[serde(rename_all = "camelCase", tag = "type", content = "payload")]
156pub enum EventType {
157    NewMessage(Box<EventPayloadNewMessage>),
158    EditedMessage(Box<EventPayloadEditedMessage>),
159    DeleteMessage(Box<EventPayloadDeleteMessage>),
160    PinnedMessage(Box<EventPayloadPinnedMessage>),
161    UnpinnedMessage(Box<EventPayloadUnpinnedMessage>),
162    NewChatMembers(Box<EventPayloadNewChatMembers>),
163    LeftChatMembers(Box<EventPayloadLeftChatMembers>),
164    CallbackQuery(Box<EventPayloadCallbackQuery>),
165    #[default]
166    None,
167}
168/// Message payload event type newMessage
169#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
170#[serde(rename_all = "camelCase")]
171pub struct EventPayloadNewMessage {
172    pub msg_id: MsgId,
173    #[serde(default)]
174    pub text: String,
175    pub chat: Chat,
176    pub from: From,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub format: Option<MessageFormat>,
179    #[serde(default)]
180    pub parts: Vec<MessageParts>,
181    pub timestamp: Timestamp,
182}
183/// Message payload event type editedMessage
184#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
185#[serde(rename_all = "camelCase")]
186pub struct EventPayloadEditedMessage {
187    pub msg_id: MsgId,
188    pub text: String,
189    pub timestamp: Timestamp,
190    pub chat: Chat,
191    pub from: From,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub format: Option<MessageFormat>,
194    pub edited_timestamp: Timestamp,
195}
196/// Message payload event type deleteMessage
197#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
198#[serde(rename_all = "camelCase")]
199pub struct EventPayloadDeleteMessage {
200    pub msg_id: MsgId,
201    pub chat: Chat,
202    pub timestamp: Timestamp,
203}
204/// Message payload event type pinnedMessage
205#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
206#[serde(rename_all = "camelCase")]
207pub struct EventPayloadPinnedMessage {
208    pub msg_id: MsgId,
209    pub chat: Chat,
210    pub from: From,
211    #[serde(default)]
212    pub text: String,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub format: Option<MessageFormat>,
215    #[serde(default)]
216    pub parts: Vec<MessageParts>,
217    pub timestamp: Timestamp,
218}
219/// Message payload event type unpinnedMessage
220#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
221#[serde(rename_all = "camelCase")]
222pub struct EventPayloadUnpinnedMessage {
223    pub msg_id: MsgId,
224    pub chat: Chat,
225    pub timestamp: Timestamp,
226}
227/// Message payload event type newChatMembers
228#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
229#[serde(rename_all = "camelCase")]
230pub struct EventPayloadNewChatMembers {
231    pub chat: Chat,
232    pub new_members: Vec<From>,
233    pub added_by: From,
234}
235/// Message payload event type leftChatMembers
236#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
237#[serde(rename_all = "camelCase")]
238pub struct EventPayloadLeftChatMembers {
239    pub chat: Chat,
240    pub left_members: Vec<From>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub removed_by: Option<From>,
243}
244/// Callback query event type
245#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
246#[serde(rename_all = "camelCase")]
247pub struct EventPayloadCallbackQuery {
248    pub query_id: QueryId,
249    pub from: From,
250    #[serde(default)]
251    pub chat: Chat,
252    pub message: EventPayloadNewMessage,
253    pub callback_data: String,
254}
255/// Message parts
256#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
257#[serde(rename_all = "camelCase")]
258pub struct MessageParts {
259    #[serde(rename = "type", flatten)]
260    pub part_type: MessagePartsType,
261    // pub payload: MessagePartsPayload,
262}
263/// Message parts type
264#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
265#[serde(rename_all = "camelCase", tag = "type", content = "payload")]
266pub enum MessagePartsType {
267    Sticker(MessagePartsPayloadSticker),
268    Mention(MessagePartsPayloadMention),
269    Voice(MessagePartsPayloadVoice),
270    File(Box<MessagePartsPayloadFile>),
271    Forward(Box<MessagePartsPayloadForward>),
272    Reply(Box<MessagePartsPayloadReply>),
273    InlineKeyboardMarkup(Vec<Vec<MessagePartsPayloadInlineKeyboard>>),
274}
275/// Message parts payload sticker
276#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
277#[serde(rename_all = "camelCase")]
278pub struct MessagePartsPayloadSticker {
279    pub file_id: FileId,
280}
281/// Message parts payload mention
282#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
283#[serde(rename_all = "camelCase")]
284pub struct MessagePartsPayloadMention {
285    #[serde(flatten)]
286    pub user_id: From,
287}
288/// Message parts payload voice
289#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
290#[serde(rename_all = "camelCase")]
291pub struct MessagePartsPayloadVoice {
292    pub file_id: FileId,
293}
294/// Message parts payload file
295#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
296#[serde(rename_all = "camelCase")]
297pub struct MessagePartsPayloadFile {
298    pub file_id: FileId,
299    #[serde(rename = "type", default)]
300    pub file_type: String,
301    #[serde(default)]
302    pub caption: String,
303    #[serde(default)]
304    pub format: MessageFormat,
305}
306/// Message parts payload forward
307#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
308#[serde(rename_all = "camelCase")]
309pub struct MessagePartsPayloadForward {
310    message: MessagePayload,
311}
312/// Message parts payload reply
313#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
314#[serde(rename_all = "camelCase")]
315pub struct MessagePartsPayloadReply {
316    message: MessagePayload,
317}
318/// Message parts payload inline keyboard
319#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
320#[serde(rename_all = "camelCase")]
321pub struct MessagePartsPayloadInlineKeyboard {
322    #[serde(default)]
323    pub callback_data: String,
324    pub style: ButtonStyle,
325    pub text: String,
326    #[serde(default)]
327    pub url: String,
328}
329/// Array of message formats
330#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
331#[serde(rename_all = "camelCase")]
332pub struct MessageFormat {
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub bold: Option<Vec<MessageFormatStruct>>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub italic: Option<Vec<MessageFormatStruct>>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub underline: Option<Vec<MessageFormatStruct>>,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub strikethrough: Option<Vec<MessageFormatStruct>>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub link: Option<Vec<MessageFormatStructLink>>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub mention: Option<Vec<MessageFormatStruct>>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub inline_code: Option<Vec<MessageFormatStruct>>,
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub pre: Option<Vec<MessageFormatStructPre>>,
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub ordered_list: Option<Vec<MessageFormatStruct>>,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub quote: Option<Vec<MessageFormatStruct>>,
353}
354/// Message format struct
355#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
356#[serde(rename_all = "camelCase")]
357pub struct MessageFormatStruct {
358    pub offset: i32,
359    pub length: i32,
360}
361/// Message format struct for link
362#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
363#[serde(rename_all = "camelCase")]
364pub struct MessageFormatStructLink {
365    pub offset: i32,
366    pub length: i32,
367    pub url: String,
368}
369/// Message format struct
370#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
371#[serde(rename_all = "camelCase")]
372pub struct MessageFormatStructPre {
373    pub offset: i32,
374    pub length: i32,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub code: Option<String>,
377}
378/// Event message payload
379#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
380#[serde(rename_all = "camelCase")]
381pub struct MessagePayload {
382    pub from: From,
383    pub msg_id: MsgId,
384    #[serde(default)]
385    pub text: String,
386    pub timestamp: u64,
387    #[serde(default)]
388    pub parts: Vec<MessageParts>,
389}
390/// Chat id struct
391#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
392pub struct ChatId(pub Cow<'static, str>);
393/// Message id struct
394#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
395pub struct MsgId(pub String);
396/// User id struct
397#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
398pub struct UserId(pub String);
399/// File id struct
400#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
401pub struct FileId(pub String);
402/// Query id struct
403#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
404pub struct QueryId(pub String);
405/// Timestamp struct
406#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
407pub struct Timestamp(pub u32);
408/// Chat struct
409#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
410#[serde(rename_all = "camelCase")]
411pub struct Chat {
412    pub chat_id: ChatId,
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub title: Option<String>,
415    #[serde(rename = "type")]
416    pub chat_type: String,
417}
418/// From struct
419#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
420#[serde(rename_all = "camelCase")]
421pub struct From {
422    pub first_name: String,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub last_name: Option<String>, //if its a bot, then it will be EMPTY
425    pub user_id: UserId,
426}
427/// Languages
428#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
429#[serde(rename_all = "camelCase")]
430pub enum Languages {
431    #[default]
432    Ru,
433    En,
434}
435/// Chat types
436#[derive(Serialize, Deserialize, Clone, Debug, Default)]
437#[serde(rename_all = "camelCase")]
438pub enum ChatType {
439    #[default]
440    Private,
441    Group,
442    Channel,
443}
444/// Chat actions
445#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
446#[serde(rename_all = "camelCase")]
447pub enum ChatActions {
448    Looking,
449    #[default]
450    Typing,
451}
452/// Multipart name
453#[derive(Serialize, Deserialize, Debug, Default, Clone)]
454pub enum MultipartName {
455    FilePath(String),
456    ImagePath(String),
457    FileContent {
458        filename: String,
459        content: Vec<u8>,
460    },
461    ImageContent {
462        filename: String,
463        content: Vec<u8>,
464    },
465    #[default]
466    None,
467}
468/// Admin struct
469#[derive(Serialize, Deserialize, Clone, Debug)]
470#[serde(rename_all = "camelCase")]
471pub struct Admin {
472    pub user_id: UserId,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub creator: Option<bool>,
475}
476/// User struct
477#[derive(Serialize, Deserialize, Clone, Debug)]
478#[serde(rename_all = "camelCase")]
479pub struct Users {
480    pub user_id: UserId,
481}
482/// Member struct
483#[derive(Serialize, Deserialize, Clone, Debug)]
484#[serde(rename_all = "camelCase")]
485pub struct Member {
486    pub user_id: UserId,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub creator: Option<bool>,
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub admin: Option<bool>,
491}
492/// Sn struct for members
493#[derive(Serialize, Deserialize, Clone, Debug)]
494#[serde(rename_all = "camelCase")]
495pub struct Sn {
496    pub sn: String,
497    pub user_id: UserId,
498}
499/// Photo url struct
500#[derive(Serialize, Deserialize, Clone, Debug)]
501pub struct PhotoUrl {
502    pub url: String,
503}
504// Intermediate structure for deserializing API responses with the "ok" field
505#[derive(Serialize, Deserialize, Debug, Clone)]
506#[serde(untagged)]
507pub enum ApiResponseWrapper<T> {
508    PayloadWithOk {
509        ok: bool,
510        #[serde(flatten)]
511        payload: T,
512    },
513    PayloadOnly(T),
514    Error {
515        ok: bool,
516        description: String,
517    },
518}
519
520// Implementation of From for automatic conversion from ApiResponseWrapper to Result
521impl<T> std::convert::From<ApiResponseWrapper<T>> for Result<T>
522where
523    T: Default + Serialize + DeserializeOwned,
524{
525    fn from(wrapper: ApiResponseWrapper<T>) -> Self {
526        match wrapper {
527            ApiResponseWrapper::PayloadWithOk { ok, payload } => {
528                if ok {
529                    debug!("Answer is ok, payload received");
530                    Ok(payload)
531                } else {
532                    debug!("Answer is not ok, but description is not provided");
533                    Err(BotError::Api(ApiError {
534                        description: "Unspecified error".to_string(),
535                    }))
536                }
537            }
538            ApiResponseWrapper::PayloadOnly(payload) => {
539                debug!("Answer is ok, payload received");
540                Ok(payload)
541            }
542            ApiResponseWrapper::Error { ok, description } => {
543                if ok {
544                    debug!("Answer is ok, BUT error description is provided");
545                } else {
546                    debug!("Answer is NOT ok and error description is provided");
547                }
548                Err(BotError::Api(ApiError { description }))
549            }
550        }
551    }
552}
553
554/// Display trait for [`ChatId`]
555impl Display for ChatId {
556    /// Format [`ChatId`] to string
557    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
558        write!(f, "{}", self.0)
559    }
560}
561
562/// From trait implementations for [`ChatId`]
563impl std::convert::From<String> for ChatId {
564    fn from(s: String) -> Self {
565        ChatId(Cow::Owned(s))
566    }
567}
568
569impl std::convert::From<&'static str> for ChatId {
570    /// Create ChatId from static string literal (zero-allocation)
571    fn from(s: &'static str) -> Self {
572        ChatId(Cow::Borrowed(s))
573    }
574}
575
576impl std::convert::From<Cow<'static, str>> for ChatId {
577    fn from(cow: Cow<'static, str>) -> Self {
578        ChatId(cow)
579    }
580}
581
582impl AsRef<str> for ChatId {
583    fn as_ref(&self) -> &str {
584        &self.0
585    }
586}
587
588impl ChatId {
589    /// Create a new ChatId from a static string (zero-allocation)
590    ///
591    /// This is equivalent to `ChatId::from(static_str)` but more explicit.
592    pub fn from_static(s: &'static str) -> Self {
593        ChatId::from(s)
594    }
595
596    /// Create a new ChatId from a borrowed string (requires allocation)
597    ///
598    /// Use this when you have a non-static &str that needs to be owned.
599    pub fn from_borrowed_str(s: &str) -> Self {
600        ChatId(Cow::Owned(s.to_string()))
601    }
602
603    /// Create a new ChatId from an owned string
604    pub fn from_owned(s: String) -> Self {
605        ChatId(Cow::Owned(s))
606    }
607
608    /// Get the string representation as a reference
609    pub fn as_str(&self) -> &str {
610        &self.0
611    }
612
613    /// Convert to owned String
614    pub fn into_string(self) -> String {
615        self.0.into_owned()
616    }
617}
618
619/// Link basse path for API version
620impl Display for APIVersionUrl {
621    /// Format [`APIVersionUrl`] to string
622    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
623        match self {
624            APIVersionUrl::V1 => write!(f, "bot/v1/"),
625        }
626    }
627}
628/// Display trait for [`MultipartName`] enum
629impl Display for MultipartName {
630    /// Format [`MultipartName`] to string
631    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
632        match self {
633            MultipartName::FilePath(..) | MultipartName::FileContent { .. } => write!(f, "file"),
634            MultipartName::ImagePath(..) | MultipartName::ImageContent { .. } => write!(f, "image"),
635            _ => write!(f, ""),
636        }
637    }
638}
639
640/// Default values for [`Keyboard`]
641impl Default for Keyboard {
642    /// Create new [`Keyboard`] with required params
643    fn default() -> Self {
644        Self {
645            // Empty vector of [`KeyboardButton`]
646            buttons: vec![vec![]],
647        }
648    }
649}
650
651impl std::fmt::Display for UserId {
652    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653        write!(f, "{}", self.0)
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn test_chat_id_display() {
663        let id = ChatId::from("test_id");
664        assert_eq!(format!("{id}"), "test_id");
665    }
666
667    #[test]
668    fn test_chat_id_from_implementations() {
669        // Test From<&'static str> - should use Cow::Borrowed (zero allocation)
670        let static_id = ChatId::from("static_chat_id");
671        assert_eq!(static_id.as_str(), "static_chat_id");
672
673        // Test from_borrowed_str for non-static strings - should use Cow::Owned
674        let dynamic_string = format!("dynamic_{}", 123);
675        let dynamic_id = ChatId::from_borrowed_str(&dynamic_string);
676        assert_eq!(dynamic_id.as_str(), "dynamic_123");
677
678        // Test From<String> - should use Cow::Owned
679        let owned_id = ChatId::from("owned_string".to_string());
680        assert_eq!(owned_id.as_str(), "owned_string");
681
682        // Test from_static method
683        let static_method_id = ChatId::from_static("static_method");
684        assert_eq!(static_method_id.as_str(), "static_method");
685
686        // Test that static strings create borrowed Cow
687        let static_literal = ChatId::from("literal");
688        match static_literal.0 {
689            Cow::Borrowed(_) => (), // Expected for static strings
690            Cow::Owned(_) => panic!("Expected Cow::Borrowed for static string literal"),
691        }
692
693        // Test that dynamic strings create owned Cow
694        let dynamic = ChatId::from_borrowed_str("not_static");
695        match dynamic.0 {
696            Cow::Owned(_) => (), // Expected for dynamic strings
697            Cow::Borrowed(_) => panic!("Expected Cow::Owned for dynamic string"),
698        }
699    }
700
701    #[test]
702    fn test_apiversionurl_display() {
703        assert_eq!(format!("{}", APIVersionUrl::V1), "bot/v1/");
704    }
705
706    #[test]
707    fn test_multipartname_display() {
708        let f = MultipartName::FilePath("file.txt".to_string());
709        let i = MultipartName::ImagePath("img.png".to_string());
710        let n = MultipartName::None;
711        assert_eq!(format!("{f}"), "file");
712        assert_eq!(format!("{i}"), "image");
713        assert_eq!(format!("{n}"), "");
714    }
715
716    #[test]
717    fn test_keyboard_default() {
718        let kb = Keyboard::default();
719        assert_eq!(kb.buttons, vec![vec![]]);
720    }
721
722    #[test]
723    fn test_userid_display() {
724        let id = UserId("u123".to_string());
725        assert_eq!(format!("{id}"), "u123");
726    }
727
728    #[test]
729    fn test_parsemode_default_and_eq() {
730        assert_eq!(ParseMode::default(), ParseMode::HTML);
731        assert_eq!(ParseMode::HTML, ParseMode::HTML);
732        assert_ne!(ParseMode::HTML, ParseMode::MarkdownV2);
733    }
734
735    #[test]
736    fn test_buttonstyle_default_and_eq() {
737        assert_eq!(ButtonStyle::default(), ButtonStyle::Base);
738        assert_eq!(ButtonStyle::Primary, ButtonStyle::Primary);
739        assert_ne!(ButtonStyle::Primary, ButtonStyle::Attention);
740    }
741
742    #[test]
743    fn test_apiresponsewrapper_from_payloadonly() {
744        let wrap = ApiResponseWrapper::PayloadOnly(42);
745        let res: Result<i32> = wrap.into();
746        assert_eq!(res.unwrap(), 42);
747    }
748
749    #[test]
750    fn test_apiresponsewrapper_from_payloadwithok() {
751        let wrap = ApiResponseWrapper::PayloadWithOk {
752            ok: true,
753            payload: 7,
754        };
755        let res: Result<i32> = wrap.into();
756        assert_eq!(res.unwrap(), 7);
757        let wrap = ApiResponseWrapper::PayloadWithOk {
758            ok: false,
759            payload: 0,
760        };
761        let res: Result<i32> = wrap.into();
762        assert!(res.is_err());
763    }
764
765    #[test]
766    fn test_apiresponsewrapper_from_error() {
767        let wrap = ApiResponseWrapper::<i32>::Error {
768            ok: false,
769            description: "fail".to_string(),
770        };
771        let res: Result<i32> = wrap.into();
772        assert!(res.is_err());
773    }
774
775    #[test]
776    fn test_message_text_format_variants() {
777        let _ = MessageTextFormat::Plain("text".to_string());
778        let _ = MessageTextFormat::Bold("b".to_string());
779        let _ = MessageTextFormat::Italic("i".to_string());
780        let _ = MessageTextFormat::Underline("u".to_string());
781        let _ = MessageTextFormat::Strikethrough("s".to_string());
782        let _ = MessageTextFormat::Link("t".to_string(), "url".to_string());
783        let _ = MessageTextFormat::Mention(ChatId::from("cid"));
784        let _ = MessageTextFormat::Code("c".to_string());
785        let _ = MessageTextFormat::Pre("p".to_string(), Some("lang".to_string()));
786        let _ = MessageTextFormat::OrderedList(vec!["1".to_string()]);
787        let _ = MessageTextFormat::UnOrderedList(vec!["2".to_string()]);
788        let _ = MessageTextFormat::Quote("q".to_string());
789        let _ = MessageTextFormat::None;
790    }
791
792    #[test]
793    fn test_eventtype_default() {
794        assert_eq!(EventType::default(), EventType::None);
795    }
796}