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
19pub mod components;
23
24pub mod href;
28
29pub mod icon;
33
34pub mod interactions;
38
39pub mod text;
43
44#[derive(Debug, Serialize, Deserialize, Default)]
90pub struct FeishuCard {
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub config: Option<FeishuCardConfig>,
94 pub i18n_header: HashMap<FeishuCardLanguage, FeishuCardTitle>,
96 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 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 pub fn config(mut self, config: FeishuCardConfig) -> Self {
133 self.config = Some(config);
134 self
135 }
136
137 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 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#[derive(Debug, Serialize, Deserialize, Default)]
181pub struct FeishuCardConfig {
182 #[serde(skip_serializing_if = "Option::is_none")]
187 enable_forward: Option<bool>,
188 #[serde(skip_serializing_if = "Option::is_none")]
195 update_multi: Option<bool>,
196 width_mode: Option<FeishuCardWidthMode>,
201 #[serde(skip_serializing_if = "Option::is_none")]
207 use_custom_translation: Option<bool>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 enable_forward_interaction: Option<bool>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub style: Option<FeishuCardStyle>,
214}
215
216impl FeishuCardConfig {
217 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn enable_forward(mut self, enable_forward: bool) -> Self {
227 self.enable_forward = Some(enable_forward);
228 self
229 }
230
231 pub fn update_multi(mut self, update_multi: bool) -> Self {
236 self.update_multi = Some(update_multi);
237 self
238 }
239
240 pub fn width_mode(mut self, width_mode: FeishuCardWidthMode) -> Self {
245 self.width_mode = Some(width_mode);
246 self
247 }
248
249 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 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 pub fn style(mut self, style: FeishuCardStyle) -> Self {
272 self.style = Some(style);
273 self
274 }
275}
276
277#[derive(Debug, Serialize, Deserialize, Default)]
279#[serde(rename_all = "lowercase")]
280pub enum FeishuCardWidthMode {
281 #[default]
283 Default,
284 Fill,
286}
287
288#[derive(Debug, Serialize, Deserialize)]
292pub struct FeishuCardStyle {
293 #[serde(skip_serializing_if = "Option::is_none")]
296 text_size: Option<HashMap<String, CustomTextSize>>,
297 #[serde(skip_serializing_if = "Option::is_none")]
300 color: Option<HashMap<String, String>>,
301}
302
303#[derive(Debug, Serialize, Deserialize, Default, Eq, PartialEq, Hash, Clone, Copy)]
307pub enum FeishuCardLanguage {
308 #[serde(rename = "zh_cn")]
310 #[default]
311 ZhCN,
312 #[serde(rename = "en_us")]
314 EnUS,
315 #[serde(rename = "ja_jp")]
317 JaJP,
318 #[serde(rename = "zh_hk")]
320 ZhHK,
321 #[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#[derive(Debug, Serialize, Deserialize)]
344pub struct TextTag {
345 tag: String,
347 text: Option<PlainText>,
349 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 pub fn new() -> Self {
366 Self::default()
367 }
368
369 pub fn text(mut self, text: PlainText) -> Self {
374 self.text = Some(text);
375 self
376 }
377
378 pub fn color(mut self, color: &str) -> Self {
383 self.color = Some(color.to_string());
384 self
385 }
386}
387
388#[derive(Debug, Serialize, Deserialize, Default, EnumString)]
392#[serde(rename_all = "lowercase")]
393#[strum(serialize_all = "lowercase")]
394pub enum FeishuCardHeaderTemplate {
395 Blue,
397 Wathet,
399 Turquoise,
401 Green,
403 Yellow,
405 Orange,
407 Red,
409 Carmine,
411 Violet,
413 Purple,
415 Indigo,
417 Grey,
419 #[default]
421 Default,
422}
423
424#[derive(Debug, Serialize, Deserialize, Default)]
428#[serde(rename_all = "lowercase")]
429pub enum MessageCardColor {
430 Neutral,
432 #[default]
434 Blue,
435 Turquoise,
437 Lime,
439 Orange,
441 Violet,
443 Indigo,
445 Wathet,
447 Green,
449 Yellow,
451 Red,
453 Purple,
455 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 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 let _: serde_json::Value = serde_json::from_str(&content).unwrap();
847 }
848}