Skip to content

Commit e00f648

Browse files
authored
feat(models): add proper typing for rich text block and rich text input element (#352)
* feat(models): add proper typing for rich text block and rich text input element Closes #321, closes #327 - Add SlackRichTextBlock replacing the serde_json::Value placeholder - Add SlackRichTextElement enum (section, list, preformatted, quote) - Add SlackRichTextInlineElement enum (text, link, user, channel, usergroup, emoji, date, broadcast, color) - Add SlackRichTextStyle for inline formatting options - Add SlackBlockRichTextInputElement and wire it into SlackInputBlockElement - Add SlackRichTextListStyle and SlackRichTextBroadcastRange enums * fix(rich-text): use SlackEmojiName, i64 timestamp, add deserialization tests - SlackRichTextEmoji.name: String -> SlackEmojiName to reuse existing type - SlackRichTextDate.timestamp: u64 -> i64 to match Unix epoch semantics - Add JSON fixture and two tests: full deserialization + round-trip * style: cargo fmt
1 parent ea9ecba commit e00f648

2 files changed

Lines changed: 340 additions & 2 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"type": "rich_text",
3+
"block_id": "test_block",
4+
"elements": [
5+
{
6+
"type": "rich_text_section",
7+
"elements": [
8+
{
9+
"type": "text",
10+
"text": "Hello ",
11+
"style": { "bold": true }
12+
},
13+
{
14+
"type": "user",
15+
"user_id": "U123ABC456"
16+
},
17+
{
18+
"type": "text",
19+
"text": "! Check out "
20+
},
21+
{
22+
"type": "link",
23+
"url": "https://example.com",
24+
"text": "this link",
25+
"style": { "italic": true }
26+
},
27+
{
28+
"type": "emoji",
29+
"name": "wave"
30+
},
31+
{
32+
"type": "channel",
33+
"channel_id": "C123ABC456"
34+
},
35+
{
36+
"type": "broadcast",
37+
"range": "here"
38+
}
39+
]
40+
},
41+
{
42+
"type": "rich_text_list",
43+
"style": "bullet",
44+
"indent": 0,
45+
"elements": [
46+
{
47+
"type": "rich_text_section",
48+
"elements": [{ "type": "text", "text": "Item one" }]
49+
},
50+
{
51+
"type": "rich_text_section",
52+
"elements": [{ "type": "text", "text": "Item two" }]
53+
}
54+
]
55+
},
56+
{
57+
"type": "rich_text_preformatted",
58+
"elements": [
59+
{ "type": "text", "text": "fn main() {}\n" }
60+
],
61+
"border": 1
62+
},
63+
{
64+
"type": "rich_text_quote",
65+
"elements": [
66+
{ "type": "text", "text": "A wise quote", "style": { "italic": true } }
67+
]
68+
}
69+
]
70+
}

src/models/blocks/kit.rs

Lines changed: 270 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ pub enum SlackBlock {
3434
#[serde(rename = "markdown")]
3535
Markdown(SlackMarkdownBlock),
3636

37-
// This block is still undocumented, so we don't define any structure yet we can return it back,
3837
#[serde(rename = "rich_text")]
39-
RichText(serde_json::Value),
38+
RichText(SlackRichTextBlock),
4039
#[serde(rename = "share_shortcut")]
4140
ShareShortcut(serde_json::Value),
4241
#[serde(rename = "event")]
@@ -291,6 +290,8 @@ pub enum SlackInputBlockElement {
291290
Checkboxes(SlackBlockCheckboxesElement),
292291
#[serde(rename = "email_text_input")]
293292
EmailInput(SlackBlockEmailInputElement),
293+
#[serde(rename = "rich_text_input")]
294+
RichTextInput(SlackBlockRichTextInputElement),
294295
}
295296

296297
#[skip_serializing_none]
@@ -1070,6 +1071,202 @@ impl From<SlackMarkdownBlock> for SlackBlock {
10701071
}
10711072
}
10721073

