From e3af918b085b0b1a231072811cb2b3a37e40ac8f Mon Sep 17 00:00:00 2001 From: Victor Li Date: Mon, 15 Jun 2026 13:45:47 -0400 Subject: [PATCH 1/2] introducing table + data table wrappers for blocks --- .../slack/client/models/blocks/Block.java | 2 + .../blocks/BlockElementLengthLimits.java | 7 +- .../client/models/blocks/DataTableIF.java | 95 ++++++ .../slack/client/models/blocks/TableIF.java | 61 ++++ .../models/blocks/table/DataTableCell.java | 20 ++ .../blocks/table/RawNumberTableCellIF.java | 26 ++ .../blocks/table/RawTextTableCellIF.java | 23 ++ .../blocks/table/RichTextTableCellIF.java | 26 ++ .../client/models/blocks/table/TableCell.java | 21 ++ .../blocks/table/TableColumnSettingIF.java | 20 ++ .../models/blocks/table/UnknownTableCell.java | 13 + .../models/blocks/DataTableBlockTest.java | 270 ++++++++++++++++++ .../client/models/blocks/TableBlockTest.java | 180 ++++++++++++ .../src/test/resources/data_table_block.json | 78 +++++ .../src/test/resources/table_block.json | 66 +++++ 15 files changed, 907 insertions(+), 1 deletion(-) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/DataTableCell.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawTextTableCellIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RichTextTableCellIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableCell.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/UnknownTableCell.java create mode 100644 slack-base/src/test/java/com/hubspot/slack/client/models/blocks/DataTableBlockTest.java create mode 100644 slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java create mode 100644 slack-base/src/test/resources/data_table_block.json create mode 100644 slack-base/src/test/resources/table_block.json diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java index f81ad170..e51880f9 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java @@ -30,6 +30,8 @@ value = ContextActionsBlock.class, name = ContextActionsBlock.TYPE ), + @JsonSubTypes.Type(value = Table.class, name = Table.TYPE), + @JsonSubTypes.Type(value = DataTable.class, name = DataTable.TYPE), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java index b9f9efc2..542a6599 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java @@ -11,7 +11,12 @@ public enum BlockElementLengthLimits { MAX_OPTION_GROUP_LABEL_LENGTH(75), MAX_OPTION_VALUE_LENGTH(75), MAX_CHECKBOXES_NUMBER(10), - MAX_RADIO_BUTTONS_NUMBER(10); + MAX_RADIO_BUTTONS_NUMBER(10), + MAX_TABLE_ROWS(100), + MAX_TABLE_COLUMNS(20), + MAX_TABLE_BLOCK_ID_LENGTH(255), + MIN_DATA_TABLE_ROWS(2), + MAX_DATA_TABLE_PAGE_SIZE(100); private final int limit; diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java new file mode 100644 index 00000000..b0bbddec --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java @@ -0,0 +1,95 @@ +package com.hubspot.slack.client.models.blocks; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.table.DataTableCell; +import com.hubspot.slack.client.models.blocks.table.RawTextTableCell; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Check; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface DataTableIF extends Block { + String TYPE = "data_table"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getCaption(); + + @Value.Parameter + ImmutableList> getRows(); + + @JsonProperty("page_size") + Optional getPageSize(); + + @JsonProperty("row_header_column_index") + Optional getRowHeaderColumnIndex(); + + @Check + default void check() { + Preconditions.checkState( + getRows().size() >= BlockElementLengthLimits.MIN_DATA_TABLE_ROWS.getLimit(), + "A data_table block must have at least %s rows (1 header + 1 body)", + BlockElementLengthLimits.MIN_DATA_TABLE_ROWS.getLimit() + ); + Preconditions.checkState( + getRows().size() <= BlockElementLengthLimits.MAX_TABLE_ROWS.getLimit(), + "A data_table block cannot have more than %s rows", + BlockElementLengthLimits.MAX_TABLE_ROWS.getLimit() + ); + getRows() + .forEach(row -> + Preconditions.checkState( + row.size() <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(), + "A data_table row cannot have more than %s columns", + BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit() + ) + ); + int columnCount = getRows().get(0).size(); + getRows() + .forEach(row -> + Preconditions.checkState( + row.size() == columnCount, + "All rows in a data_table block must have the same number of columns" + ) + ); + getRows() + .get(0) + .forEach(cell -> + Preconditions.checkState( + cell instanceof RawTextTableCell, + "Header row cells must be of type raw_text" + ) + ); + getPageSize() + .ifPresent(pageSize -> + Preconditions.checkState( + pageSize >= 1 && + pageSize <= BlockElementLengthLimits.MAX_DATA_TABLE_PAGE_SIZE.getLimit(), + "page_size must be between 1 and %s", + BlockElementLengthLimits.MAX_DATA_TABLE_PAGE_SIZE.getLimit() + ) + ); + getBlockId() + .ifPresent(blockId -> + Preconditions.checkState( + blockId.length() <= + BlockElementLengthLimits.MAX_TABLE_BLOCK_ID_LENGTH.getLimit(), + "block_id cannot exceed %s characters", + BlockElementLengthLimits.MAX_TABLE_BLOCK_ID_LENGTH.getLimit() + ) + ); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java new file mode 100644 index 00000000..2e9df519 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java @@ -0,0 +1,61 @@ +package com.hubspot.slack.client.models.blocks; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.table.TableCell; +import com.hubspot.slack.client.models.blocks.table.TableColumnSetting; +import org.immutables.value.Value; +import org.immutables.value.Value.Check; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface TableIF extends Block { + String TYPE = "table"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + ImmutableList> getRows(); + + ImmutableList getColumnSettings(); + + @Check + default void check() { + Preconditions.checkState( + getRows().size() <= BlockElementLengthLimits.MAX_TABLE_ROWS.getLimit(), + "A table block cannot have more than %s rows", + BlockElementLengthLimits.MAX_TABLE_ROWS.getLimit() + ); + getRows() + .forEach(row -> + Preconditions.checkState( + row.size() <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(), + "A table row cannot have more than %s cells", + BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit() + ) + ); + Preconditions.checkState( + getColumnSettings().size() <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(), + "A table block cannot have more than %s column settings", + BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit() + ); + getBlockId() + .ifPresent(blockId -> + Preconditions.checkState( + blockId.length() <= + BlockElementLengthLimits.MAX_TABLE_BLOCK_ID_LENGTH.getLimit(), + "block_id cannot exceed %s characters", + BlockElementLengthLimits.MAX_TABLE_BLOCK_ID_LENGTH.getLimit() + ) + ); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/DataTableCell.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/DataTableCell.java new file mode 100644 index 00000000..7a14bddc --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/DataTableCell.java @@ -0,0 +1,20 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type", + defaultImpl = UnknownTableCell.class +) +@JsonSubTypes( + { + @JsonSubTypes.Type(value = RawTextTableCell.class, name = RawTextTableCell.TYPE), + @JsonSubTypes.Type(value = RawNumberTableCell.class, name = RawNumberTableCell.TYPE), + @JsonSubTypes.Type(value = RichTextTableCell.class, name = RichTextTableCell.TYPE), + } +) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public interface DataTableCell extends TableCell {} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java new file mode 100644 index 00000000..6748fb6b --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java @@ -0,0 +1,26 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface RawNumberTableCellIF extends DataTableCell { + String TYPE = "raw_number"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + double getValue(); + + @Value.Parameter + String getText(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawTextTableCellIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawTextTableCellIF.java new file mode 100644 index 00000000..fd3d2b69 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawTextTableCellIF.java @@ -0,0 +1,23 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface RawTextTableCellIF extends DataTableCell { + String TYPE = "raw_text"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getText(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RichTextTableCellIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RichTextTableCellIF.java new file mode 100644 index 00000000..4a8255fa --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RichTextTableCellIF.java @@ -0,0 +1,26 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.google.common.collect.ImmutableList; +import com.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextObject; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextTableCellIF extends DataTableCell { + String TYPE = "rich_text"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @JsonProperty("elements") + ImmutableList getElements(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableCell.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableCell.java new file mode 100644 index 00000000..6039ec8d --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableCell.java @@ -0,0 +1,21 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type", + defaultImpl = UnknownTableCell.class +) +@JsonSubTypes( + { + @JsonSubTypes.Type(value = RawTextTableCell.class, name = RawTextTableCell.TYPE), + @JsonSubTypes.Type(value = RichTextTableCell.class, name = RichTextTableCell.TYPE), + } +) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public interface TableCell { + String getType(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java new file mode 100644 index 00000000..792d2bc5 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java @@ -0,0 +1,20 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_ABSENT) +public interface TableColumnSettingIF { + Optional getAlign(); + + @JsonProperty("is_wrapped") + Optional isWrapped(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/UnknownTableCell.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/UnknownTableCell.java new file mode 100644 index 00000000..bec91f9d --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/UnknownTableCell.java @@ -0,0 +1,13 @@ +package com.hubspot.slack.client.models.blocks.table; + +public class UnknownTableCell implements DataTableCell { + + public static final String TYPE = "unknown"; + + protected UnknownTableCell() {} + + @Override + public String getType() { + return TYPE; + } +} diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/DataTableBlockTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/DataTableBlockTest.java new file mode 100644 index 00000000..262dfdc7 --- /dev/null +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/DataTableBlockTest.java @@ -0,0 +1,270 @@ +package com.hubspot.slack.client.models.blocks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.InstanceOfAssertFactories.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.hubspot.slack.client.jackson.ObjectMapperUtils; +import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextSection; +import com.hubspot.slack.client.models.blocks.table.DataTableCell; +import com.hubspot.slack.client.models.blocks.table.RawNumberTableCell; +import com.hubspot.slack.client.models.blocks.table.RawTextTableCell; +import com.hubspot.slack.client.models.blocks.table.RichTextTableCell; +import com.hubspot.slack.client.models.blocks.table.UnknownTableCell; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.Test; + +public class DataTableBlockTest { + + private static final ObjectMapper MAPPER = ObjectMapperUtils.mapper(); + private static final int RICH_TEXT_BODY_INDEX = 0; + private static final int RAW_NUMBER_INDEX = 1; + private static final int WITH_OPTIONS_INDEX = 2; + private static final int UNKNOWN_CELL_INDEX = 3; + private static DataTable[] blocks; + + @BeforeClass + public static void loadBlocks() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("data_table_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + blocks = new DataTable[root.size()]; + for (int i = 0; i < root.size(); i++) { + blocks[i] = MAPPER.treeToValue(root.get(i), DataTable.class); + } + } + + @Test + public void itDeserializesAsCorrectBlockType() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("data_table_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + Block block = MAPPER.treeToValue(root.get(0), Block.class); + assertThat(block).isInstanceOf(DataTable.class); + } + + @Test + public void itDeserializesCaption() { + assertThat(blocks[RICH_TEXT_BODY_INDEX].getCaption()).isEqualTo("Team Directory"); + } + + @Test + public void itDeserializesHeaderRowAsRawTextCells() { + ImmutableList headerRow = + blocks[RICH_TEXT_BODY_INDEX].getRows().get(0); + assertThat(headerRow) + .containsExactly( + RawTextTableCell.of("Name"), + RawTextTableCell.of("Department"), + RawTextTableCell.of("Badge") + ); + } + + @Test + public void itDeserializesRichTextBodyCell() { + ImmutableList bodyRow = blocks[RICH_TEXT_BODY_INDEX].getRows().get(1); + assertThat(bodyRow.get(2)) + .asInstanceOf(type(RichTextTableCell.class)) + .satisfies(cell -> { + assertThat(cell.getElements()).hasSize(1); + assertThat(cell.getElements().get(0)).isInstanceOf(RichTextSection.class); + }); + } + + @Test + public void itDeserializesRawNumberCells() { + ImmutableList bodyRow = blocks[RAW_NUMBER_INDEX].getRows().get(1); + assertThat(bodyRow.get(1)).isEqualTo(RawNumberTableCell.of(95.0, "95")); + assertThat(bodyRow.get(2)).isEqualTo(RawNumberTableCell.of(10.5, "10.5")); + } + + @Test + public void itDeserializesRawNumberValueAndText() { + ImmutableList bodyRow = blocks[RAW_NUMBER_INDEX].getRows().get(1); + assertThat(bodyRow.get(1)) + .asInstanceOf(type(RawNumberTableCell.class)) + .satisfies(cell -> { + assertThat(cell.getValue()).isEqualTo(95.0); + assertThat(cell.getText()).isEqualTo("95"); + }); + } + + @Test + public void itDeserializesBlockId() { + assertThat(blocks[WITH_OPTIONS_INDEX].getBlockId()).contains("my_data_table"); + } + + @Test + public void itDeserializesPageSize() { + assertThat(blocks[WITH_OPTIONS_INDEX].getPageSize()).contains(10); + } + + @Test + public void itDeserializesRowHeaderColumnIndex() { + assertThat(blocks[WITH_OPTIONS_INDEX].getRowHeaderColumnIndex()).contains(1); + } + + @Test + public void itDeserializesUnknownCellAsUnknownTableCell() { + assertThat(blocks[UNKNOWN_CELL_INDEX].getRows().get(1)) + .hasSize(1) + .first() + .isInstanceOf(UnknownTableCell.class); + } + + @Test + public void itSerializesAndDeserializes() throws IOException { + DataTable original = DataTable + .builder() + .setCaption("Scores") + .addRows( + ImmutableList.of(RawTextTableCell.of("Player"), RawTextTableCell.of("Score")) + ) + .addRows( + ImmutableList.of(RawTextTableCell.of("Alice"), RawNumberTableCell.of(95.0, "95")) + ) + .build(); + String serialized = MAPPER.writeValueAsString(original); + DataTable deserialized = MAPPER.readValue(serialized, DataTable.class); + assertThat(deserialized).isEqualTo(original); + } + + @Test + public void itFailsToBuildWithTooFewRows() { + try { + DataTable + .builder() + .setCaption("Empty") + .addRows(ImmutableList.of(RawTextTableCell.of("Header"))) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("2"); + return; + } + fail("Expected IllegalStateException for fewer than 2 rows"); + } + + @Test + public void itFailsToBuildWithTooManyRows() { + DataTable.Builder builder = DataTable.builder().setCaption("Big"); + builder.addRows(ImmutableList.of(RawTextTableCell.of("Header"))); + for (int i = 0; i <= BlockElementLengthLimits.MAX_TABLE_ROWS.getLimit(); i++) { + builder.addRows(ImmutableList.of(RawTextTableCell.of("cell"))); + } + try { + builder.build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("100"); + return; + } + fail("Expected IllegalStateException for too many rows"); + } + + @Test + public void itFailsToBuildWithTooManyColumns() { + ImmutableList.Builder rowBuilder = ImmutableList.builder(); + for (int i = 0; i <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(); i++) { + rowBuilder.add(RawTextTableCell.of("cell")); + } + ImmutableList wideRow = rowBuilder.build(); + try { + DataTable.builder().setCaption("Wide").addRows(wideRow).addRows(wideRow).build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("20"); + return; + } + fail("Expected IllegalStateException for too many columns"); + } + + @Test + public void itFailsToBuildWithInconsistentColumnCounts() { + try { + DataTable + .builder() + .setCaption("Inconsistent") + .addRows(ImmutableList.of(RawTextTableCell.of("A"), RawTextTableCell.of("B"))) + .addRows(ImmutableList.of(RawTextTableCell.of("only one"))) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("same number of columns"); + return; + } + fail("Expected IllegalStateException for inconsistent column counts"); + } + + @Test + public void itFailsToBuildWithNonRawTextHeaderCell() { + try { + DataTable + .builder() + .setCaption("Bad Header") + .addRows( + ImmutableList.of(RawTextTableCell.of("OK"), RawNumberTableCell.of(1.0, "1")) + ) + .addRows( + ImmutableList.of(RawTextTableCell.of("body"), RawTextTableCell.of("body")) + ) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("raw_text"); + return; + } + fail("Expected IllegalStateException for non-raw_text header cell"); + } + + @Test + public void itFailsToBuildWithInvalidPageSizeTooLow() { + try { + DataTable + .builder() + .setCaption("Bad Page") + .addRows(ImmutableList.of(RawTextTableCell.of("Header"))) + .addRows(ImmutableList.of(RawTextTableCell.of("body"))) + .setPageSize(0) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("between 1"); + return; + } + fail("Expected IllegalStateException for page_size of 0"); + } + + @Test + public void itFailsToBuildWithInvalidPageSizeTooHigh() { + try { + DataTable + .builder() + .setCaption("Bad Page") + .addRows(ImmutableList.of(RawTextTableCell.of("Header"))) + .addRows(ImmutableList.of(RawTextTableCell.of("body"))) + .setPageSize(BlockElementLengthLimits.MAX_DATA_TABLE_PAGE_SIZE.getLimit() + 1) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("100"); + return; + } + fail("Expected IllegalStateException for page_size exceeding maximum"); + } + + @Test + public void itFailsToBuildWithBlockIdExceedingMaxLength() { + try { + DataTable + .builder() + .setCaption("Long ID") + .addRows(ImmutableList.of(RawTextTableCell.of("Header"))) + .addRows(ImmutableList.of(RawTextTableCell.of("body"))) + .setBlockId( + "a".repeat(BlockElementLengthLimits.MAX_TABLE_BLOCK_ID_LENGTH.getLimit() + 1) + ) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("255"); + return; + } + fail("Expected IllegalStateException for block_id exceeding max length"); + } +} diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java new file mode 100644 index 00000000..44023963 --- /dev/null +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java @@ -0,0 +1,180 @@ +package com.hubspot.slack.client.models.blocks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.InstanceOfAssertFactories.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.hubspot.slack.client.jackson.ObjectMapperUtils; +import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextSection; +import com.hubspot.slack.client.models.blocks.table.RawTextTableCell; +import com.hubspot.slack.client.models.blocks.table.RichTextTableCell; +import com.hubspot.slack.client.models.blocks.table.TableCell; +import com.hubspot.slack.client.models.blocks.table.TableColumnSetting; +import com.hubspot.slack.client.models.blocks.table.UnknownTableCell; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TableBlockTest { + + private static final ObjectMapper MAPPER = ObjectMapperUtils.mapper(); + private static final int RAW_TEXT_INDEX = 0; + private static final int RICH_TEXT_INDEX = 1; + private static final int COLUMN_SETTINGS_INDEX = 2; + private static final int BLOCK_ID_INDEX = 3; + private static final int UNKNOWN_CELL_INDEX = 4; + private static Table[] blocks; + + @BeforeClass + public static void loadBlocks() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("table_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + blocks = new Table[root.size()]; + for (int i = 0; i < root.size(); i++) { + blocks[i] = MAPPER.treeToValue(root.get(i), Table.class); + } + } + + @Test + public void itDeserializesAsCorrectBlockType() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("table_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + Block block = MAPPER.treeToValue(root.get(0), Block.class); + assertThat(block).isInstanceOf(Table.class); + } + + @Test + public void itDeserializesRawTextCells() { + Table block = blocks[RAW_TEXT_INDEX]; + assertThat(block.getRows()).hasSize(2); + assertThat(block.getRows().get(0)) + .containsExactly(RawTextTableCell.of("Name"), RawTextTableCell.of("Department")); + assertThat(block.getRows().get(1)) + .containsExactly(RawTextTableCell.of("Alice"), RawTextTableCell.of("Engineering")); + } + + @Test + public void itDeserializesRichTextCells() { + Table block = blocks[RICH_TEXT_INDEX]; + assertThat(block.getRows()).hasSize(1); + assertThat(block.getRows().get(0).get(0)) + .asInstanceOf(type(RichTextTableCell.class)) + .satisfies(cell -> { + assertThat(cell.getElements()).hasSize(1); + assertThat(cell.getElements().get(0)).isInstanceOf(RichTextSection.class); + }); + } + + @Test + public void itDeserializesColumnSettings() { + Table block = blocks[COLUMN_SETTINGS_INDEX]; + assertThat(block.getColumnSettings()).hasSize(3); + assertThat(block.getColumnSettings().get(0)) + .isEqualTo(TableColumnSetting.builder().setAlign("left").build()); + assertThat(block.getColumnSettings().get(1)) + .isEqualTo( + TableColumnSetting.builder().setAlign("center").setWrapped(true).build() + ); + assertThat(block.getColumnSettings().get(2)) + .isEqualTo(TableColumnSetting.builder().setAlign("right").build()); + } + + @Test + public void itDeserializesBlockId() { + assertThat(blocks[BLOCK_ID_INDEX].getBlockId()).contains("my_table_block"); + } + + @Test + public void itDeserializesUnknownCellAsUnknownTableCell() { + assertThat(blocks[UNKNOWN_CELL_INDEX].getRows()).hasSize(1); + assertThat(blocks[UNKNOWN_CELL_INDEX].getRows().get(0)) + .hasSize(1) + .first() + .isInstanceOf(UnknownTableCell.class); + } + + @Test + public void itSerializesAndDeserializes() throws IOException { + Table original = Table + .builder() + .addRows( + ImmutableList.of(RawTextTableCell.of("Name"), RawTextTableCell.of("Score")) + ) + .addRows(ImmutableList.of(RawTextTableCell.of("Alice"), RawTextTableCell.of("100"))) + .addColumnSettings(TableColumnSetting.builder().setAlign("left").build()) + .addColumnSettings( + TableColumnSetting.builder().setAlign("right").setWrapped(true).build() + ) + .build(); + String serialized = MAPPER.writeValueAsString(original); + Table deserialized = MAPPER.readValue(serialized, Table.class); + assertThat(deserialized).isEqualTo(original); + } + + @Test + public void itFailsToBuildWithTooManyRows() { + Table.Builder builder = Table.builder(); + for (int i = 0; i <= BlockElementLengthLimits.MAX_TABLE_ROWS.getLimit(); i++) { + builder.addRows(ImmutableList.of(RawTextTableCell.of("cell"))); + } + try { + builder.build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("100"); + return; + } + fail("Expected IllegalStateException for too many rows"); + } + + @Test + public void itFailsToBuildWithTooManyCellsInRow() { + ImmutableList.Builder rowBuilder = ImmutableList.builder(); + for (int i = 0; i <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(); i++) { + rowBuilder.add(RawTextTableCell.of("cell")); + } + try { + Table.builder().addRows(rowBuilder.build()).build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("20"); + return; + } + fail("Expected IllegalStateException for too many cells in row"); + } + + @Test + public void itFailsToBuildWithTooManyColumnSettings() { + Table.Builder builder = Table.builder(); + builder.addRows(ImmutableList.of(RawTextTableCell.of("cell"))); + for (int i = 0; i <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(); i++) { + builder.addColumnSettings(TableColumnSetting.builder().setAlign("left").build()); + } + try { + builder.build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("20"); + return; + } + fail("Expected IllegalStateException for too many column settings"); + } + + @Test + public void itFailsToBuildWithBlockIdExceedingMaxLength() { + try { + Table + .builder() + .addRows(ImmutableList.of(RawTextTableCell.of("cell"))) + .setBlockId( + "a".repeat(BlockElementLengthLimits.MAX_TABLE_BLOCK_ID_LENGTH.getLimit() + 1) + ) + .build(); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("255"); + return; + } + fail("Expected IllegalStateException for block_id exceeding max length"); + } +} diff --git a/slack-base/src/test/resources/data_table_block.json b/slack-base/src/test/resources/data_table_block.json new file mode 100644 index 00000000..00a406cb --- /dev/null +++ b/slack-base/src/test/resources/data_table_block.json @@ -0,0 +1,78 @@ +[ + { + "type": "data_table", + "caption": "Team Directory", + "rows": [ + [ + {"type": "raw_text", "text": "Name"}, + {"type": "raw_text", "text": "Department"}, + {"type": "raw_text", "text": "Badge"} + ], + [ + {"type": "raw_text", "text": "Alice"}, + {"type": "raw_text", "text": "Engineering"}, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Blue", "style": {"bold": true}} + ] + } + ] + } + ] + ] + }, + { + "type": "data_table", + "caption": "Scores", + "rows": [ + [ + {"type": "raw_text", "text": "Player"}, + {"type": "raw_text", "text": "Score"}, + {"type": "raw_text", "text": "Bonus"} + ], + [ + {"type": "raw_text", "text": "Alice"}, + {"type": "raw_number", "value": 95.0, "text": "95"}, + {"type": "raw_number", "value": 10.5, "text": "10.5"} + ], + [ + {"type": "raw_text", "text": "Bob"}, + {"type": "raw_number", "value": 82.5, "text": "82.5"}, + {"type": "raw_number", "value": 0.0, "text": "0"} + ] + ] + }, + { + "type": "data_table", + "caption": "With Options", + "block_id": "my_data_table", + "page_size": 10, + "row_header_column_index": 1, + "rows": [ + [ + {"type": "raw_text", "text": "ID"}, + {"type": "raw_text", "text": "Name"} + ], + [ + {"type": "raw_text", "text": "1"}, + {"type": "raw_text", "text": "Alice"} + ] + ] + }, + { + "type": "data_table", + "caption": "Unknown Cell", + "rows": [ + [ + {"type": "raw_text", "text": "Column"} + ], + [ + {"type": "future_slack_cell_type"} + ] + ] + } +] diff --git a/slack-base/src/test/resources/table_block.json b/slack-base/src/test/resources/table_block.json new file mode 100644 index 00000000..9586a79f --- /dev/null +++ b/slack-base/src/test/resources/table_block.json @@ -0,0 +1,66 @@ +[ + { + "type": "table", + "rows": [ + [ + {"type": "raw_text", "text": "Name"}, + {"type": "raw_text", "text": "Department"} + ], + [ + {"type": "raw_text", "text": "Alice"}, + {"type": "raw_text", "text": "Engineering"} + ] + ] + }, + { + "type": "table", + "rows": [ + [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello "}, + {"type": "text", "text": "world", "style": {"bold": true}} + ] + } + ] + } + ] + ] + }, + { + "type": "table", + "rows": [ + [ + {"type": "raw_text", "text": "Left"}, + {"type": "raw_text", "text": "Center"}, + {"type": "raw_text", "text": "Right"} + ] + ], + "column_settings": [ + {"align": "left"}, + {"align": "center", "is_wrapped": true}, + {"align": "right"} + ] + }, + { + "type": "table", + "block_id": "my_table_block", + "rows": [ + [ + {"type": "raw_text", "text": "Value"} + ] + ] + }, + { + "type": "table", + "rows": [ + [ + {"type": "future_slack_cell_type"} + ] + ] + } +] From 2145bb84746a16bfef44ee454006864b9a880d65 Mon Sep 17 00:00:00 2001 From: Victor Li Date: Mon, 22 Jun 2026 10:22:00 -0400 Subject: [PATCH 2/2] addressing comments --- .../client/models/blocks/DataTableIF.java | 6 +++-- .../blocks/table/RawNumberTableCellIF.java | 4 +-- .../models/blocks/table/TableColumnAlign.java | 20 +++++++++++++++ .../blocks/table/TableColumnSettingIF.java | 2 +- .../client/models/blocks/TableBlockTest.java | 25 ++++++++++++++----- 5 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnAlign.java diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java index b0bbddec..0140ac42 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DataTableIF.java @@ -1,5 +1,6 @@ package com.hubspot.slack.client.models.blocks; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; @@ -16,6 +17,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_EMPTY) public interface DataTableIF extends Block { String TYPE = "data_table"; @@ -25,10 +27,10 @@ default String getType() { return TYPE; } - @Value.Parameter + @Value.Parameter(order = 1) String getCaption(); - @Value.Parameter + @Value.Parameter(order = 2) ImmutableList> getRows(); @JsonProperty("page_size") diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java index 6748fb6b..d66e0085 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/RawNumberTableCellIF.java @@ -18,9 +18,9 @@ default String getType() { return TYPE; } - @Value.Parameter + @Value.Parameter(order = 1) double getValue(); - @Value.Parameter + @Value.Parameter(order = 2) String getText(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnAlign.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnAlign.java new file mode 100644 index 00000000..194157a7 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnAlign.java @@ -0,0 +1,20 @@ +package com.hubspot.slack.client.models.blocks.table; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum TableColumnAlign { + LEFT("left"), + CENTER("center"), + RIGHT("right"); + + private final String value; + + TableColumnAlign(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java index 792d2bc5..7fe06c8c 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/table/TableColumnSettingIF.java @@ -13,7 +13,7 @@ @JsonNaming(SnakeCaseStrategy.class) @JsonInclude(JsonInclude.Include.NON_ABSENT) public interface TableColumnSettingIF { - Optional getAlign(); + Optional getAlign(); @JsonProperty("is_wrapped") Optional isWrapped(); diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java index 44023963..0b0c46d9 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/TableBlockTest.java @@ -13,6 +13,7 @@ import com.hubspot.slack.client.models.blocks.table.RawTextTableCell; import com.hubspot.slack.client.models.blocks.table.RichTextTableCell; import com.hubspot.slack.client.models.blocks.table.TableCell; +import com.hubspot.slack.client.models.blocks.table.TableColumnAlign; import com.hubspot.slack.client.models.blocks.table.TableColumnSetting; import com.hubspot.slack.client.models.blocks.table.UnknownTableCell; import java.io.IOException; @@ -74,13 +75,17 @@ public void itDeserializesColumnSettings() { Table block = blocks[COLUMN_SETTINGS_INDEX]; assertThat(block.getColumnSettings()).hasSize(3); assertThat(block.getColumnSettings().get(0)) - .isEqualTo(TableColumnSetting.builder().setAlign("left").build()); + .isEqualTo(TableColumnSetting.builder().setAlign(TableColumnAlign.LEFT).build()); assertThat(block.getColumnSettings().get(1)) .isEqualTo( - TableColumnSetting.builder().setAlign("center").setWrapped(true).build() + TableColumnSetting + .builder() + .setAlign(TableColumnAlign.CENTER) + .setWrapped(true) + .build() ); assertThat(block.getColumnSettings().get(2)) - .isEqualTo(TableColumnSetting.builder().setAlign("right").build()); + .isEqualTo(TableColumnSetting.builder().setAlign(TableColumnAlign.RIGHT).build()); } @Test @@ -105,9 +110,15 @@ public void itSerializesAndDeserializes() throws IOException { ImmutableList.of(RawTextTableCell.of("Name"), RawTextTableCell.of("Score")) ) .addRows(ImmutableList.of(RawTextTableCell.of("Alice"), RawTextTableCell.of("100"))) - .addColumnSettings(TableColumnSetting.builder().setAlign("left").build()) .addColumnSettings( - TableColumnSetting.builder().setAlign("right").setWrapped(true).build() + TableColumnSetting.builder().setAlign(TableColumnAlign.LEFT).build() + ) + .addColumnSettings( + TableColumnSetting + .builder() + .setAlign(TableColumnAlign.RIGHT) + .setWrapped(true) + .build() ) .build(); String serialized = MAPPER.writeValueAsString(original); @@ -150,7 +161,9 @@ public void itFailsToBuildWithTooManyColumnSettings() { Table.Builder builder = Table.builder(); builder.addRows(ImmutableList.of(RawTextTableCell.of("cell"))); for (int i = 0; i <= BlockElementLengthLimits.MAX_TABLE_COLUMNS.getLimit(); i++) { - builder.addColumnSettings(TableColumnSetting.builder().setAlign("left").build()); + builder.addColumnSettings( + TableColumnSetting.builder().setAlign(TableColumnAlign.LEFT).build() + ); } try { builder.build();