From 0c0929d6cf3d5446d0a9f6272c5986e22fd4a434 Mon Sep 17 00:00:00 2001 From: Victor Li Date: Wed, 17 Jun 2026 14:59:00 -0400 Subject: [PATCH 1/4] introducing card block and slack icon object wrappers Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../slack/client/models/blocks/Block.java | 1 + .../blocks/BlockElementLengthLimits.java | 6 +- .../slack/client/models/blocks/CardIF.java | 112 +++++++++++ .../blocks/objects/SlackIconObjectIF.java | 22 +++ .../client/models/blocks/CardBlockTest.java | 186 ++++++++++++++++++ slack-base/src/test/resources/card_block.json | 124 ++++++++++++ 6 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java create mode 100644 slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java create mode 100644 slack-base/src/test/resources/card_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..c23751cf 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,7 @@ value = ContextActionsBlock.class, name = ContextActionsBlock.TYPE ), + @JsonSubTypes.Type(value = Card.class, name = Card.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..2aef46e0 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,11 @@ 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_CARD_TITLE_LENGTH(150), + MAX_CARD_BODY_LENGTH(200), + MAX_CARD_ACTIONS_COUNT(3), + MAX_CARD_BLOCK_ID_LENGTH(255); private final int limit; diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java new file mode 100644 index 00000000..e5972484 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java @@ -0,0 +1,112 @@ +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.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.elements.BlockElement; +import com.hubspot.slack.client.models.blocks.elements.Image; +import com.hubspot.slack.client.models.blocks.objects.SlackIconObject; +import com.hubspot.slack.client.models.blocks.objects.Text; +import java.util.List; +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 CardIF extends Block { + String TYPE = "card"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + Optional getHeroImage(); + + Optional getIcon(); + + Optional getTitle(); + + Optional getSubtitle(); + + Optional getBody(); + + Optional getSubtext(); + + Optional> getActions(); + + Optional getSlackIcon(); + + @Check + default void check() { + Preconditions.checkState( + getHeroImage().isPresent() || + getTitle().isPresent() || + getActions().isPresent() || + getBody().isPresent(), + "A card block must have at least one of: hero_image, title, actions, or body" + ); + Preconditions.checkState( + !(getIcon().isPresent() && getSlackIcon().isPresent()), + "A card block cannot have both icon and slack_icon" + ); + getTitle() + .ifPresent(title -> + Preconditions.checkState( + title.getText().length() <= + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit(), + "title cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit() + ) + ); + getSubtitle() + .ifPresent(subtitle -> + Preconditions.checkState( + subtitle.getText().length() <= + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit(), + "subtitle cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit() + ) + ); + getBody() + .ifPresent(body -> + Preconditions.checkState( + body.getText().length() <= + BlockElementLengthLimits.MAX_CARD_BODY_LENGTH.getLimit(), + "body cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_BODY_LENGTH.getLimit() + ) + ); + getSubtext() + .ifPresent(subtext -> + Preconditions.checkState( + subtext.getText().length() <= + BlockElementLengthLimits.MAX_CARD_BODY_LENGTH.getLimit(), + "subtext cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_BODY_LENGTH.getLimit() + ) + ); + getActions() + .ifPresent(actions -> + Preconditions.checkState( + actions.size() <= BlockElementLengthLimits.MAX_CARD_ACTIONS_COUNT.getLimit(), + "A card block cannot have more than %s actions", + BlockElementLengthLimits.MAX_CARD_ACTIONS_COUNT.getLimit() + ) + ); + getBlockId() + .ifPresent(blockId -> + Preconditions.checkState( + blockId.length() <= + BlockElementLengthLimits.MAX_CARD_BLOCK_ID_LENGTH.getLimit(), + "block_id cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_BLOCK_ID_LENGTH.getLimit() + ) + ); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java new file mode 100644 index 00000000..cc8e7e58 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java @@ -0,0 +1,22 @@ +package com.hubspot.slack.client.models.blocks.objects; + +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 SlackIconObjectIF extends CompositionObject { + String TYPE = "icon"; + + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getName(); +} diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java new file mode 100644 index 00000000..4b2139db --- /dev/null +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java @@ -0,0 +1,186 @@ +package com.hubspot.slack.client.models.blocks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hubspot.slack.client.jackson.ObjectMapperUtils; +import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.elements.Button; +import com.hubspot.slack.client.models.blocks.elements.Image; +import com.hubspot.slack.client.models.blocks.objects.SlackIconObject; +import com.hubspot.slack.client.models.blocks.objects.Text; +import com.hubspot.slack.client.models.blocks.objects.TextType; +import java.io.IOException; +import java.util.List; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CardBlockTest { + + private static final ObjectMapper MAPPER = ObjectMapperUtils.mapper(); + private static final int SLACK_ICON_WITH_ACTIONS_INDEX = 0; + private static final int HERO_IMAGE_INDEX = 1; + private static final int ICON_WITH_ACTIONS_INDEX = 2; + private static final int TITLE_ONLY_INDEX = 3; + private static final int ACTIONS_ONLY_INDEX = 4; + private static Card[] blocks; + + @BeforeClass + public static void loadBlocks() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("card_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + blocks = new Card[root.size()]; + for (int i = 0; i < root.size(); i++) { + blocks[i] = MAPPER.treeToValue(root.get(i), Card.class); + } + } + + @Test + public void itDeserializesAsCorrectBlockType() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("card_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + Block block = MAPPER.treeToValue(root.get(0), Block.class); + assertThat(block).isInstanceOf(Card.class); + } + + @Test + public void itDeserializesSlackIcon() { + Card block = blocks[SLACK_ICON_WITH_ACTIONS_INDEX]; + assertThat(block.getSlackIcon()).isPresent(); + SlackIconObject icon = block.getSlackIcon().get(); + assertThat(icon.getName()).isEqualTo("calendar"); + assertThat(icon.getType()).isEqualTo("icon"); + } + + @Test + public void itDeserializesTextFields() { + Card block = blocks[SLACK_ICON_WITH_ACTIONS_INDEX]; + assertThat(block.getTitle()).isPresent(); + assertThat(block.getTitle().get().getText()).isEqualTo("Meeting title"); + assertThat(block.getTitle().get().getType()).isEqualTo(TextType.MARKDOWN); + assertThat(block.getSubtitle()).isPresent(); + assertThat(block.getSubtitle().get().getText()).isEqualTo("This is a subtitle"); + assertThat(block.getBody()).isPresent(); + assertThat(block.getBody().get().getText()).isEqualTo("This is the body text."); + } + + @Test + public void itDeserializesActions() { + Card block = blocks[SLACK_ICON_WITH_ACTIONS_INDEX]; + assertThat(block.getActions()).isPresent(); + List actions = block.getActions().get(); + assertThat(actions).hasSize(3); + assertThat(actions.get(0)).isInstanceOf(Button.class); + assertThat(actions.get(1)).isInstanceOf(Button.class); + assertThat(actions.get(2)).isInstanceOf(Button.class); + } + + @Test + public void itDeserializesHeroImage() { + Card block = blocks[HERO_IMAGE_INDEX]; + assertThat(block.getHeroImage()).isPresent(); + Image heroImage = block.getHeroImage().get(); + assertThat(heroImage.getImageUrl()).isEqualTo("https://example.com/hero.png"); + assertThat(heroImage.getAltText()).isEqualTo("Sample hero image"); + } + + @Test + public void itDeserializesIconImage() { + Card block = blocks[ICON_WITH_ACTIONS_INDEX]; + assertThat(block.getIcon()).isPresent(); + Image icon = block.getIcon().get(); + assertThat(icon.getImageUrl()).isEqualTo("https://example.com/icon.png"); + assertThat(icon.getAltText()).isEqualTo("Icon"); + } + + @Test + public void itDeserializesTitleOnlyCard() { + Card block = blocks[TITLE_ONLY_INDEX]; + assertThat(block.getTitle()).isPresent(); + assertThat(block.getHeroImage()).isEmpty(); + assertThat(block.getBody()).isEmpty(); + assertThat(block.getActions()).isEmpty(); + } + + @Test + public void itDeserializesActionsOnlyCard() { + Card block = blocks[ACTIONS_ONLY_INDEX]; + assertThat(block.getActions()).isPresent(); + assertThat(block.getTitle()).isEmpty(); + assertThat(block.getHeroImage()).isEmpty(); + assertThat(block.getBody()).isEmpty(); + } + + @Test + public void itSerializesAndDeserializes() throws IOException { + Card original = Card + .builder() + .setTitle(Text.of(TextType.MARKDOWN, "Test Title")) + .setBody(Text.of(TextType.MARKDOWN, "Test body")) + .build(); + String serialized = MAPPER.writeValueAsString(original); + Card deserialized = MAPPER.readValue(serialized, Card.class); + assertThat(deserialized).isEqualTo(original); + } + + @Test + public void itFailsWhenNoRequiredFieldsPresent() { + assertThatThrownBy(() -> Card.builder().build()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("at least one of"); + } + + @Test + public void itFailsWhenIconAndSlackIconBothPresent() { + assertThatThrownBy(() -> + Card + .builder() + .setTitle(Text.of(TextType.MARKDOWN, "Title")) + .setIcon(Image.of("https://example.com/icon.png", "icon")) + .setSlackIcon(SlackIconObject.of("rocket")) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot have both icon and slack_icon"); + } + + @Test + public void itFailsWhenTitleExceedsMaxLength() { + String longTitle = "a".repeat(151); + assertThatThrownBy(() -> + Card.builder().setTitle(Text.of(TextType.MARKDOWN, longTitle)).build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("title cannot exceed"); + } + + @Test + public void itFailsWhenBodyExceedsMaxLength() { + String longBody = "a".repeat(201); + assertThatThrownBy(() -> + Card.builder().setBody(Text.of(TextType.MARKDOWN, longBody)).build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("body cannot exceed"); + } + + @Test + public void itFailsWhenActionsExceedMaxCount() { + Button button = Button + .builder() + .setText(Text.of(TextType.PLAIN_TEXT, "Click")) + .setActionId("btn") + .build(); + assertThatThrownBy(() -> + Card + .builder() + .setTitle(Text.of(TextType.MARKDOWN, "Title")) + .setActions(List.of(button, button, button, button)) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot have more than"); + } +} diff --git a/slack-base/src/test/resources/card_block.json b/slack-base/src/test/resources/card_block.json new file mode 100644 index 00000000..0b59bd3c --- /dev/null +++ b/slack-base/src/test/resources/card_block.json @@ -0,0 +1,124 @@ +[ + { + "type": "card", + "slack_icon": { + "type": "icon", + "name": "calendar" + }, + "title": { + "type": "mrkdwn", + "text": "Meeting title", + "verbatim": false + }, + "subtitle": { + "type": "mrkdwn", + "text": "This is a subtitle", + "verbatim": false + }, + "body": { + "type": "mrkdwn", + "text": "This is the body text.", + "verbatim": false + }, + "actions": [ + { + "type": "button", + "style": "danger", + "text": { + "type": "plain_text", + "text": "Delete", + "emoji": false + }, + "action_id": "button_danger" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Cancel", + "emoji": false + }, + "action_id": "button_default" + }, + { + "type": "button", + "style": "primary", + "text": { + "type": "plain_text", + "text": "Confirm", + "emoji": false + }, + "action_id": "button_primary" + } + ] + }, + { + "type": "card", + "title": { + "type": "mrkdwn", + "text": "Card with hero image", + "verbatim": false + }, + "hero_image": { + "type": "image", + "image_url": "https://example.com/hero.png", + "alt_text": "Sample hero image" + }, + "body": { + "type": "mrkdwn", + "text": "Body text.", + "verbatim": false + } + }, + { + "type": "card", + "icon": { + "type": "image", + "image_url": "https://example.com/icon.png", + "alt_text": "Icon" + }, + "title": { + "type": "mrkdwn", + "text": "Card with icon and actions", + "verbatim": false + }, + "body": { + "type": "mrkdwn", + "text": "Body text.", + "verbatim": false + }, + "actions": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Action Button", + "emoji": false + }, + "action_id": "button_action" + } + ] + }, + { + "type": "card", + "title": { + "type": "mrkdwn", + "text": "Title only card", + "verbatim": false + } + }, + { + "type": "card", + "actions": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Action Button", + "emoji": false + }, + "action_id": "button_action" + } + ] + } +] From 35ca13a0af9a99f492f024aad782dbb1f3166a30 Mon Sep 17 00:00:00 2001 From: Victor Li Date: Mon, 22 Jun 2026 10:42:38 -0400 Subject: [PATCH 2/4] addressing comments --- .../slack/client/models/blocks/CardIF.java | 21 +++--- .../models/blocks/objects/SlackIconName.java | 71 +++++++++++++++++++ .../blocks/objects/SlackIconObjectIF.java | 2 +- .../client/models/blocks/CardBlockTest.java | 31 +++++--- 4 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java index e5972484..f166d0dd 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/CardIF.java @@ -1,14 +1,15 @@ package com.hubspot.slack.client.models.blocks; +import com.fasterxml.jackson.annotation.JsonInclude; 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.elements.BlockElement; import com.hubspot.slack.client.models.blocks.elements.Image; import com.hubspot.slack.client.models.blocks.objects.SlackIconObject; import com.hubspot.slack.client.models.blocks.objects.Text; -import java.util.List; import java.util.Optional; import org.immutables.value.Value; import org.immutables.value.Value.Check; @@ -17,6 +18,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_EMPTY) public interface CardIF extends Block { String TYPE = "card"; @@ -38,7 +40,7 @@ default String getType() { Optional getSubtext(); - Optional> getActions(); + ImmutableList getActions(); Optional getSlackIcon(); @@ -47,7 +49,7 @@ default void check() { Preconditions.checkState( getHeroImage().isPresent() || getTitle().isPresent() || - getActions().isPresent() || + !getActions().isEmpty() || getBody().isPresent(), "A card block must have at least one of: hero_image, title, actions, or body" ); @@ -91,14 +93,11 @@ default void check() { BlockElementLengthLimits.MAX_CARD_BODY_LENGTH.getLimit() ) ); - getActions() - .ifPresent(actions -> - Preconditions.checkState( - actions.size() <= BlockElementLengthLimits.MAX_CARD_ACTIONS_COUNT.getLimit(), - "A card block cannot have more than %s actions", - BlockElementLengthLimits.MAX_CARD_ACTIONS_COUNT.getLimit() - ) - ); + Preconditions.checkState( + getActions().size() <= BlockElementLengthLimits.MAX_CARD_ACTIONS_COUNT.getLimit(), + "A card block cannot have more than %s actions", + BlockElementLengthLimits.MAX_CARD_ACTIONS_COUNT.getLimit() + ); getBlockId() .ifPresent(blockId -> Preconditions.checkState( diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java new file mode 100644 index 00000000..fdc8e576 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java @@ -0,0 +1,71 @@ +package com.hubspot.slack.client.models.blocks.objects; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum SlackIconName { + ARCHIVE("archive"), + BOOK("book"), + BOOKMARK("bookmark"), + BOT("bot"), + BUG("bug"), + CALENDAR("calendar"), + CALL("call"), + CARET_LEFT("caret-left"), + CARET_RIGHT("caret-right"), + CHECK("check"), + CLIPBOARD("clipboard"), + CODE("code"), + COMMENT("comment"), + COMPASS("compass"), + COPY("copy"), + CUBE("cube"), + DOWNLOAD("download"), + EDIT("edit"), + EMAIL("email"), + EYE_CLOSED("eye-closed"), + EYE_OPEN("eye-open"), + FILE("file"), + FLAG("flag"), + FOLDER("folder"), + GEAR("gear"), + GLOBE("globe"), + HEART("heart"), + HELP("help"), + IMAGE("image"), + INFO("info"), + KEY("key"), + LIGHTBULB("lightbulb"), + LINK("link"), + MAP("map"), + MOBILE("mobile"), + NEW_WINDOW("new-window"), + PIN("pin"), + PLUS("plus"), + REFINE("refine"), + REFRESH("refresh"), + ROCKET("rocket"), + SAVE("save"), + SCREEN("screen"), + SHARE("share"), + SPARKLE("sparkle"), + STAR("star"), + STAR_FILLED("star-filled"), + TAG("tag"), + THUMBS_DOWN("thumbs-down"), + THUMBS_UP("thumbs-up"), + TRASH("trash"), + UPLOAD("upload"), + USER("user"), + WARNING("warning"); + + private final String value; + + SlackIconName(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/objects/SlackIconObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java index cc8e7e58..2601f3ba 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconObjectIF.java @@ -18,5 +18,5 @@ default String getType() { } @Value.Parameter - String getName(); + SlackIconName getName(); } diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java index 4b2139db..888faa91 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/CardBlockTest.java @@ -5,15 +5,16 @@ 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.Button; import com.hubspot.slack.client.models.blocks.elements.Image; +import com.hubspot.slack.client.models.blocks.objects.SlackIconName; import com.hubspot.slack.client.models.blocks.objects.SlackIconObject; import com.hubspot.slack.client.models.blocks.objects.Text; import com.hubspot.slack.client.models.blocks.objects.TextType; import java.io.IOException; -import java.util.List; import org.junit.BeforeClass; import org.junit.Test; @@ -50,10 +51,20 @@ public void itDeserializesSlackIcon() { Card block = blocks[SLACK_ICON_WITH_ACTIONS_INDEX]; assertThat(block.getSlackIcon()).isPresent(); SlackIconObject icon = block.getSlackIcon().get(); - assertThat(icon.getName()).isEqualTo("calendar"); + assertThat(icon.getName()).isEqualTo(SlackIconName.CALENDAR); + assertThat(icon.getName().getValue()).isEqualTo("calendar"); assertThat(icon.getType()).isEqualTo("icon"); } + @Test + public void itSerializesIconNameToHyphenatedWireValue() throws IOException { + SlackIconObject icon = SlackIconObject.of(SlackIconName.STAR_FILLED); + JsonNode node = MAPPER.readTree(MAPPER.writeValueAsString(icon)); + assertThat(node.get("name").asText()).isEqualTo("star-filled"); + assertThat(MAPPER.readValue(MAPPER.writeValueAsString(icon), SlackIconObject.class)) + .isEqualTo(icon); + } + @Test public void itDeserializesTextFields() { Card block = blocks[SLACK_ICON_WITH_ACTIONS_INDEX]; @@ -69,12 +80,10 @@ public void itDeserializesTextFields() { @Test public void itDeserializesActions() { Card block = blocks[SLACK_ICON_WITH_ACTIONS_INDEX]; - assertThat(block.getActions()).isPresent(); - List actions = block.getActions().get(); - assertThat(actions).hasSize(3); - assertThat(actions.get(0)).isInstanceOf(Button.class); - assertThat(actions.get(1)).isInstanceOf(Button.class); - assertThat(actions.get(2)).isInstanceOf(Button.class); + assertThat(block.getActions()).hasSize(3); + assertThat(block.getActions().get(0)).isInstanceOf(Button.class); + assertThat(block.getActions().get(1)).isInstanceOf(Button.class); + assertThat(block.getActions().get(2)).isInstanceOf(Button.class); } @Test @@ -107,7 +116,7 @@ public void itDeserializesTitleOnlyCard() { @Test public void itDeserializesActionsOnlyCard() { Card block = blocks[ACTIONS_ONLY_INDEX]; - assertThat(block.getActions()).isPresent(); + assertThat(block.getActions()).isNotEmpty(); assertThat(block.getTitle()).isEmpty(); assertThat(block.getHeroImage()).isEmpty(); assertThat(block.getBody()).isEmpty(); @@ -139,7 +148,7 @@ public void itFailsWhenIconAndSlackIconBothPresent() { .builder() .setTitle(Text.of(TextType.MARKDOWN, "Title")) .setIcon(Image.of("https://example.com/icon.png", "icon")) - .setSlackIcon(SlackIconObject.of("rocket")) + .setSlackIcon(SlackIconObject.of(SlackIconName.ROCKET)) .build() ) .isInstanceOf(IllegalStateException.class) @@ -177,7 +186,7 @@ public void itFailsWhenActionsExceedMaxCount() { Card .builder() .setTitle(Text.of(TextType.MARKDOWN, "Title")) - .setActions(List.of(button, button, button, button)) + .setActions(ImmutableList.of(button, button, button, button)) .build() ) .isInstanceOf(IllegalStateException.class) From caf260510a695ee2abd8150254f8b861c703ed77 Mon Sep 17 00:00:00 2001 From: Victor Li Date: Mon, 22 Jun 2026 10:45:23 -0400 Subject: [PATCH 3/4] adding link --- .../slack/client/models/blocks/objects/SlackIconName.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java index fdc8e576..445270e7 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java @@ -1,7 +1,9 @@ package com.hubspot.slack.client.models.blocks.objects; import com.fasterxml.jackson.annotation.JsonValue; - +/** + * Icon names derived from @slack docs + */ public enum SlackIconName { ARCHIVE("archive"), BOOK("book"), From 8bc633800a4145b5e92e77aaab58e3097f928395 Mon Sep 17 00:00:00 2001 From: Victor Li Date: Mon, 22 Jun 2026 10:46:56 -0400 Subject: [PATCH 4/4] fix --- .../slack/client/models/blocks/objects/SlackIconName.java | 1 + 1 file changed, 1 insertion(+) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java index 445270e7..d7dc27cb 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/SlackIconName.java @@ -1,6 +1,7 @@ package com.hubspot.slack.client.models.blocks.objects; import com.fasterxml.jackson.annotation.JsonValue; + /** * Icon names derived from @slack docs */