1074+
/**
1075+
* https://api.slack.com/reference/block-kit/blocks#rich_text
1076+
*/
1077+
#[skip_serializing_none]
1078+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1079+
pub struct SlackRichTextBlock {
1080+
pub block_id: Option<SlackBlockId>,
1081+
pub elements: Vec<SlackRichTextElement>,
1082+
}
1083+
1084+
impl From<SlackRichTextBlock> for SlackBlock {
1085+
fn from(block: SlackRichTextBlock) -> Self {
1086+
SlackBlock::RichText(block)
1087+
}
1088+
}
1089+
1090+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1091+
#[serde(tag = "type")]
1092+
pub enum SlackRichTextElement {
1093+
#[serde(rename = "rich_text_section")]
1094+
Section(SlackRichTextSection),
1095+
#[serde(rename = "rich_text_list")]
1096+
List(SlackRichTextList),
1097+
#[serde(rename = "rich_text_preformatted")]
1098+
Preformatted(SlackRichTextPreformatted),
1099+
#[serde(rename = "rich_text_quote")]
1100+
Quote(SlackRichTextQuote),
1101+
}
1102+
1103+
#[skip_serializing_none]
1104+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1105+
pub struct SlackRichTextSection {
1106+
pub elements: Vec<SlackRichTextInlineElement>,
1107+
}
1108+
1109+
#[skip_serializing_none]
1110+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1111+
pub struct SlackRichTextList {
1112+
pub style: SlackRichTextListStyle,
1113+
pub elements: Vec<SlackRichTextSection>,
1114+
pub indent: Option<u64>,
1115+
pub offset: Option<u64>,
1116+
pub border: Option<u64>,
1117+
}
1118+
1119+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1120+
#[serde(rename_all = "snake_case")]
1121+
pub enum SlackRichTextListStyle {
1122+
Bullet,
1123+
Ordered,
1124+
}
1125+
1126+
#[skip_serializing_none]
1127+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1128+
pub struct SlackRichTextPreformatted {
1129+
pub elements: Vec<SlackRichTextInlineElement>,
1130+
pub border: Option<u64>,
1131+
pub language: Option<String>,
1132+
}
1133+
1134+
#[skip_serializing_none]
1135+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1136+
pub struct SlackRichTextQuote {
1137+
pub elements: Vec<SlackRichTextInlineElement>,
1138+
pub border: Option<u64>,
1139+
}
1140+
1141+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1142+
#[serde(tag = "type")]
1143+
pub enum SlackRichTextInlineElement {
1144+
#[serde(rename = "text")]
1145+
Text(SlackRichTextText),
1146+
#[serde(rename = "link")]
1147+
Link(SlackRichTextLink),
1148+
#[serde(rename = "user")]
1149+
User(SlackRichTextUser),
1150+
#[serde(rename = "channel")]
1151+
Channel(SlackRichTextChannel),
1152+
#[serde(rename = "usergroup")]
1153+
UserGroup(SlackRichTextUserGroup),
1154+
#[serde(rename = "emoji")]
1155+
Emoji(SlackRichTextEmoji),
1156+
#[serde(rename = "date")]
1157+
Date(SlackRichTextDate),
1158+
#[serde(rename = "broadcast")]
1159+
Broadcast(SlackRichTextBroadcast),
1160+
#[serde(rename = "color")]
1161+
Color(SlackRichTextColor),
1162+
}
1163+
1164+
#[skip_serializing_none]
1165+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1166+
pub struct SlackRichTextStyle {
1167+
pub bold: Option<bool>,
1168+
pub italic: Option<bool>,
1169+
pub strike: Option<bool>,
1170+
pub code: Option<bool>,
1171+
pub underline: Option<bool>,
1172+
pub highlight: Option<bool>,
1173+
pub client_highlight: Option<bool>,
1174+
pub unlink: Option<bool>,
1175+
}
1176+
1177+
#[skip_serializing_none]
1178+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1179+
pub struct SlackRichTextText {
1180+
pub text: String,
1181+
pub style: Option<SlackRichTextStyle>,
1182+
}
1183+
1184+
#[skip_serializing_none]
1185+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1186+
pub struct SlackRichTextLink {
1187+
pub url: String,
1188+
pub text: Option<String>,
1189+
#[serde(rename = "unsafe")]
1190+
pub unsafe_: Option<bool>,
1191+
pub style: Option<SlackRichTextStyle>,
1192+
}
1193+
1194+
#[skip_serializing_none]
1195+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1196+
pub struct SlackRichTextUser {
1197+
pub user_id: SlackUserId,
1198+
pub style: Option<SlackRichTextStyle>,
1199+
}
1200+
1201+
#[skip_serializing_none]
1202+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1203+
pub struct SlackRichTextChannel {
1204+
pub channel_id: SlackChannelId,
1205+
pub style: Option<SlackRichTextStyle>,
1206+
}
1207+
1208+
#[skip_serializing_none]
1209+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1210+
pub struct SlackRichTextUserGroup {
1211+
pub usergroup_id: SlackUserGroupId,
1212+
pub style: Option<SlackRichTextStyle>,
1213+
}
1214+
1215+
#[skip_serializing_none]
1216+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1217+
pub struct SlackRichTextEmoji {
1218+
pub name: SlackEmojiName,
1219+
pub unicode: Option<String>,
1220+
}
1221+
1222+
#[skip_serializing_none]
1223+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1224+
pub struct SlackRichTextDate {
1225+
pub timestamp: i64,
1226+
pub format: String,
1227+
pub fallback: Option<String>,
1228+
pub style: Option<SlackRichTextStyle>,
1229+
}
1230+
1231+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1232+
#[serde(rename_all = "snake_case")]
1233+
pub enum SlackRichTextBroadcastRange {
1234+
Here,
1235+
Channel,
1236+
Everyone,
1237+
}
1238+
1239+
#[skip_serializing_none]
1240+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1241+
pub struct SlackRichTextBroadcast {
1242+
pub range: SlackRichTextBroadcastRange,
1243+
pub style: Option<SlackRichTextStyle>,
1244+
}
1245+
1246+
#[skip_serializing_none]
1247+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1248+
pub struct SlackRichTextColor {
1249+
pub value: String,
1250+
}
1251+
1252+
/**
1253+
* https://api.slack.com/reference/block-kit/block-elements#rich_text_input
1254+
*/
1255+
#[skip_serializing_none]
1256+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1257+
pub struct SlackBlockRichTextInputElement {
1258+
pub action_id: SlackActionId,
1259+
pub initial_value: Option<SlackRichTextBlock>,
1260+
pub focus_on_load: Option<bool>,
1261+
pub placeholder: Option<SlackBlockPlainTextOnly>,
1262+
}
1263+
1264+
impl From<SlackBlockRichTextInputElement> for SlackInputBlockElement {
1265+
fn from(element: SlackBlockRichTextInputElement) -> Self {
1266+
SlackInputBlockElement::RichTextInput(element)
1267+
}
1268+
}
1269+
10731270
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
10741271
#[serde(untagged)]
10751272
pub enum SlackImageUrlOrFile {
@@ -1222,4 +1419,75 @@ mod test {
12221419
}
12231420
Ok(())
12241421
}
1422+
1423+
#[test]
1424+
fn test_rich_text_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
1425+
let payload = include_str!("./fixtures/slack_rich_text_block.json");
1426+
let block: SlackBlock = serde_json::from_str(payload)?;
1427+
1428+
let rich = match block {
1429+
SlackBlock::RichText(r) => r,
1430+
_ => panic!("Expected a RichText block"),
1431+
};
1432+
1433+
assert_eq!(rich.block_id, Some(SlackBlockId("test_block".into())));
1434+
assert_eq!(rich.elements.len(), 4);
1435+
1436+
// section
1437+
let section = match &rich.elements[0] {
1438+
SlackRichTextElement::Section(s) => s,
1439+
_ => panic!("Expected a Section element"),
1440+
};
1441+
assert_eq!(section.elements.len(), 7);
1442+
1443+
// bold text
1444+
let text = match &section.elements[0] {
1445+
SlackRichTextInlineElement::Text(t) => t,
1446+
_ => panic!("Expected a Text element"),
1447+
};
1448+
assert_eq!(text.text, "Hello ");
1449+
assert_eq!(text.style.as_ref().and_then(|s| s.bold), Some(true));
1450+
1451+
// user
1452+
assert!(matches!(
1453+
&section.elements[1],
1454+
SlackRichTextInlineElement::User(_)
1455+
));
1456+
1457+
// emoji — name should deserialize as SlackEmojiName
1458+
let emoji = match &section.elements[4] {
1459+
SlackRichTextInlineElement::Emoji(e) => e,
1460+
_ => panic!("Expected an Emoji element"),
1461+
};
1462+
assert_eq!(emoji.name, SlackEmojiName::new("wave".into()));
1463+
1464+
// list
1465+
let list = match &rich.elements[1] {
1466+
SlackRichTextElement::List(l) => l,
1467+
_ => panic!("Expected a List element"),
1468+
};
1469+
assert_eq!(list.style, SlackRichTextListStyle::Bullet);
1470+
assert_eq!(list.elements.len(), 2);
1471+
1472+
// preformatted
1473+
assert!(matches!(
1474+
&rich.elements[2],
1475+
SlackRichTextElement::Preformatted(_)
1476+
));
1477+
1478+
// quote
1479+
assert!(matches!(&rich.elements[3], SlackRichTextElement::Quote(_)));
1480+
1481+
Ok(())
1482+
}
1483+
1484+
#[test]
1485+
fn test_rich_text_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
1486+
let payload = include_str!("./fixtures/slack_rich_text_block.json");
1487+
let block: SlackBlock = serde_json::from_str(payload)?;
1488+
let serialized = serde_json::to_string(&block)?;
1489+
let block2: SlackBlock = serde_json::from_str(&serialized)?;
1490+
assert_eq!(block, block2);
1491+
Ok(())
1492+
}
12251493
}

0 commit comments

Comments
 (0)