From 038976e253c5d727c985928c760ac468d3563ca6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 26 Jun 2026 16:46:49 -0700 Subject: [PATCH 1/3] feat(model): add table Block Kit block Add support for the `table` layout block, which renders tabular data as a grid of rows and cells. Cells may be `raw_text`, `raw_number`, or a `rich_text` block, with optional per-column alignment/wrapping settings. - TableBlock (type "table") with rows, columnSettings, blockId - TableCell interface + RawTextTableCell, RawNumberTableCell impls; RichTextBlock now also implements TableCell for rich_text cells - TableColumnSetting (align, isWrapped) - GsonTableCellFactory for polymorphic cell (de)serialization, registered in GsonFactory; TableBlock registered in GsonLayoutBlockFactory - Blocks.table(...) builder helper - Tests covering parse, round-trip, and builder usage Docs: https://docs.slack.dev/reference/block-kit/blocks/table-block Co-Authored-By: Claude --- .../com/slack/api/util/json/GsonFactory.java | 2 + .../com/slack/api/model/block/Blocks.java | 6 ++ .../api/model/block/RawNumberTableCell.java | 23 +++++ .../api/model/block/RawTextTableCell.java | 21 +++++ .../slack/api/model/block/RichTextBlock.java | 2 +- .../com/slack/api/model/block/TableBlock.java | 43 +++++++++ .../com/slack/api/model/block/TableCell.java | 18 ++++ .../api/model/block/TableColumnSetting.java | 28 ++++++ .../api/util/json/GsonLayoutBlockFactory.java | 2 + .../api/util/json/GsonTableCellFactory.java | 56 ++++++++++++ .../api/model/block/BlockKitTest.java | 90 +++++++++++++++++++ .../java/test_locally/unit/GsonFactory.java | 2 + 12 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java create mode 100644 slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java diff --git a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java index 4468f2df4..3a25b3af3 100644 --- a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java +++ b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java @@ -11,6 +11,7 @@ import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.TableCell; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; @@ -79,6 +80,7 @@ public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnkno .registerTypeAdapter(ContextActionsBlockElement.class, new GsonContextActionsBlockElementFactory(failOnUnknownProps)) .registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProps)) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProps)) + .registerTypeAdapter(TableCell.class, new GsonTableCellFactory(failOnUnknownProps)) .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory()) .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProps)) .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProps)) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index fd451d8d2..b2e730fe9 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -129,4 +129,10 @@ public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.builder().build(); } + // TableBlock + + public static TableBlock table(ModelConfigurator configurator) { + return configurator.configure(TableBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java new file mode 100644 index 000000000..0264b068f --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java @@ -0,0 +1,23 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A {@code raw_number} table cell, holding a numeric value. The optional {@code text} + * field carries the display representation of the value (for example a formatted string). + * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawNumberTableCell implements TableCell { + public static final String TYPE = "raw_number"; + private final String type = TYPE; + private Double value; + private String text; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java new file mode 100644 index 000000000..4bd36e77c --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java @@ -0,0 +1,21 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A {@code raw_text} table cell, holding unformatted plain text. + * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawTextTableCell implements TableCell { + public static final String TYPE = "raw_text"; + private final String type = TYPE; + private String text; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java index ad204e225..ac0476e29 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class RichTextBlock implements LayoutBlock { +public class RichTextBlock implements LayoutBlock, TableCell { public static final String TYPE = "rich_text"; private final String type = TYPE; @Builder.Default diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java new file mode 100644 index 000000000..61debad48 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java @@ -0,0 +1,43 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays tabular data as a grid of rows and cells. + * + *

Each row is a list of {@link TableCell cells}; a cell may be a + * {@link RawTextTableCell} ({@code raw_text}), {@link RawNumberTableCell} + * ({@code raw_number}), or a {@link RichTextBlock} ({@code rich_text}). A table + * supports up to 100 rows and up to 20 cells per row, with optional per-column + * behavior configured via {@link TableColumnSetting column settings}.

+ * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TableBlock implements LayoutBlock { + public static final String TYPE = "table"; + private final String type = TYPE; + + /** + * The rows of the table. Each row is a list of cells. Maximum 100 rows, with up to + * 20 cells per row. + */ + @Builder.Default + private List> rows = new ArrayList<>(); + + /** + * Per-column behavior configuration (alignment, wrapping). Maximum 20 items. + */ + private List columnSettings; + + private String blockId; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java new file mode 100644 index 000000000..27cda7555 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java @@ -0,0 +1,18 @@ +package com.slack.api.model.block; + +/** + * A single cell within a {@link TableBlock} row. A cell can be one of: + * + *
    + *
  • {@link RawTextTableCell} ({@code raw_text})
  • + *
  • {@link RawNumberTableCell} ({@code raw_number})
  • + *
  • {@link com.slack.api.model.block.RichTextBlock RichTextBlock} ({@code rich_text})
  • + *
+ * + * @see Table block + */ +public interface TableCell { + + String getType(); + +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java new file mode 100644 index 000000000..5e0411116 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java @@ -0,0 +1,28 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Describes the behavior of a single column in a {@link TableBlock}. Up to 20 column + * settings can be supplied, one per column. + * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TableColumnSetting { + /** + * Horizontal text alignment for the column. One of {@code left}, {@code center}, + * or {@code right}. Defaults to {@code left}. + */ + private String align; + /** + * Whether text in the column wraps onto multiple lines. Defaults to {@code false}. + */ + private Boolean isWrapped; +} diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java index 9286e6b51..810d5a1b7 100644 --- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java @@ -61,6 +61,8 @@ private Class getLayoutClassInstance(String typeName) { return RichTextBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; + case TableBlock.TYPE: + return TableBlock.class; default: if (failOnUnknownProperties) { throw new JsonParseException("Unsupported layout block type: " + typeName); diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java new file mode 100644 index 000000000..176f80c01 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java @@ -0,0 +1,56 @@ +package com.slack.api.util.json; + +import com.google.gson.*; +import com.slack.api.model.block.RawNumberTableCell; +import com.slack.api.model.block.RawTextTableCell; +import com.slack.api.model.block.RichTextBlock; +import com.slack.api.model.block.TableCell; + +import java.lang.reflect.Type; + +/** + * Factory for deserializing the cells of a + * {@link com.slack.api.model.block.TableBlock table block}. + * + * @see Table block + */ +public class GsonTableCellFactory implements JsonDeserializer, JsonSerializer { + + private final boolean failOnUnknownProperties; + + public GsonTableCellFactory() { + this(false); + } + + public GsonTableCellFactory(boolean failOnUnknownProperties) { + this.failOnUnknownProperties = failOnUnknownProperties; + } + + @Override + public TableCell deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); + final JsonPrimitive prim = (JsonPrimitive) jsonObject.get("type"); + final String typeName = prim.getAsString(); + final Class clazz = getTableCellClassInstance(typeName); + return context.deserialize(jsonObject, clazz); + } + + @Override + public JsonElement serialize(TableCell src, Type typeOfSrc, JsonSerializationContext context) { + return context.serialize(src); + } + + private Class getTableCellClassInstance(String typeName) { + switch (typeName) { + case RawTextTableCell.TYPE: + return RawTextTableCell.class; + case RawNumberTableCell.TYPE: + return RawNumberTableCell.class; + case RichTextBlock.TYPE: + return RichTextBlock.class; + default: + throw new JsonParseException("Unknown table cell type: " + typeName); + } + } +} diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 1fc12d58d..aa698a375 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -1940,4 +1940,94 @@ public void parseRichTextElements() { RichTextSectionElement section = (RichTextSectionElement) ((RichTextBlock) message.getBlocks().get(0)).getElements().get(0); assertThat(section.getElements().size(), is(10)); } + + @Test + public void parseTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"table\",\n" + + " \"block_id\": \"table1\",\n" + + " \"column_settings\": [\n" + + " { \"align\": \"left\", \"is_wrapped\": false },\n" + + " { \"align\": \"right\", \"is_wrapped\": true }\n" + + " ],\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Gson gson = GsonFactory.createSnakeCase(); + Message message = gson.fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + + TableBlock table = (TableBlock) message.getBlocks().get(0); + assertThat(table.getType(), is("table")); + assertThat(table.getBlockId(), is("table1")); + + assertThat(table.getColumnSettings().size(), is(2)); + assertThat(table.getColumnSettings().get(0).getAlign(), is("left")); + assertThat(table.getColumnSettings().get(0).getIsWrapped(), is(false)); + assertThat(table.getColumnSettings().get(1).getAlign(), is("right")); + assertThat(table.getColumnSettings().get(1).getIsWrapped(), is(true)); + + assertThat(table.getRows().size(), is(2)); + + RawTextTableCell header0 = (RawTextTableCell) table.getRows().get(0).get(0); + assertThat(header0.getType(), is(RawTextTableCell.TYPE)); + assertThat(header0.getText(), is("Item")); + + RichTextBlock richCell = (RichTextBlock) table.getRows().get(1).get(0); + assertThat(richCell.getType(), is(RichTextBlock.TYPE)); + RichTextSectionElement richSection = (RichTextSectionElement) richCell.getElements().get(0); + RichTextSectionElement.Text richText = (RichTextSectionElement.Text) richSection.getElements().get(0); + assertThat(richText.getText(), is("Rent")); + assertThat(richText.getStyle().isBold(), is(true)); + + RawNumberTableCell numberCell = (RawNumberTableCell) table.getRows().get(1).get(1); + assertThat(numberCell.getType(), is(RawNumberTableCell.TYPE)); + assertThat(numberCell.getValue(), is(400.0)); + assertThat(numberCell.getText(), is("$400")); + + // round-trips back to a table block + String output = gson.toJson(table); + TableBlock reparsed = gson.fromJson(output, TableBlock.class); + assertThat(reparsed.getRows().size(), is(2)); + assertThat(((RawTextTableCell) reparsed.getRows().get(0).get(1)).getText(), is("Cost")); + } + + @Test + public void buildTableBlock() { + TableBlock table = Blocks.table(t -> t + .blockId("b") + .columnSettings(Arrays.asList( + TableColumnSetting.builder().align("center").isWrapped(true).build())) + .rows(Arrays.asList( + Arrays.asList( + RawTextTableCell.builder().text("Header").build(), + RawNumberTableCell.builder().value(1.0).text("1").build())))); + assertThat(table, is(notNullValue())); + assertThat(table.getType(), is("table")); + assertThat(table.getRows().get(0).size(), is(2)); + } } diff --git a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java index 0d30bab1c..4d3b1c5fa 100644 --- a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java +++ b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java @@ -8,6 +8,7 @@ import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.TableCell; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; @@ -39,6 +40,7 @@ public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unkn .registerTypeAdapter(ContextActionsBlockElement.class, new GsonContextActionsBlockElementFactory(failOnUnknownProperties)) .registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProperties)) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProperties)) + .registerTypeAdapter(TableCell.class, new GsonTableCellFactory(failOnUnknownProperties)) .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory(failOnUnknownProperties)) .registerTypeAdapter(Attachment.VideoHtml.class, From cdf0bf743f45bf7ef8e82ac645e942c1579a2639 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 28 Aug 2026 13:28:49 -0400 Subject: [PATCH 2/3] fix: resolve merge conflict errors in Blocks.java and BlockKitTest.java --- .../com/slack/api/model/block/Blocks.java | 1 + .../api/model/block/BlockKitTest.java | 126 ++++++++++-------- 2 files changed, 68 insertions(+), 59 deletions(-) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index b3f17b4d4..ad6160fc1 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -151,5 +151,6 @@ public static ShareShortcutBlock shareShortcut() { public static TableBlock table(ModelConfigurator configurator) { return configurator.configure(TableBlock.builder()).build(); + } } diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 55a0996ab..fef740e75 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -1999,40 +1999,6 @@ public void parseRichTextElements() { } @Test - public void parseTableBlock() { - // https://docs.slack.dev/reference/block-kit/blocks/table-block - String json = "{\n" + - " \"blocks\": [\n" + - " {\n" + - " \"type\": \"table\",\n" + - " \"block_id\": \"table1\",\n" + - " \"column_settings\": [\n" + - " { \"align\": \"left\", \"is_wrapped\": false },\n" + - " { \"align\": \"right\", \"is_wrapped\": true }\n" + - " ],\n" + - " \"rows\": [\n" + - " [\n" + - " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + - " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + - " ],\n" + - " [\n" + - " {\n" + - " \"type\": \"rich_text\",\n" + - " \"elements\": [\n" + - " {\n" + - " \"type\": \"rich_text_section\",\n" + - " \"elements\": [\n" + - " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + - " ]\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; public void parseCardBlock() { // https://docs.slack.dev/reference/block-kit/blocks/card-block String json = "{\"blocks\":[\n" + @@ -2078,6 +2044,70 @@ public void parseCardBlock() { assertThat(message, is(notNullValue())); assertThat(message.getBlocks().size(), is(1)); + CardBlock card = (CardBlock) message.getBlocks().get(0); + assertThat(card.getType(), is("card")); + assertThat(card.getBlockId(), is("card1")); + assertThat(card.getIcon().getImageUrl(), is("https://picsum.photos/36/36")); + assertThat(card.getIcon().getAltText(), is("Icon")); + assertThat(card.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300")); + assertThat(card.getHeroImage().getAltText(), is("Sample hero image")); + assertThat(card.getTitle().getType(), is("mrkdwn")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getTitle()).getText(), is("Lumon Industries")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getSubtitle()).getText(), is("Committed to work-life balance")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getBody()).getText(), is("Please enjoy each card equally.")); + assertThat(card.getActions().size(), is(1)); + ButtonElement button = (ButtonElement) card.getActions().get(0); + assertThat(button.getActionId(), is("button_action")); + assertThat(button.getText().getText(), is("Action Button")); + + // Round-trip back to JSON and ensure the card survives serialization. + Message roundTrip = gson.fromJson(gson.toJson(message), Message.class); + CardBlock reparsed = (CardBlock) roundTrip.getBlocks().get(0); + assertThat(reparsed.getBlockId(), is("card1")); + assertThat(reparsed.getActions().size(), is(1)); + assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action")); + } + + @Test + public void parseTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"table\",\n" + + " \"block_id\": \"table1\",\n" + + " \"column_settings\": [\n" + + " { \"align\": \"left\", \"is_wrapped\": false },\n" + + " { \"align\": \"right\", \"is_wrapped\": true }\n" + + " ],\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Gson gson = GsonFactory.createSnakeCase(); + Message message = gson.fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + TableBlock table = (TableBlock) message.getBlocks().get(0); assertThat(table.getType(), is("table")); assertThat(table.getBlockId(), is("table1")); @@ -2108,9 +2138,9 @@ public void parseCardBlock() { // round-trips back to a table block String output = gson.toJson(table); - TableBlock reparsed = gson.fromJson(output, TableBlock.class); - assertThat(reparsed.getRows().size(), is(2)); - assertThat(((RawTextTableCell) reparsed.getRows().get(0).get(1)).getText(), is("Cost")); + TableBlock reparsedTable = gson.fromJson(output, TableBlock.class); + assertThat(reparsedTable.getRows().size(), is(2)); + assertThat(((RawTextTableCell) reparsedTable.getRows().get(0).get(1)).getText(), is("Cost")); } @Test @@ -2126,28 +2156,6 @@ public void buildTableBlock() { assertThat(table, is(notNullValue())); assertThat(table.getType(), is("table")); assertThat(table.getRows().get(0).size(), is(2)); - CardBlock card = (CardBlock) message.getBlocks().get(0); - assertThat(card.getType(), is("card")); - assertThat(card.getBlockId(), is("card1")); - assertThat(card.getIcon().getImageUrl(), is("https://picsum.photos/36/36")); - assertThat(card.getIcon().getAltText(), is("Icon")); - assertThat(card.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300")); - assertThat(card.getHeroImage().getAltText(), is("Sample hero image")); - assertThat(card.getTitle().getType(), is("mrkdwn")); - assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getTitle()).getText(), is("Lumon Industries")); - assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getSubtitle()).getText(), is("Committed to work-life balance")); - assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getBody()).getText(), is("Please enjoy each card equally.")); - assertThat(card.getActions().size(), is(1)); - ButtonElement button = (ButtonElement) card.getActions().get(0); - assertThat(button.getActionId(), is("button_action")); - assertThat(button.getText().getText(), is("Action Button")); - - // Round-trip back to JSON and ensure the card survives serialization. - Message roundTrip = gson.fromJson(gson.toJson(message), Message.class); - CardBlock reparsed = (CardBlock) roundTrip.getBlocks().get(0); - assertThat(reparsed.getBlockId(), is("card1")); - assertThat(reparsed.getActions().size(), is(1)); - assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action")); } @Test From 2f5a86e28886bbe4e7d12d655129ee6b2ee574c0 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 28 Aug 2026 13:35:47 -0400 Subject: [PATCH 3/3] style: alphabetize block methods, switch cases, and tests for readability --- .../com/slack/api/model/block/Blocks.java | 12 +- .../api/util/json/GsonLayoutBlockFactory.java | 40 ++-- .../api/model/block/BlockKitTest.java | 180 +++++++++--------- 3 files changed, 117 insertions(+), 115 deletions(-) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index ad6160fc1..0a593ca96 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -137,12 +137,8 @@ public static SectionBlock section(ModelConfigurator configurator) { - return configurator.configure(VideoBlock.builder()).build(); - } - // ShareShortcutBlock + public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.builder().build(); } @@ -153,4 +149,10 @@ public static TableBlock table(ModelConfigurator c return configurator.configure(TableBlock.builder()).build(); } + // VideoBlock + + public static VideoBlock video(ModelConfigurator configurator) { + return configurator.configure(VideoBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java index 3618b5666..62c304c98 100644 --- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java @@ -33,42 +33,42 @@ public LayoutBlock deserialize(JsonElement json, Type typeOfT, JsonDeserializati private Class getLayoutClassInstance(String typeName) { switch (typeName) { - case SectionBlock.TYPE: - return SectionBlock.class; - case DividerBlock.TYPE: - return DividerBlock.class; - case ImageBlock.TYPE: - return ImageBlock.class; + case ActionsBlock.TYPE: + return ActionsBlock.class; + case AlertBlock.TYPE: + return AlertBlock.class; + case CallBlock.TYPE: + return CallBlock.class; + case CardBlock.TYPE: + return CardBlock.class; + case CarouselBlock.TYPE: + return CarouselBlock.class; case ContextBlock.TYPE: return ContextBlock.class; case ContextActionsBlock.TYPE: return ContextActionsBlock.class; - case CallBlock.TYPE: - return CallBlock.class; - case ActionsBlock.TYPE: - return ActionsBlock.class; + case DividerBlock.TYPE: + return DividerBlock.class; case FileBlock.TYPE: return FileBlock.class; - case InputBlock.TYPE: - return InputBlock.class; case HeaderBlock.TYPE: return HeaderBlock.class; + case ImageBlock.TYPE: + return ImageBlock.class; + case InputBlock.TYPE: + return InputBlock.class; case MarkdownBlock.TYPE: return MarkdownBlock.class; - case VideoBlock.TYPE: - return VideoBlock.class; case RichTextBlock.TYPE: return RichTextBlock.class; + case SectionBlock.TYPE: + return SectionBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; case TableBlock.TYPE: return TableBlock.class; - case AlertBlock.TYPE: - return AlertBlock.class; - case CardBlock.TYPE: - return CardBlock.class; - case CarouselBlock.TYPE: - return CarouselBlock.class; + case VideoBlock.TYPE: + return VideoBlock.class; default: if (failOnUnknownProperties) { throw new JsonParseException("Unsupported layout block type: " + typeName); diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index fef740e75..ef93a1c01 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -2068,96 +2068,6 @@ public void parseCardBlock() { assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action")); } - @Test - public void parseTableBlock() { - // https://docs.slack.dev/reference/block-kit/blocks/table-block - String json = "{\n" + - " \"blocks\": [\n" + - " {\n" + - " \"type\": \"table\",\n" + - " \"block_id\": \"table1\",\n" + - " \"column_settings\": [\n" + - " { \"align\": \"left\", \"is_wrapped\": false },\n" + - " { \"align\": \"right\", \"is_wrapped\": true }\n" + - " ],\n" + - " \"rows\": [\n" + - " [\n" + - " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + - " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + - " ],\n" + - " [\n" + - " {\n" + - " \"type\": \"rich_text\",\n" + - " \"elements\": [\n" + - " {\n" + - " \"type\": \"rich_text_section\",\n" + - " \"elements\": [\n" + - " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + - " ]\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - Gson gson = GsonFactory.createSnakeCase(); - Message message = gson.fromJson(json, Message.class); - assertThat(message, is(notNullValue())); - assertThat(message.getBlocks().size(), is(1)); - - TableBlock table = (TableBlock) message.getBlocks().get(0); - assertThat(table.getType(), is("table")); - assertThat(table.getBlockId(), is("table1")); - - assertThat(table.getColumnSettings().size(), is(2)); - assertThat(table.getColumnSettings().get(0).getAlign(), is("left")); - assertThat(table.getColumnSettings().get(0).getIsWrapped(), is(false)); - assertThat(table.getColumnSettings().get(1).getAlign(), is("right")); - assertThat(table.getColumnSettings().get(1).getIsWrapped(), is(true)); - - assertThat(table.getRows().size(), is(2)); - - RawTextTableCell header0 = (RawTextTableCell) table.getRows().get(0).get(0); - assertThat(header0.getType(), is(RawTextTableCell.TYPE)); - assertThat(header0.getText(), is("Item")); - - RichTextBlock richCell = (RichTextBlock) table.getRows().get(1).get(0); - assertThat(richCell.getType(), is(RichTextBlock.TYPE)); - RichTextSectionElement richSection = (RichTextSectionElement) richCell.getElements().get(0); - RichTextSectionElement.Text richText = (RichTextSectionElement.Text) richSection.getElements().get(0); - assertThat(richText.getText(), is("Rent")); - assertThat(richText.getStyle().isBold(), is(true)); - - RawNumberTableCell numberCell = (RawNumberTableCell) table.getRows().get(1).get(1); - assertThat(numberCell.getType(), is(RawNumberTableCell.TYPE)); - assertThat(numberCell.getValue(), is(400.0)); - assertThat(numberCell.getText(), is("$400")); - - // round-trips back to a table block - String output = gson.toJson(table); - TableBlock reparsedTable = gson.fromJson(output, TableBlock.class); - assertThat(reparsedTable.getRows().size(), is(2)); - assertThat(((RawTextTableCell) reparsedTable.getRows().get(0).get(1)).getText(), is("Cost")); - } - - @Test - public void buildTableBlock() { - TableBlock table = Blocks.table(t -> t - .blockId("b") - .columnSettings(Arrays.asList( - TableColumnSetting.builder().align("center").isWrapped(true).build())) - .rows(Arrays.asList( - Arrays.asList( - RawTextTableCell.builder().text("Header").build(), - RawNumberTableCell.builder().value(1.0).text("1").build())))); - assertThat(table, is(notNullValue())); - assertThat(table.getType(), is("table")); - assertThat(table.getRows().get(0).size(), is(2)); - } - @Test public void buildCardBlock() { CardBlock card = card(c -> c @@ -2292,4 +2202,94 @@ public void buildCarouselBlock() { assertThat(parsedCarousel.getElements().get(0).getBlockId(), is("card1")); assertThat(parsedCarousel.getElements().get(0).getActions().size(), is(1)); } + + @Test + public void parseTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"table\",\n" + + " \"block_id\": \"table1\",\n" + + " \"column_settings\": [\n" + + " { \"align\": \"left\", \"is_wrapped\": false },\n" + + " { \"align\": \"right\", \"is_wrapped\": true }\n" + + " ],\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Gson gson = GsonFactory.createSnakeCase(); + Message message = gson.fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + + TableBlock table = (TableBlock) message.getBlocks().get(0); + assertThat(table.getType(), is("table")); + assertThat(table.getBlockId(), is("table1")); + + assertThat(table.getColumnSettings().size(), is(2)); + assertThat(table.getColumnSettings().get(0).getAlign(), is("left")); + assertThat(table.getColumnSettings().get(0).getIsWrapped(), is(false)); + assertThat(table.getColumnSettings().get(1).getAlign(), is("right")); + assertThat(table.getColumnSettings().get(1).getIsWrapped(), is(true)); + + assertThat(table.getRows().size(), is(2)); + + RawTextTableCell header0 = (RawTextTableCell) table.getRows().get(0).get(0); + assertThat(header0.getType(), is(RawTextTableCell.TYPE)); + assertThat(header0.getText(), is("Item")); + + RichTextBlock richCell = (RichTextBlock) table.getRows().get(1).get(0); + assertThat(richCell.getType(), is(RichTextBlock.TYPE)); + RichTextSectionElement richSection = (RichTextSectionElement) richCell.getElements().get(0); + RichTextSectionElement.Text richText = (RichTextSectionElement.Text) richSection.getElements().get(0); + assertThat(richText.getText(), is("Rent")); + assertThat(richText.getStyle().isBold(), is(true)); + + RawNumberTableCell numberCell = (RawNumberTableCell) table.getRows().get(1).get(1); + assertThat(numberCell.getType(), is(RawNumberTableCell.TYPE)); + assertThat(numberCell.getValue(), is(400.0)); + assertThat(numberCell.getText(), is("$400")); + + // round-trips back to a table block + String output = gson.toJson(table); + TableBlock reparsedTable = gson.fromJson(output, TableBlock.class); + assertThat(reparsedTable.getRows().size(), is(2)); + assertThat(((RawTextTableCell) reparsedTable.getRows().get(0).get(1)).getText(), is("Cost")); + } + + @Test + public void buildTableBlock() { + TableBlock table = Blocks.table(t -> t + .blockId("b") + .columnSettings(Arrays.asList( + TableColumnSetting.builder().align("center").isWrapped(true).build())) + .rows(Arrays.asList( + Arrays.asList( + RawTextTableCell.builder().text("Header").build(), + RawNumberTableCell.builder().value(1.0).text("1").build())))); + assertThat(table, is(notNullValue())); + assertThat(table.getType(), is("table")); + assertThat(table.getRows().get(0).size(), is(2)); + } }