open_lark/
card.rs

1use std::{collections::HashMap, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "im")]
5use serde_json::json;
6use strum_macros::EnumString;
7
8use crate::card::{
9    components::{
10        content_components::{plain_text::PlainText, title::FeishuCardTitle},
11        CardElement,
12    },
13    text::CustomTextSize,
14};
15
16#[cfg(feature = "im")]
17use crate::service::im::v1::message::SendMessageTrait;
18
19/// 卡片组件模块
20///
21/// 提供各种卡片UI组件,包括内容组件、交互组件、布局组件等
22pub mod components;
23
24/// 链接处理模块
25///
26/// 处理卡片中的链接和跳转逻辑
27pub mod href;
28
29/// 图标管理模块
30///
31/// 管理卡片中使用的各种图标资源
32pub mod icon;
33
34/// 交互处理模块
35///
36/// 处理卡片的用户交互事件和回调
37pub mod interactions;
38
39/// 文本样式模块
40///
41/// 定义卡片中文本的样式和格式化
42pub mod text;
43
44/// 飞书消息卡片
45///
46/// 用于创建和发送交互式消息卡片。支持多语言、富文本、交互组件等丰富功能。
47/// 卡片可以包含文本、图片、按钮、表单等多种组件,提供丰富的用户交互体验。
48///
49/// # 主要特性
50///
51/// - 🌐 多语言支持
52/// - 🎨 丰富的UI组件
53/// - 🔄 交互式操作
54/// - 📱 响应式布局
55/// - 🔧 高度可定制
56///
57/// # 支持的组件
58///
59/// - **文本组件**: 纯文本、富文本、标题
60/// - **媒体组件**: 图片、视频
61/// - **交互组件**: 按钮、输入框、选择器
62/// - **布局组件**: 分栏、折叠面板
63/// - **数据组件**: 表格、图表
64///
65/// # 示例
66///
67/// ```no_run
68/// use open_lark::card::{FeishuCard, FeishuCardConfig};
69/// use open_lark::card::components::content_components::title::FeishuCardTitle;
70/// use open_lark::card::components::content_components::title::Title;
71/// use open_lark::card::components::CardElement;
72///
73/// // 创建简单卡片
74/// let card = FeishuCard::new()
75///     .config(
76///         FeishuCardConfig::new()
77///             .enable_forward(true)
78///             .update_multi(false)
79///     )
80///     .header("zh_cn",
81///         FeishuCardTitle::new()
82///             .title(Title::new("欢迎使用飞书卡片"))
83///     )?
84///     .elements("zh_cn", vec![
85///         // 添加卡片元素
86///     ])?;
87/// # Ok::<(), open_lark::core::error::LarkAPIError>(())
88/// ```
89#[derive(Debug, Serialize, Deserialize, Default)]
90pub struct FeishuCard {
91    /// config 用于配置卡片的全局行为,包括是否允许被转发、是否为共享卡片等。
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub config: Option<FeishuCardConfig>,
94    /// 用于配置卡片的标题
95    pub i18n_header: HashMap<FeishuCardLanguage, FeishuCardTitle>,
96    /// 卡片的多语言正文内容
97    pub i18n_elements: HashMap<FeishuCardLanguage, Vec<CardElement>>,
98}
99
100#[cfg(feature = "im")]
101impl SendMessageTrait for FeishuCard {
102    fn msg_type(&self) -> String {
103        "interactive".to_string()
104    }
105
106    fn content(&self) -> String {
107        json!(self).to_string()
108    }
109}
110
111impl FeishuCard {
112    /// 创建新的飞书卡片
113    ///
114    /// 默认使用中文语言环境,创建包含默认标题和空元素列表的卡片。
115    pub fn new() -> Self {
116        let lng = FeishuCardLanguage::ZhCN;
117        let mut header = HashMap::new();
118        header.insert(lng, FeishuCardTitle::default());
119        let mut elements = HashMap::new();
120        elements.insert(lng, vec![]);
121        Self {
122            config: None,
123            i18n_header: header,
124            i18n_elements: elements,
125        }
126    }
127
128    /// 设置卡片全局配置
129    ///
130    /// # 参数
131    /// * `config` - 卡片配置对象
132    pub fn config(mut self, config: FeishuCardConfig) -> Self {
133        self.config = Some(config);
134        self
135    }
136
137    /// 设置卡片标题
138    ///
139    /// # 参数  
140    /// * `lng` - 语言代码 (如 "zh_cn", "en_us")
141    /// * `header` - 卡片标题对象
142    pub fn header(
143        mut self,
144        lng: &str,
145        header: FeishuCardTitle,
146    ) -> Result<Self, crate::core::error::LarkAPIError> {
147        let language: FeishuCardLanguage = lng.parse().map_err(|e| {
148            crate::core::error::LarkAPIError::illegal_param(format!(
149                "unknown language '{lng}': {e}"
150            ))
151        })?;
152        let origin_header = self.i18n_header.entry(language).or_default();
153        *origin_header = header;
154
155        Ok(self)
156    }
157
158    /// 添加卡片组件
159    ///
160    /// # 参数
161    /// * `lng` - 语言代码 (如 "zh_cn", "en_us")
162    /// * `elements` - 卡片组件列表
163    pub fn elements(
164        mut self,
165        lng: &str,
166        elements: Vec<CardElement>,
167    ) -> Result<Self, crate::core::error::LarkAPIError> {
168        let language: FeishuCardLanguage = lng.parse().map_err(|e| {
169            crate::core::error::LarkAPIError::illegal_param(format!(
170                "unknown language '{lng}': {e}"
171            ))
172        })?;
173        let self_elements = self.i18n_elements.entry(language).or_default();
174        self_elements.extend(elements);
175        Ok(self)
176    }
177}
178
179/// 卡片全局行为设置
180#[derive(Debug, Serialize, Deserialize, Default)]
181pub struct FeishuCardConfig {
182    /// 是否允许转发卡片。取值:
183    ///
184    /// - true:允许
185    /// - false:不允许 默认值为 true,该字段要求飞书客户端的版本为 V3.31.0 及以上。
186    #[serde(skip_serializing_if = "Option::is_none")]
187    enable_forward: Option<bool>,
188    /// 是否为共享卡片。取值:
189    ///
190    /// - true:是共享卡片,更新卡片的内容对所有收到这张卡片的人员可见。
191    /// - false:非共享卡片,即独享卡片,仅操作用户可见卡片的更新内容。
192    ///
193    /// 默认值为 false。
194    #[serde(skip_serializing_if = "Option::is_none")]
195    update_multi: Option<bool>,
196    /// 卡片宽度模式。取值:
197    ///
198    /// - default:默认宽度。PC 端宽版、iPad 端上的宽度上限为 600px。
199    /// - fill:自适应屏幕宽度
200    width_mode: Option<FeishuCardWidthMode>,
201    /// 是否使用自定义翻译数据。取值:
202    ///
203    /// - true:在用户点击消息翻译后,使用 i18n 对应的目标语种作为翻译结果。若 i18n
204    ///   取不到,则使用当前内容请求飞书的机器翻译。
205    /// - false:不使用自定义翻译数据,直接请求飞书的机器翻译。
206    #[serde(skip_serializing_if = "Option::is_none")]
207    use_custom_translation: Option<bool>,
208    /// 转发的卡片是否仍然支持回传交互。
209    #[serde(skip_serializing_if = "Option::is_none")]
210    enable_forward_interaction: Option<bool>,
211    ///  添加自定义字号和颜色。可应用于组件的 JSON 数据中,设置字号和颜色属性。
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub style: Option<FeishuCardStyle>,
214}
215
216impl FeishuCardConfig {
217    /// 创建新的卡片配置
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    /// 设置是否允许转发卡片
223    ///
224    /// # 参数
225    /// * `enable_forward` - true为允许转发,false为不允许
226    pub fn enable_forward(mut self, enable_forward: bool) -> Self {
227        self.enable_forward = Some(enable_forward);
228        self
229    }
230
231    /// 设置是否为共享卡片
232    ///
233    /// # 参数  
234    /// * `update_multi` - true为共享卡片,false为独享卡片
235    pub fn update_multi(mut self, update_multi: bool) -> Self {
236        self.update_multi = Some(update_multi);
237        self
238    }
239
240    /// 设置卡片宽度模式
241    ///
242    /// # 参数
243    /// * `width_mode` - 宽度模式:默认宽度或自适应屏幕宽度
244    pub fn width_mode(mut self, width_mode: FeishuCardWidthMode) -> Self {
245        self.width_mode = Some(width_mode);
246        self
247    }
248
249    /// 设置是否使用自定义翻译数据
250    ///
251    /// # 参数
252    /// * `use_custom_translation` - true为使用自定义翻译,false为使用机器翻译
253    pub fn use_custom_translation(mut self, use_custom_translation: bool) -> Self {
254        self.use_custom_translation = Some(use_custom_translation);
255        self
256    }
257
258    /// 设置转发的卡片是否仍然支持回传交互
259    ///
260    /// # 参数
261    /// * `enable_forward_interaction` - true为支持交互,false为不支持
262    pub fn enable_forward_interaction(mut self, enable_forward_interaction: bool) -> Self {
263        self.enable_forward_interaction = Some(enable_forward_interaction);
264        self
265    }
266
267    /// 设置卡片样式
268    ///
269    /// # 参数
270    /// * `style` - 卡片样式配置,包括字号和颜色
271    pub fn style(mut self, style: FeishuCardStyle) -> Self {
272        self.style = Some(style);
273        self
274    }
275}
276
277/// 卡片宽度模式
278#[derive(Debug, Serialize, Deserialize, Default)]
279#[serde(rename_all = "lowercase")]
280pub enum FeishuCardWidthMode {
281    /// 默认宽度。PC 端宽版、iPad 端上的宽度上限为 600px。
282    #[default]
283    Default,
284    /// 自适应屏幕宽度
285    Fill,
286}
287
288/// 卡片样式配置
289///
290/// 用于定义卡片的字号和颜色样式,支持为不同主题和设备定制样式
291#[derive(Debug, Serialize, Deserialize)]
292pub struct FeishuCardStyle {
293    /// 分别为移动端和桌面端添加自定义字号。用于在普通文本组件和富文本组件 JSON
294    /// 中设置字号属性。支持添加多个自定义字号对象。
295    #[serde(skip_serializing_if = "Option::is_none")]
296    text_size: Option<HashMap<String, CustomTextSize>>,
297    /// 分别为飞书客户端浅色主题和深色主题添加 RGBA 语法。用于在组件 JSON
298    /// 中设置颜色属性。支持添加多个自定义颜色对象。
299    #[serde(skip_serializing_if = "Option::is_none")]
300    color: Option<HashMap<String, String>>,
301}
302
303/// 飞书卡片支持的语言类型
304///
305/// 用于卡片的多语言支持,可为不同语言环境提供相应的内容
306#[derive(Debug, Serialize, Deserialize, Default, Eq, PartialEq, Hash, Clone, Copy)]
307pub enum FeishuCardLanguage {
308    /// 简体中文
309    #[serde(rename = "zh_cn")]
310    #[default]
311    ZhCN,
312    /// 英文(美国)
313    #[serde(rename = "en_us")]
314    EnUS,
315    /// 日文
316    #[serde(rename = "ja_jp")]
317    JaJP,
318    /// 繁体中文(香港)
319    #[serde(rename = "zh_hk")]
320    ZhHK,
321    /// 繁体中文(台湾)
322    #[serde(rename = "zh_tw")]
323    ZhTW,
324}
325
326impl FromStr for FeishuCardLanguage {
327    type Err = String;
328
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        match s.to_ascii_lowercase().as_str() {
331            "zh_cn" => Ok(FeishuCardLanguage::ZhCN),
332            "en_us" => Ok(FeishuCardLanguage::EnUS),
333            "ja_jp" => Ok(FeishuCardLanguage::JaJP),
334            "zh_hk" => Ok(FeishuCardLanguage::ZhHK),
335            "zh_tw" => Ok(FeishuCardLanguage::ZhTW),
336            _ => Err(format!("unknown language: {s}")),
337        }
338    }
339}
340
341/// 标题的标签属性。最多可配置 3 个标签内容,如果配置的标签数量超过 3 个,则取前 3
342/// 个标签进行展示。标签展示顺序与数组顺序一致。
343#[derive(Debug, Serialize, Deserialize)]
344pub struct TextTag {
345    /// 标题标签的标识。固定取值:text_tag
346    tag: String,
347    /// 标题标签的内容。基于文本组件的 plain_text 模式定义内容。
348    text: Option<PlainText>,
349    /// 标题标签的颜色,默认为蓝色(blue)
350    color: Option<String>,
351}
352
353impl Default for TextTag {
354    fn default() -> Self {
355        TextTag {
356            tag: "text_tag".to_string(),
357            text: None,
358            color: None,
359        }
360    }
361}
362
363impl TextTag {
364    /// 创建新的文本标签
365    pub fn new() -> Self {
366        Self::default()
367    }
368
369    /// 设置标签文本内容
370    ///
371    /// # 参数
372    /// * `text` - 标签的文本内容
373    pub fn text(mut self, text: PlainText) -> Self {
374        self.text = Some(text);
375        self
376    }
377
378    /// 设置标签颜色
379    ///
380    /// # 参数
381    /// * `color` - 标签的颜色值
382    pub fn color(mut self, color: &str) -> Self {
383        self.color = Some(color.to_string());
384        self
385    }
386}
387
388/// 标题样式表
389///
390/// 定义飞书卡片标题的颜色主题模板
391#[derive(Debug, Serialize, Deserialize, Default, EnumString)]
392#[serde(rename_all = "lowercase")]
393#[strum(serialize_all = "lowercase")]
394pub enum FeishuCardHeaderTemplate {
395    /// 蓝色主题
396    Blue,
397    /// 浅蓝色主题
398    Wathet,
399    /// 青色主题
400    Turquoise,
401    /// 绿色主题
402    Green,
403    /// 黄色主题
404    Yellow,
405    /// 橙色主题
406    Orange,
407    /// 红色主题
408    Red,
409    /// 胭脂红主题
410    Carmine,
411    /// 紫罗兰主题
412    Violet,
413    /// 紫色主题
414    Purple,
415    /// 靛蓝色主题
416    Indigo,
417    /// 灰色主题
418    Grey,
419    /// 默认主题
420    #[default]
421    Default,
422}
423
424/// 消息卡片颜色主题
425///
426/// 定义消息卡片的颜色主题选项
427#[derive(Debug, Serialize, Deserialize, Default)]
428#[serde(rename_all = "lowercase")]
429pub enum MessageCardColor {
430    /// 中性色主题
431    Neutral,
432    /// 蓝色主题(默认)
433    #[default]
434    Blue,
435    /// 青色主题
436    Turquoise,
437    /// 青柠色主题
438    Lime,
439    /// 橙色主题
440    Orange,
441    /// 紫罗兰主题
442    Violet,
443    /// 靛蓝色主题
444    Indigo,
445    /// 浅蓝色主题
446    Wathet,
447    /// 绿色主题
448    Green,
449    /// 黄色主题
450    Yellow,
451    /// 红色主题
452    Red,
453    /// 紫色主题
454    Purple,
455    /// 胭脂红主题
456    Carmine,
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use crate::card::components::content_components::{
463        plain_text::PlainText,
464        title::{FeishuCardTitle, Title},
465    };
466
467    #[test]
468    fn test_feishu_card_new() {
469        let card = FeishuCard::new();
470
471        assert!(card.config.is_none());
472        assert_eq!(card.i18n_header.len(), 1);
473        assert!(card.i18n_header.contains_key(&FeishuCardLanguage::ZhCN));
474        assert_eq!(card.i18n_elements.len(), 1);
475        assert!(card.i18n_elements.contains_key(&FeishuCardLanguage::ZhCN));
476        assert!(card.i18n_elements[&FeishuCardLanguage::ZhCN].is_empty());
477    }
478
479    #[test]
480    fn test_feishu_card_config() {
481        let config = FeishuCardConfig::new()
482            .enable_forward(false)
483            .update_multi(true);
484
485        let card = FeishuCard::new().config(config);
486
487        assert!(card.config.is_some());
488        let card_config = card.config.unwrap();
489        assert_eq!(card_config.enable_forward, Some(false));
490        assert_eq!(card_config.update_multi, Some(true));
491    }
492
493    #[test]
494    fn test_feishu_card_header_valid_language() {
495        let title = FeishuCardTitle::new().title(Title::new("Test Title"));
496        let result = FeishuCard::new().header("en_us", title);
497
498        assert!(result.is_ok());
499        let card = result.unwrap();
500        assert!(card.i18n_header.contains_key(&FeishuCardLanguage::EnUS));
501    }
502
503    #[test]
504    fn test_feishu_card_header_invalid_language() {
505        let title = FeishuCardTitle::new().title(Title::new("Test Title"));
506        let result = FeishuCard::new().header("invalid_lang", title);
507
508        assert!(result.is_err());
509        assert!(result
510            .unwrap_err()
511            .to_string()
512            .contains("unknown language 'invalid_lang'"));
513    }
514
515    #[test]
516    fn test_feishu_card_elements_valid_language() {
517        let elements = vec![];
518        let result = FeishuCard::new().elements("ja_jp", elements);
519
520        assert!(result.is_ok());
521        let card = result.unwrap();
522        assert!(card.i18n_elements.contains_key(&FeishuCardLanguage::JaJP));
523    }
524
525    #[test]
526    fn test_feishu_card_elements_invalid_language() {
527        let elements = vec![];
528        let result = FeishuCard::new().elements("unknown_lang", elements);
529
530        assert!(result.is_err());
531        assert!(result
532            .unwrap_err()
533            .to_string()
534            .contains("unknown language 'unknown_lang'"));
535    }
536
537    #[test]
538    fn test_feishu_card_config_new() {
539        let config = FeishuCardConfig::new();
540
541        assert!(config.enable_forward.is_none());
542        assert!(config.update_multi.is_none());
543        assert!(config.width_mode.is_none());
544        assert!(config.use_custom_translation.is_none());
545        assert!(config.enable_forward_interaction.is_none());
546        assert!(config.style.is_none());
547    }
548
549    #[test]
550    fn test_feishu_card_config_enable_forward() {
551        let config = FeishuCardConfig::new().enable_forward(true);
552        assert_eq!(config.enable_forward, Some(true));
553    }
554
555    #[test]
556    fn test_feishu_card_config_update_multi() {
557        let config = FeishuCardConfig::new().update_multi(false);
558        assert_eq!(config.update_multi, Some(false));
559    }
560
561    #[test]
562    fn test_feishu_card_config_width_mode() {
563        let config = FeishuCardConfig::new().width_mode(FeishuCardWidthMode::Fill);
564        assert!(matches!(config.width_mode, Some(FeishuCardWidthMode::Fill)));
565    }
566
567    #[test]
568    fn test_feishu_card_config_use_custom_translation() {
569        let config = FeishuCardConfig::new().use_custom_translation(true);
570        assert_eq!(config.use_custom_translation, Some(true));
571    }
572
573    #[test]
574    fn test_feishu_card_config_enable_forward_interaction() {
575        let config = FeishuCardConfig::new().enable_forward_interaction(false);
576        assert_eq!(config.enable_forward_interaction, Some(false));
577    }
578
579    #[test]
580    fn test_feishu_card_config_style() {
581        let style = FeishuCardStyle {
582            text_size: None,
583            color: None,
584        };
585        let config = FeishuCardConfig::new().style(style);
586        assert!(config.style.is_some());
587    }
588
589    #[test]
590    fn test_feishu_card_config_builder_pattern() {
591        let config = FeishuCardConfig::new()
592            .enable_forward(true)
593            .update_multi(false)
594            .width_mode(FeishuCardWidthMode::Default)
595            .use_custom_translation(true)
596            .enable_forward_interaction(false);
597
598        assert_eq!(config.enable_forward, Some(true));
599        assert_eq!(config.update_multi, Some(false));
600        assert!(matches!(
601            config.width_mode,
602            Some(FeishuCardWidthMode::Default)
603        ));
604        assert_eq!(config.use_custom_translation, Some(true));
605        assert_eq!(config.enable_forward_interaction, Some(false));
606    }
607
608    #[test]
609    fn test_feishu_card_width_mode_default() {
610        let mode = FeishuCardWidthMode::default();
611        assert!(matches!(mode, FeishuCardWidthMode::Default));
612    }
613
614    #[test]
615    fn test_feishu_card_width_mode_serde() {
616        let mode_default = FeishuCardWidthMode::Default;
617        let mode_fill = FeishuCardWidthMode::Fill;
618
619        let json_default = serde_json::to_string(&mode_default).unwrap();
620        let json_fill = serde_json::to_string(&mode_fill).unwrap();
621
622        assert_eq!(json_default, "\"default\"");
623        assert_eq!(json_fill, "\"fill\"");
624    }
625
626    #[test]
627    fn test_feishu_card_language_from_str() {
628        assert_eq!(
629            "zh_cn".parse::<FeishuCardLanguage>().unwrap(),
630            FeishuCardLanguage::ZhCN
631        );
632        assert_eq!(
633            "en_us".parse::<FeishuCardLanguage>().unwrap(),
634            FeishuCardLanguage::EnUS
635        );
636        assert_eq!(
637            "ja_jp".parse::<FeishuCardLanguage>().unwrap(),
638            FeishuCardLanguage::JaJP
639        );
640        assert_eq!(
641            "zh_hk".parse::<FeishuCardLanguage>().unwrap(),
642            FeishuCardLanguage::ZhHK
643        );
644        assert_eq!(
645            "zh_tw".parse::<FeishuCardLanguage>().unwrap(),
646            FeishuCardLanguage::ZhTW
647        );
648    }
649
650    #[test]
651    fn test_feishu_card_language_from_str_case_insensitive() {
652        assert_eq!(
653            "ZH_CN".parse::<FeishuCardLanguage>().unwrap(),
654            FeishuCardLanguage::ZhCN
655        );
656        assert_eq!(
657            "En_Us".parse::<FeishuCardLanguage>().unwrap(),
658            FeishuCardLanguage::EnUS
659        );
660    }
661
662    #[test]
663    fn test_feishu_card_language_from_str_invalid() {
664        let result = "invalid_lang".parse::<FeishuCardLanguage>();
665        assert!(result.is_err());
666        assert_eq!(result.unwrap_err(), "unknown language: invalid_lang");
667    }
668
669    #[test]
670    fn test_feishu_card_language_default() {
671        let lang = FeishuCardLanguage::default();
672        assert_eq!(lang, FeishuCardLanguage::ZhCN);
673    }
674
675    #[test]
676    fn test_feishu_card_language_serde() {
677        let lang = FeishuCardLanguage::EnUS;
678        let json = serde_json::to_string(&lang).unwrap();
679        assert_eq!(json, "\"en_us\"");
680
681        let deserialized: FeishuCardLanguage = serde_json::from_str(&json).unwrap();
682        assert_eq!(deserialized, FeishuCardLanguage::EnUS);
683    }
684
685    #[test]
686    fn test_text_tag_new() {
687        let tag = TextTag::new();
688        assert_eq!(tag.tag, "text_tag");
689        assert!(tag.text.is_none());
690        assert!(tag.color.is_none());
691    }
692
693    #[test]
694    fn test_text_tag_text() {
695        let plain_text = PlainText::text("Test content");
696        let tag = TextTag::new().text(plain_text);
697        assert!(tag.text.is_some());
698    }
699
700    #[test]
701    fn test_text_tag_color() {
702        let tag = TextTag::new().color("red");
703        assert_eq!(tag.color, Some("red".to_string()));
704    }
705
706    #[test]
707    fn test_text_tag_builder_pattern() {
708        let plain_text = PlainText::text("Test content");
709        let tag = TextTag::new().text(plain_text).color("blue");
710
711        assert_eq!(tag.tag, "text_tag");
712        assert!(tag.text.is_some());
713        assert_eq!(tag.color, Some("blue".to_string()));
714    }
715
716    #[test]
717    fn test_text_tag_default() {
718        let tag = TextTag::default();
719        assert_eq!(tag.tag, "text_tag");
720        assert!(tag.text.is_none());
721        assert!(tag.color.is_none());
722    }
723
724    #[test]
725    fn test_feishu_card_header_template_default() {
726        let template = FeishuCardHeaderTemplate::default();
727        assert!(matches!(template, FeishuCardHeaderTemplate::Default));
728    }
729
730    #[test]
731    fn test_feishu_card_header_template_from_str() {
732        assert!(matches!(
733            "blue".parse::<FeishuCardHeaderTemplate>().unwrap(),
734            FeishuCardHeaderTemplate::Blue
735        ));
736        assert!(matches!(
737            "red".parse::<FeishuCardHeaderTemplate>().unwrap(),
738            FeishuCardHeaderTemplate::Red
739        ));
740        assert!(matches!(
741            "green".parse::<FeishuCardHeaderTemplate>().unwrap(),
742            FeishuCardHeaderTemplate::Green
743        ));
744    }
745
746    #[test]
747    fn test_feishu_card_header_template_serde() {
748        let template = FeishuCardHeaderTemplate::Blue;
749        let json = serde_json::to_string(&template).unwrap();
750        assert_eq!(json, "\"blue\"");
751
752        let deserialized: FeishuCardHeaderTemplate = serde_json::from_str(&json).unwrap();
753        assert!(matches!(deserialized, FeishuCardHeaderTemplate::Blue));
754    }
755
756    #[test]
757    fn test_message_card_color_default() {
758        let color = MessageCardColor::default();
759        assert!(matches!(color, MessageCardColor::Blue));
760    }
761
762    #[test]
763    fn test_message_card_color_serde() {
764        let color = MessageCardColor::Green;
765        let json = serde_json::to_string(&color).unwrap();
766        assert_eq!(json, "\"green\"");
767
768        let deserialized: MessageCardColor = serde_json::from_str(&json).unwrap();
769        assert!(matches!(deserialized, MessageCardColor::Green));
770    }
771
772    #[test]
773    fn test_feishu_card_serde() {
774        let card = FeishuCard::new();
775        let json = serde_json::to_string(&card).unwrap();
776
777        // Should be able to serialize and deserialize
778        let deserialized: FeishuCard = serde_json::from_str(&json).unwrap();
779        assert_eq!(deserialized.i18n_header.len(), 1);
780        assert_eq!(deserialized.i18n_elements.len(), 1);
781    }
782
783    #[test]
784    fn test_feishu_card_config_serde() {
785        let config = FeishuCardConfig::new()
786            .enable_forward(true)
787            .update_multi(false);
788
789        let json = serde_json::to_string(&config).unwrap();
790        let deserialized: FeishuCardConfig = serde_json::from_str(&json).unwrap();
791
792        assert_eq!(deserialized.enable_forward, Some(true));
793        assert_eq!(deserialized.update_multi, Some(false));
794    }
795
796    #[test]
797    fn test_feishu_card_complete_builder() {
798        let config = FeishuCardConfig::new()
799            .enable_forward(true)
800            .update_multi(false)
801            .width_mode(FeishuCardWidthMode::Fill);
802
803        let title = FeishuCardTitle::new().title(Title::new("Test Card"));
804
805        let result = FeishuCard::new()
806            .config(config)
807            .header("en_us", title)
808            .and_then(|card| card.elements("en_us", vec![]));
809
810        assert!(result.is_ok());
811        let card = result.unwrap();
812        assert!(card.config.is_some());
813        assert!(card.i18n_header.contains_key(&FeishuCardLanguage::EnUS));
814        assert!(card.i18n_elements.contains_key(&FeishuCardLanguage::EnUS));
815    }
816
817    #[test]
818    fn test_feishu_card_multiple_languages() {
819        let zh_title = FeishuCardTitle::new().title(Title::new("中文标题"));
820        let en_title = FeishuCardTitle::new().title(Title::new("English Title"));
821
822        let result = FeishuCard::new()
823            .header("zh_cn", zh_title)
824            .and_then(|card| card.header("en_us", en_title))
825            .and_then(|card| card.elements("zh_cn", vec![]))
826            .and_then(|card| card.elements("en_us", vec![]));
827
828        assert!(result.is_ok());
829        let card = result.unwrap();
830        assert_eq!(card.i18n_header.len(), 2);
831        assert_eq!(card.i18n_elements.len(), 2);
832        assert!(card.i18n_header.contains_key(&FeishuCardLanguage::ZhCN));
833        assert!(card.i18n_header.contains_key(&FeishuCardLanguage::EnUS));
834    }
835
836    #[cfg(feature = "im")]
837    #[test]
838    fn test_feishu_card_send_message_trait() {
839        let card = FeishuCard::new();
840        assert_eq!(card.msg_type(), "interactive");
841
842        let content = card.content();
843        assert!(!content.is_empty());
844
845        // Should be valid JSON
846        let _: serde_json::Value = serde_json::from_str(&content).unwrap();
847    }
848}