Skip to content

Commit c848d38

Browse files
authored
feat: add SlackTableBlock and SlackTaskCardBlock types with tests and socket_mode example (#362)
- Add SlackTaskId newtype (ValueStruct wrapping String) - Implement SlackTableBlock with rows, column_settings, SlackTableCell (raw_text/rich_text), SlackTableColumnSetting, SlackTableColumnAlign - Replace Table(serde_json::Value) placeholder in SlackBlock enum with Table(SlackTableBlock) - Implement SlackTaskCardBlock with task_id, title, status, details, output, sources - Add SlackTaskCardStatus enum (pending/in_progress/complete/error) - Add SlackUrlSourceElement for task card sources - Add fixture JSON files and deserialize/roundtrip tests for both blocks - Extend socket_mode example with table and task card block usage
1 parent aa2e8c5 commit c848d38

4 files changed

Lines changed: 337 additions & 3 deletions

File tree

examples/socket_mode.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,49 @@ async fn test_command_events_function(
5757
"my-option1-value".to_string()
5858
)])
5959
)
60-
]))
60+
])),
61+
some_into(
62+
SlackTableBlock::new(vec![
63+
vec![
64+
SlackTableCell::RawText(SlackTableRawTextCell::new("Name".into())),
65+
SlackTableCell::RawText(SlackTableRawTextCell::new("Status".into())),
66+
],
67+
vec![
68+
SlackTableCell::RawText(SlackTableRawTextCell::new(
69+
"Slack Morphism".into()
70+
)),
71+
SlackTableCell::RichText(SlackTableRichTextCell::new(vec![
72+
SlackRichTextSection::new(vec![SlackRichTextInlineElement::Text(
73+
SlackRichTextText::new("Active".into())
74+
.with_style(SlackRichTextStyle::new().with_bold(true))
75+
)])
76+
.into()
77+
])),
78+
],
79+
])
80+
.with_column_settings(vec![
81+
SlackTableColumnSetting::new(),
82+
SlackTableColumnSetting::new().with_align(SlackTableColumnAlign::Right),
83+
])
84+
),
85+
some_into(
86+
SlackTaskCardBlock::new("task_demo".into(), "Checking library status".into())
87+
.with_status(SlackTaskCardStatus::Complete)
88+
.with_output(
89+
SlackRichTextBlock::new(vec![SlackRichTextSection::new(vec![
90+
SlackRichTextInlineElement::Text(SlackRichTextText::new(
91+
"All systems operational".into()
92+
))
93+
])
94+
.into()])
95+
.into()
96+
)
97+
.with_sources(vec![SlackUrlSourceElement::new(
98+
Url::parse("https://slack-rust.abdolence.dev").expect("A proper url"),
99+
"slack-morphism docs".into()
100+
)
101+
.into()])
102+
)
61103
]),
62104
))
63105
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"type": "table",
3+
"block_id": "table_block_1",
4+
"column_settings": [
5+
{
6+
"is_wrapped": true
7+
},
8+
{
9+
"align": "right"
10+
}
11+
],
12+
"rows": [
13+
[
14+
{
15+
"type": "raw_text",
16+
"text": "Header A"
17+
},
18+
{
19+
"type": "raw_text",
20+
"text": "Header B"
21+
}
22+
],
23+
[
24+
{
25+
"type": "raw_text",
26+
"text": "Data 1A"
27+
},
28+
{
29+
"type": "rich_text",
30+
"elements": [
31+
{
32+
"type": "rich_text_section",
33+
"elements": [
34+
{
35+
"type": "link",
36+
"url": "https://slack.com",
37+
"text": "Data 1B"
38+
}
39+
]
40+
}
41+
]
42+
}
43+
]
44+
]
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"type": "task_card",
3+
"task_id": "task_1",
4+
"title": "Fetching weather data",
5+
"block_id": "task_card_block_1",
6+
"status": "in_progress",
7+
"output": {
8+
"type": "rich_text",
9+
"elements": [
10+
{
11+
"type": "rich_text_section",
12+
"elements": [
13+
{
14+
"type": "text",
15+
"text": "Found weather data for Chicago from 2 sources"
16+
}
17+
]
18+
}
19+
]
20+
},
21+
"sources": [
22+
{
23+
"type": "url",
24+
"url": "https://weather.com/",
25+
"text": "weather.com"
26+
},
27+
{
28+
"type": "url",
29+
"url": "https://www.accuweather.com/",
30+
"text": "accuweather.com"
31+
}
32+
]
33+
}

src/models/blocks/kit.rs

Lines changed: 216 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use crate::*;
1010
#[derive(Debug, PartialEq, Clone, Eq, Hash, Serialize, Deserialize, ValueStruct)]
1111
pub struct SlackBlockId(pub String);
1212

