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 cd0e602c5..42b1934a7 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 @@ -135,4 +135,10 @@ public static CardBlock card(ModelConfigurator confi return configurator.configure(CardBlock.builder()).build(); } + // CarouselBlock + + public static CarouselBlock carousel(ModelConfigurator configurator) { + return configurator.configure(CarouselBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java new file mode 100644 index 000000000..335c53c79 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java @@ -0,0 +1,32 @@ +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; + +/** + * A carousel is a layout block used to display a horizontally scrollable collection of + * {@link CardBlock cards}. It must contain between 1 and 10 cards. + * + * https://docs.slack.dev/reference/block-kit/blocks/carousel-block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CarouselBlock implements LayoutBlock { + public static final String TYPE = "carousel"; + private final String type = TYPE; + + /** + * An array of {@link CardBlock card} blocks. Must contain between 1 and 10 cards. + */ + @Builder.Default + private List elements = new ArrayList<>(); + + private String blockId; +} 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 a4995ca74..1f006860a 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 @@ -63,6 +63,8 @@ private Class getLayoutClassInstance(String typeName) { return ShareShortcutBlock.class; case CardBlock.TYPE: return CardBlock.class; + case CarouselBlock.TYPE: + return CarouselBlock.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 cfc44ff3d..37ff67ec5 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 @@ -2034,4 +2034,115 @@ public void buildCardBlock() { assertThat(parsedCard.getBlockId(), is("card2")); assertThat(parsedCard.getActions().size(), is(1)); } + + @Test + public void parseCarouselBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/carousel-block + String json = "{\"blocks\":[\n" + + " {\n" + + " \"type\": \"carousel\",\n" + + " \"block_id\": \"carousel1\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"card\",\n" + + " \"block_id\": \"card1\",\n" + + " \"title\": {\n" + + " \"type\": \"mrkdwn\",\n" + + " \"text\": \"First card\"\n" + + " },\n" + + " \"body\": {\n" + + " \"type\": \"mrkdwn\",\n" + + " \"text\": \"Body of the first card.\"\n" + + " },\n" + + " \"hero_image\": {\n" + + " \"type\": \"image\",\n" + + " \"image_url\": \"https://picsum.photos/400/300\",\n" + + " \"alt_text\": \"hero\"\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"type\": \"button\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Open\"\n" + + " },\n" + + " \"action_id\": \"open\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"type\": \"card\",\n" + + " \"title\": {\n" + + " \"type\": \"mrkdwn\",\n" + + " \"text\": \"Second card\"\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)); + + CarouselBlock carousel = (CarouselBlock) message.getBlocks().get(0); + assertThat(carousel.getType(), is("carousel")); + assertThat(carousel.getBlockId(), is("carousel1")); + assertThat(carousel.getElements().size(), is(2)); + + CardBlock firstCard = carousel.getElements().get(0); + assertThat(firstCard.getType(), is("card")); + assertThat(firstCard.getBlockId(), is("card1")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) firstCard.getTitle()).getText(), is("First card")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) firstCard.getBody()).getText(), is("Body of the first card.")); + assertThat(firstCard.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300")); + assertThat(firstCard.getActions().size(), is(1)); + ButtonElement button = (ButtonElement) firstCard.getActions().get(0); + assertThat(button.getActionId(), is("open")); + + CardBlock secondCard = carousel.getElements().get(1); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) secondCard.getTitle()).getText(), is("Second card")); + assertThat(secondCard.getBlockId(), is(nullValue())); + + // Round-trip back to JSON and ensure the carousel survives serialization. + Message roundTrip = gson.fromJson(gson.toJson(message), Message.class); + CarouselBlock reparsed = (CarouselBlock) roundTrip.getBlocks().get(0); + assertThat(reparsed.getBlockId(), is("carousel1")); + assertThat(reparsed.getElements().size(), is(2)); + assertThat(reparsed.getElements().get(0).getType(), is("card")); + } + + @Test + public void buildCarouselBlock() { + CarouselBlock carousel = carousel(c -> c + .blockId("carousel1") + .elements(Arrays.asList( + card(card -> card + .blockId("card1") + .title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Promo").build()) + .body(com.slack.api.model.block.composition.PlainTextObject.builder().text("Check this out").build()) + .actions(Arrays.asList( + ButtonElement.builder() + .actionId("open") + .text(plainText("Open")) + .build() + ))), + card(card -> card + .title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Second").build())) + )) + ); + assertThat(carousel.getType(), is("carousel")); + assertThat(carousel.getElements().size(), is(2)); + + Gson gson = GsonFactory.createSnakeCase(); + Message message = new Message(); + message.setBlocks(asBlocks(carousel)); + Message reparsed = gson.fromJson(gson.toJson(message), Message.class); + CarouselBlock parsedCarousel = (CarouselBlock) reparsed.getBlocks().get(0); + assertThat(parsedCarousel.getBlockId(), is("carousel1")); + assertThat(parsedCarousel.getElements().size(), is(2)); + assertThat(parsedCarousel.getElements().get(0).getType(), is("card")); + assertThat(parsedCarousel.getElements().get(0).getBlockId(), is("card1")); + assertThat(parsedCarousel.getElements().get(0).getActions().size(), is(1)); + } }