From 4acfcfebbba8850171a6ab396b9b5aba7922df1c Mon Sep 17 00:00:00 2001 From: Kanji Tanaka Date: Sun, 21 Jun 2026 12:09:29 +0900 Subject: [PATCH] Add table row validation for DataTable block --- slack-messaging/src/blocks/data_table/cell.rs | 14 +++++ slack-messaging/src/blocks/data_table/mod.rs | 55 ++++++++++++++++++- slack-messaging/src/errors.rs | 4 ++ slack-messaging/src/validators/list.rs | 2 +- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/slack-messaging/src/blocks/data_table/cell.rs b/slack-messaging/src/blocks/data_table/cell.rs index a8d3485..0a5d0cb 100644 --- a/slack-messaging/src/blocks/data_table/cell.rs +++ b/slack-messaging/src/blocks/data_table/cell.rs @@ -17,6 +17,20 @@ pub enum DataTableCell { RichText(RichText), } +impl DataTableCell { + pub fn is_raw_text(&self) -> bool { + matches!(self, Self::RawText(_)) + } + + pub fn is_raw_number(&self) -> bool { + matches!(self, Self::RawNumber(_)) + } + + pub fn is_rich_text(&self) -> bool { + matches!(self, Self::RichText(_)) + } +} + impl> From for DataTableCell { fn from(value: T) -> Self { Self::RawText(RawText::from(value)) diff --git a/slack-messaging/src/blocks/data_table/mod.rs b/slack-messaging/src/blocks/data_table/mod.rs index 6c3e951..3843ef5 100644 --- a/slack-messaging/src/blocks/data_table/mod.rs +++ b/slack-messaging/src/blocks/data_table/mod.rs @@ -1,3 +1,5 @@ +use crate::errors::ValidationErrorKind; +use crate::value::Value; use crate::validators::*; use serde::Serialize; @@ -24,7 +26,7 @@ pub use row::DataTableRow; /// | Field | Type | Required | Validation | /// |-------|------|----------|------------| /// | block_id | String | No | Maximum 255 characters. | -/// | rows | Vec<[DataTableRow]> | Yes | Maximum 101 items (100 regular rows plus the header). Minimum 2 items (1 regular row plus the header). | +/// | rows | Vec<[DataTableRow]> | Yes | Maximum 101 items (100 regular rows plus the header). Minimum 2 items (1 regular row plus the header). The first row is a header, and `rich_text` cannot be used for the header cells. | /// | caption | String | Yes | N/A | /// | page_size | i64 | No | Minimum 1, maximum 100. | /// | row_header_column_index | i64 | No | Minimum 0. | @@ -247,7 +249,10 @@ pub struct DataTable { #[builder(validate("text::max_255"))] pub(crate) block_id: Option, - #[builder(push_item = "row", validate("required", "list::min_item_2", "list::max_item_101"))] + #[builder( + push_item = "row", + validate("required", "list::min_item_2", "list::max_item_101", "valid_header") + )] pub(crate) rows: Option>, #[builder(validate("required"))] @@ -262,10 +267,19 @@ pub struct DataTable { pub(crate) row_header_column_index: Option, } +fn valid_header(value: Value>) -> Value> { + list::inner_validator(value, ValidationErrorKind::RichTextTableHeader, |rows| { + rows.first().and_then(|row| row.cells.as_ref()).is_some_and(|cells| { + cells.iter().any(DataTableCell::is_rich_text) + }) + }) +} + #[cfg(test)] mod tests { use super::*; use crate::errors::*; + use crate::blocks::rich_text::prelude::*; #[test] fn it_implements_builder() { @@ -382,6 +396,20 @@ mod tests { assert!(errors.includes(ValidationErrorKind::MaxArraySize(101))); } + #[test] + fn it_requires_rows_field_to_have_header_row_without_rich_text() { + let err = DataTable::builder() + .caption("A Fabulous Table") + .row(rich_text_row(vec!["foo", "bar", "baz"])) + .row(row(vec!["000", "001", "002"])) + .build() + .unwrap_err(); + assert_eq!(err.object(), "DataTable"); + + let errors = err.field("rows"); + assert!(errors.includes(ValidationErrorKind::RichTextTableHeader)); + } + #[test] fn it_requires_caption_field() { let err = DataTable::builder() @@ -443,4 +471,27 @@ mod tests { fn row>(cells: Vec) -> DataTableRow { DataTableRow::from_iter(cells) } + + fn rich_text_row(cells: Vec<&str>) -> DataTableRow { + let cells = cells + .into_iter() + .map(|text| { + RichText::builder() + .element( + RichTextSection::builder() + .element( + RichTextElementText::builder() + .text(text) + .build() + .unwrap(), + ) + .build() + .unwrap(), + ) + .build() + .unwrap() + }) + .collect::>(); + row(cells) + } } diff --git a/slack-messaging/src/errors.rs b/slack-messaging/src/errors.rs index dfd9984..be5ba83 100644 --- a/slack-messaging/src/errors.rs +++ b/slack-messaging/src/errors.rs @@ -58,6 +58,10 @@ pub enum ValidationErrorKind { /// Rich text block should have exactly one element. #[error("rich text block should have exactly one element")] RichTextSingleElement, + + /// Rich text cannot be used for a table header row. + #[error("rich text cannot be used for a table header row")] + RichTextTableHeader, } /// Validation error from single field or across fields. diff --git a/slack-messaging/src/validators/list.rs b/slack-messaging/src/validators/list.rs index 6e93043..b677e2e 100644 --- a/slack-messaging/src/validators/list.rs +++ b/slack-messaging/src/validators/list.rs @@ -5,7 +5,7 @@ use paste::paste; type List = Value>; -fn inner_validator( +pub(crate) fn inner_validator( mut value: List, error: ValidationErrorKind, predicate: impl Fn(&[T]) -> bool,