13+
#[derive(Debug, PartialEq, Clone, Eq, Hash, Serialize, Deserialize, ValueStruct)]
14+
pub struct SlackTaskId(pub String);
15+
1316
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1417
#[serde(tag = "type")]
1518
pub enum SlackBlock {
@@ -35,12 +38,14 @@ pub enum SlackBlock {
3538
Markdown(SlackMarkdownBlock),
3639
#[serde(rename = "rich_text")]
3740
RichText(SlackRichTextBlock),
41+
#[serde(rename = "table")]
42+
Table(SlackTableBlock),
43+
#[serde(rename = "task_card")]
44+
TaskCard(SlackTaskCardBlock),
3845
#[serde(rename = "share_shortcut")]
3946
ShareShortcut(serde_json::Value),
4047
#[serde(rename = "event")]
4148
Event(serde_json::Value),
42-
#[serde(rename = "table")]
43-
Table(serde_json::Value),
4449
}
4550

4651
#[skip_serializing_none]
@@ -1088,6 +1093,19 @@ impl From<SlackRichTextBlock> for SlackBlock {
10881093
}
10891094
}
10901095

1096+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1097+
#[serde(tag = "type", rename_all = "snake_case")]
1098+
pub enum SlackRichTextInlineContent {
1099+
#[serde(rename = "rich_text")]
1100+
RichText(SlackRichTextBlock),
1101+
}
1102+
1103+
impl From<SlackRichTextBlock> for SlackRichTextInlineContent {
1104+
fn from(block: SlackRichTextBlock) -> Self {
1105+
SlackRichTextInlineContent::RichText(block)
1106+
}
1107+
}
1108+
10911109
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
10921110
#[serde(tag = "type")]
10931111
pub enum SlackRichTextElement {
@@ -1343,6 +1361,113 @@ impl From<SlackFileId> for SlackFileIdOrUrl {
13431361
}
13441362
}
13451363

1364+
/**
1365+
* https://docs.slack.dev/reference/block-kit/blocks/table-block
1366+
*/
1367+
#[skip_serializing_none]
1368+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1369+
pub struct SlackTableBlock {
1370+
pub block_id: Option<SlackBlockId>,
1371+
pub rows: Vec<Vec<SlackTableCell>>,
1372+
pub column_settings: Option<Vec<SlackTableColumnSetting>>,
1373+
}
1374+
1375+
impl From<SlackTableBlock> for SlackBlock {
1376+
fn from(block: SlackTableBlock) -> Self {
1377+
SlackBlock::Table(block)
1378+
}
1379+
}
1380+
1381+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1382+
#[serde(tag = "type")]
1383+
pub enum SlackTableCell {
1384+
#[serde(rename = "raw_text")]
1385+
RawText(SlackTableRawTextCell),
1386+
#[serde(rename = "rich_text")]
1387+
RichText(SlackTableRichTextCell),
1388+
}
1389+
1390+
#[skip_serializing_none]
1391+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1392+
pub struct SlackTableRawTextCell {
1393+
pub text: String,
1394+
}
1395+
1396+
#[skip_serializing_none]
1397+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1398+
pub struct SlackTableRichTextCell {
1399+
pub elements: Vec<SlackRichTextElement>,
1400+
}
1401+
1402+
#[skip_serializing_none]
1403+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1404+
pub struct SlackTableColumnSetting {
1405+
pub align: Option<SlackTableColumnAlign>,
1406+
pub is_wrapped: Option<bool>,
1407+
}
1408+
1409+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1410+
#[serde(rename_all = "snake_case")]
1411+
pub enum SlackTableColumnAlign {
1412+
Left,
1413+
Center,
1414+
Right,
1415+
}
1416+
1417+
/**
1418+
* https://docs.slack.dev/reference/block-kit/blocks/task-card-block
1419+
*/
1420+
#[skip_serializing_none]
1421+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1422+
pub struct SlackTaskCardBlock {
1423+
pub task_id: SlackTaskId,
1424+
pub title: String,
1425+
pub block_id: Option<SlackBlockId>,
1426+
pub status: Option<SlackTaskCardStatus>,
1427+
#[serde(rename = "details")]
1428+
pub details: Option<SlackRichTextInlineContent>,
1429+
#[serde(rename = "output")]
1430+
pub output: Option<SlackRichTextInlineContent>,
1431+
pub sources: Option<Vec<SlackTaskCardSource>>,
1432+
}
1433+
1434+
impl From<SlackTaskCardBlock> for SlackBlock {
1435+
fn from(block: SlackTaskCardBlock) -> Self {
1436+
SlackBlock::TaskCard(block)
1437+
}
1438+
}
1439+
1440+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1441+
#[serde(rename_all = "snake_case")]
1442+
pub enum SlackTaskCardStatus {
1443+
Pending,
1444+
InProgress,
1445+
Complete,
1446+
Error,
1447+
}
1448+
1449+
/**
1450+
* https://docs.slack.dev/reference/block-kit/block-elements/url-source-element
1451+
*/
1452+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1453+
pub struct SlackUrlSourceElement {
1454+
pub url: Url,
1455+
pub text: String,
1456+
}
1457+
1458+
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1459+
#[serde(tag = "type")]
1460+
pub enum SlackTaskCardSource {
1461+
#[serde(rename = "url")]
1462+
Url(SlackUrlSourceElement),
1463+
}
1464+
1465+
impl From<SlackUrlSourceElement> for SlackTaskCardSource {
1466+
fn from(element: SlackUrlSourceElement) -> Self {
1467+
SlackTaskCardSource::Url(element)
1468+
}
1469+
}
1470+
13461471
#[cfg(test)]
13471472
mod test {
13481473
use super::*;
@@ -1538,4 +1663,93 @@ mod test {
15381663
assert_eq!(block, block2);
15391664
Ok(())
15401665
}
1666+
1667+
#[test]
1668+
fn test_slack_table_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
1669+
let payload = include_str!("./fixtures/slack_table_block.json");
1670+
let block: SlackBlock = serde_json::from_str(payload)?;
1671+
1672+
let table = match block {
1673+
SlackBlock::Table(t) => t,
1674+
_ => panic!("Expected a Table block"),
1675+
};
1676+
1677+
assert_eq!(table.block_id, Some(SlackBlockId("table_block_1".into())));
1678+
assert_eq!(table.rows.len(), 2);
1679+
assert_eq!(table.rows[0].len(), 2);
1680+
1681+
// first row, first cell is raw_text
1682+
match &table.rows[0][0] {
1683+
SlackTableCell::RawText(c) => assert_eq!(c.text, "Header A"),
1684+
_ => panic!("Expected RawText cell"),
1685+
}
1686+
1687+
// second row, second cell is rich_text
1688+
match &table.rows[1][1] {
1689+
SlackTableCell::RichText(c) => assert_eq!(c.elements.len(), 1),
1690+
_ => panic!("Expected RichText cell"),
1691+
}
1692+
1693+
let settings = table
1694+
.column_settings
1695+
.expect("column_settings should be present");
1696+
assert_eq!(settings.len(), 2);
1697+
assert_eq!(settings[0].is_wrapped, Some(true));
1698+
assert_eq!(settings[1].align, Some(SlackTableColumnAlign::Right));
1699+
1700+
Ok(())
1701+
}
1702+
1703+
#[test]
1704+
fn test_slack_table_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
1705+
let payload = include_str!("./fixtures/slack_table_block.json");
1706+
let block: SlackBlock = serde_json::from_str(payload)?;
1707+
let serialized = serde_json::to_string(&block)?;
1708+
let block2: SlackBlock = serde_json::from_str(&serialized)?;
1709+
assert_eq!(block, block2);
1710+
Ok(())
1711+
}
1712+
1713+
#[test]
1714+
fn test_slack_task_card_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
1715+
let payload = include_str!("./fixtures/slack_task_card_block.json");
1716+
let block: SlackBlock = serde_json::from_str(payload)?;
1717+
1718+
let task_card = match block {
1719+
SlackBlock::TaskCard(t) => t,
1720+
_ => panic!("Expected a TaskCard block"),
1721+
};
1722+
1723+
assert_eq!(task_card.task_id, SlackTaskId("task_1".into()));
1724+
assert_eq!(task_card.title, "Fetching weather data");
1725+
assert_eq!(
1726+
task_card.block_id,
1727+
Some(SlackBlockId("task_card_block_1".into()))
1728+
);
1729+
assert_eq!(task_card.status, Some(SlackTaskCardStatus::InProgress));
1730+
1731+
let output = task_card.output.expect("output should be present");
1732+
let output_block = match output {
1733+
SlackRichTextInlineContent::RichText(b) => b,
1734+
};
1735+
assert_eq!(output_block.elements.len(), 1);
1736+
1737+
let sources = task_card.sources.expect("sources should be present");
1738+
assert_eq!(sources.len(), 2);
1739+
match &sources[0] {
1740+
SlackTaskCardSource::Url(u) => assert_eq!(u.text, "weather.com"),
1741+
}
1742+
1743+
Ok(())
1744+
}
1745+
1746+
#[test]
1747+
fn test_slack_task_card_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
1748+
let payload = include_str!("./fixtures/slack_task_card_block.json");
1749+
let block: SlackBlock = serde_json::from_str(payload)?;
1750+
let serialized = serde_json::to_string(&block)?;
1751+
let block2: SlackBlock = serde_json::from_str(&serialized)?;
1752+
assert_eq!(block, block2);
1753+
Ok(())
1754+
}
15411755
}

0 commit comments

Comments
 (0)