// Block elements: the interactive parts.
//
// https://docs.slack.dev/reference/block-kit/block-elements
//
// The ten select menus share one struct and the three pickers share another,
// keyed by a `kind`. Slack documents them separately, but their JSON differs
// only in which `initial_*` field appears -- and java-slack-sdk's own fixture
// corpus merges every observed shape of every element into one object anyway,
// so a struct per type would spend a thousand lines to model a distinction the
// data does not make. The builders below are per type, which is where the
// distinction is actually useful: `BlockElement::users_select` cannot be given
// an `initial_channel`.
///|
/// Which select menu this is.
pub(all) enum SelectKind {
Static
MultiStatic
External
MultiExternal
Users
MultiUsers
Conversations
MultiConversations
Channels
MultiChannels
} derive(Eq, Debug)
///|
pub fn SelectKind::type_name(self : Self) -> String {
match self {
Static => "static_select"
MultiStatic => "multi_static_select"
External => "external_select"
MultiExternal => "multi_external_select"
Users => "users_select"
MultiUsers => "multi_users_select"
Conversations => "conversations_select"
MultiConversations => "multi_conversations_select"
Channels => "channels_select"
MultiChannels => "multi_channels_select"
}
}
///|
pub fn SelectKind::of_name(name : String) -> SelectKind? {
match name {
"static_select" => Some(Static)
"multi_static_select" => Some(MultiStatic)
"external_select" => Some(External)
"multi_external_select" => Some(MultiExternal)
"users_select" => Some(Users)
"multi_users_select" => Some(MultiUsers)
"conversations_select" => Some(Conversations)
"multi_conversations_select" => Some(MultiConversations)
"channels_select" => Some(Channels)
"multi_channels_select" => Some(MultiChannels)
_ => None
}
}
///|
/// Any of the ten select menus.
pub(all) struct SelectElement {
kind : SelectKind
action_id : String?
placeholder : TextObject?
confirm : ConfirmationDialogObject?
focus_on_load : Bool?
/// `static_select` and `multi_static_select`.
options : Array[OptionObject]?
option_groups : Array[OptionGroupObject]?
initial_option : OptionObject?
initial_options : Array[OptionObject]?
initial_user : String?
initial_users : Array[String]?
initial_conversation : String?
initial_conversations : Array[String]?
initial_channel : String?
initial_channels : Array[String]?
/// `external_select`, `multi_external_select`.
min_query_length : Int?
/// The `multi_*` menus.
max_selected_items : Int?
/// Conversation menus: preselect whatever conversation the user is in.
default_to_current_conversation : Bool?
/// Conversation and channel menus in a message: Slack sends the selection to
/// a response URL.
response_url_enabled : Bool?
filter : ConversationFilterObject?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
fn SelectElement::blank(kind : SelectKind) -> SelectElement {
{
kind,
action_id: None,
placeholder: None,
confirm: None,
focus_on_load: None,
options: None,
option_groups: None,
initial_option: None,
initial_options: None,
initial_user: None,
initial_users: None,
initial_conversation: None,
initial_conversations: None,
initial_channel: None,
initial_channels: None,
min_query_length: None,
max_selected_items: None,
default_to_current_conversation: None,
response_url_enabled: None,
filter: None,
extra: Map([]),
}
}
///|
fn SelectElement::of_fields(
kind : SelectKind,
rest : Map[String, Json],
) -> SelectElement {
{
kind,
action_id: take_str(rest, "action_id"),
placeholder: take_obj(rest, "placeholder", TextObject::from_json),
confirm: take_obj(rest, "confirm", ConfirmationDialogObject::from_json),
focus_on_load: take_bool(rest, "focus_on_load"),
options: take_arr(rest, "options", OptionObject::from_json),
option_groups: take_arr(rest, "option_groups", OptionGroupObject::from_json),
initial_option: take_obj(rest, "initial_option", OptionObject::from_json),
initial_options: take_arr(rest, "initial_options", OptionObject::from_json),
initial_user: take_str(rest, "initial_user"),
initial_users: take_str_arr(rest, "initial_users"),
initial_conversation: take_str(rest, "initial_conversation"),
initial_conversations: take_str_arr(rest, "initial_conversations"),
initial_channel: take_str(rest, "initial_channel"),
initial_channels: take_str_arr(rest, "initial_channels"),
min_query_length: take_int(rest, "min_query_length"),
max_selected_items: take_int(rest, "max_selected_items"),
default_to_current_conversation: take_bool(
rest, "default_to_current_conversation",
),
response_url_enabled: take_bool(rest, "response_url_enabled"),
filter: take_obj(rest, "filter", ConversationFilterObject::from_json),
extra: rest,
}
}
///|
pub fn SelectElement::to_json(self : Self) -> Json {
let o = out_of(self.kind.type_name())
put_str(o, "action_id", self.action_id)
put_obj(o, "placeholder", self.placeholder, TextObject::to_json)
put_obj(o, "confirm", self.confirm, ConfirmationDialogObject::to_json)
put_bool(o, "focus_on_load", self.focus_on_load)
put_arr(o, "options", self.options, OptionObject::to_json)
put_arr(o, "option_groups", self.option_groups, OptionGroupObject::to_json)
put_obj(o, "initial_option", self.initial_option, OptionObject::to_json)
put_arr(o, "initial_options", self.initial_options, OptionObject::to_json)
put_str(o, "initial_user", self.initial_user)
put_str_arr(o, "initial_users", self.initial_users)
put_str(o, "initial_conversation", self.initial_conversation)
put_str_arr(o, "initial_conversations", self.initial_conversations)
put_str(o, "initial_channel", self.initial_channel)
put_str_arr(o, "initial_channels", self.initial_channels)
put_int(o, "min_query_length", self.min_query_length)
put_int(o, "max_selected_items", self.max_selected_items)
put_bool(
o,
"default_to_current_conversation",
self.default_to_current_conversation,
)
put_bool(o, "response_url_enabled", self.response_url_enabled)
put_obj(o, "filter", self.filter, ConversationFilterObject::to_json)
merge_extra(o, self.extra)
}
///|
/// A button.
pub(all) struct ButtonElement {
text : TextObject?
action_id : String?
/// A button with a `url` opens it; one without posts an interaction.
url : String?
value : String?
/// `primary` or `danger`.
style : String?
confirm : ConfirmationDialogObject?
/// Read by screen readers instead of the button's text.
accessibility_label : String?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn ButtonElement::to_json(self : Self) -> Json {
let o = out_of("button")
put_obj(o, "text", self.text, TextObject::to_json)
put_str(o, "action_id", self.action_id)
put_str(o, "url", self.url)
put_str(o, "value", self.value)
put_str(o, "style", self.style)
put_obj(o, "confirm", self.confirm, ConfirmationDialogObject::to_json)
put_str(o, "accessibility_label", self.accessibility_label)
merge_extra(o, self.extra)
}
///|
/// A button that runs a workflow directly, without a round trip to an app.
pub(all) struct WorkflowButtonElement {
text : TextObject?
action_id : String?
style : String?
accessibility_label : String?
/// The workflow and its trigger. Kept as raw JSON: the shape is a nested
/// trigger object with customisable inputs whose keys are defined by the
/// workflow itself, so there is nothing general to model.
workflow : Json?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn WorkflowButtonElement::to_json(self : Self) -> Json {
let o = out_of("workflow_button")
put_obj(o, "text", self.text, TextObject::to_json)
put_str(o, "action_id", self.action_id)
put_str(o, "style", self.style)
put_str(o, "accessibility_label", self.accessibility_label)
put_json(o, "workflow", self.workflow)
merge_extra(o, self.extra)
}
///|
/// An image, as an element rather than as a whole block.
pub(all) struct ImageElement {
/// One of `image_url` or `slack_file` is required by Slack; both are optional
/// here so a scrubbed fixture with neither still parses.
image_url : String?
slack_file : SlackFileObject?
alt_text : String?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn ImageElement::to_json(self : Self) -> Json {
let o = out_of("image")
put_str(o, "image_url", self.image_url)
put_obj(o, "slack_file", self.slack_file, SlackFileObject::to_json)
put_str(o, "alt_text", self.alt_text)
merge_extra(o, self.extra)
}
///|
/// Which option-list element this is.
pub(all) enum OptionsKind {
RadioButtons
Checkboxes
Overflow
} derive(Eq, Debug)
///|
pub fn OptionsKind::type_name(self : Self) -> String {
match self {
RadioButtons => "radio_buttons"
Checkboxes => "checkboxes"
Overflow => "overflow"
}
}
///|
pub fn OptionsKind::of_name(name : String) -> OptionsKind? {
match name {
"radio_buttons" => Some(RadioButtons)
"checkboxes" => Some(Checkboxes)
"overflow" => Some(Overflow)
_ => None
}
}
///|
/// Radio buttons, checkboxes and overflow menus: an inline list of options.
///
/// One struct because they differ only in whether one option or several may be
/// initially selected -- and an overflow menu, which is a menu rather than an
/// input, has neither.
pub(all) struct OptionsElement {
kind : OptionsKind
action_id : String?
options : Array[OptionObject]?
/// Radio buttons.
initial_option : OptionObject?
/// Checkboxes.
initial_options : Array[OptionObject]?
confirm : ConfirmationDialogObject?
focus_on_load : Bool?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn OptionsElement::to_json(self : Self) -> Json {
let o = out_of(self.kind.type_name())
put_str(o, "action_id", self.action_id)
put_arr(o, "options", self.options, OptionObject::to_json)
put_obj(o, "initial_option", self.initial_option, OptionObject::to_json)
put_arr(o, "initial_options", self.initial_options, OptionObject::to_json)
put_obj(o, "confirm", self.confirm, ConfirmationDialogObject::to_json)
put_bool(o, "focus_on_load", self.focus_on_load)
merge_extra(o, self.extra)
}
///|
/// Which date or time picker this is.
pub(all) enum PickerKind {
Date
Time
DateTime
} derive(Eq, Debug)
///|
pub fn PickerKind::type_name(self : Self) -> String {
match self {
Date => "datepicker"
Time => "timepicker"
DateTime => "datetimepicker"
}
}
///|
pub fn PickerKind::of_name(name : String) -> PickerKind? {
match name {
"datepicker" => Some(Date)
"timepicker" => Some(Time)
"datetimepicker" => Some(DateTime)
_ => None
}
}
///|
pub(all) struct PickerElement {
kind : PickerKind
action_id : String?
placeholder : TextObject?
confirm : ConfirmationDialogObject?
focus_on_load : Bool?
/// `YYYY-MM-DD`.
initial_date : String?
/// `HH:mm`, 24-hour.
initial_time : String?
/// Epoch seconds. `datetimepicker` has no placeholder, which is why Slack
/// keeps it a separate type.
initial_date_time : Int?
/// A tz database name, for `timepicker`.
timezone : String?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn PickerElement::to_json(self : Self) -> Json {
let o = out_of(self.kind.type_name())
put_str(o, "action_id", self.action_id)
put_obj(o, "placeholder", self.placeholder, TextObject::to_json)
put_obj(o, "confirm", self.confirm, ConfirmationDialogObject::to_json)
put_bool(o, "focus_on_load", self.focus_on_load)
put_str(o, "initial_date", self.initial_date)
put_str(o, "initial_time", self.initial_time)
put_int(o, "initial_date_time", self.initial_date_time)
put_str(o, "timezone", self.timezone)
merge_extra(o, self.extra)
}
///|
/// A free-text input.
pub(all) struct PlainTextInputElement {
action_id : String?
placeholder : TextObject?
initial_value : String?
multiline : Bool?
min_length : Int?
max_length : Int?
dispatch_action_config : DispatchActionConfigObject?
focus_on_load : Bool?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn PlainTextInputElement::to_json(self : Self) -> Json {
let o = out_of("plain_text_input")
put_str(o, "action_id", self.action_id)
put_obj(o, "placeholder", self.placeholder, TextObject::to_json)
put_str(o, "initial_value", self.initial_value)
put_bool(o, "multiline", self.multiline)
put_int(o, "min_length", self.min_length)
put_int(o, "max_length", self.max_length)
put_obj(
o,
"dispatch_action_config",
self.dispatch_action_config,
DispatchActionConfigObject::to_json,
)
put_bool(o, "focus_on_load", self.focus_on_load)
merge_extra(o, self.extra)
}
///|
/// A rich-text input: the composer, embedded in a modal.
pub(all) struct RichTextInputElement {
action_id : String?
placeholder : TextObject?
/// A `rich_text` block. Kept as raw JSON so that the input's value and the
/// block model cannot drift apart.
initial_value : Json?
dispatch_action_config : DispatchActionConfigObject?
focus_on_load : Bool?
min_lines : Int?
max_lines : Int?
extra : Map[String, Json]
} derive(Eq, Debug)
///|
pub fn RichTextInputElement::to_json(self : Self) -> Json {
let o = out_of("rich_text_input")
put_str(o, "action_id", self.action_id)
put_obj(o, "placeholder", self.placeholder, TextObject::to_json)
put_json(o, "initial_value", self.initial_value)
put_obj(
o,
"dispatch_action_config",
self.dispatch_action_config,
DispatchActionConfigObject::to_json,
)
put_bool(o, "focus_on_load", self.focus_on_load)
put_int(o, "min_lines", self.min_lines)
put_int(o, "max_lines", self.max_lines)
merge_extra(o, self.extra)
}
///|
/// Anything that can sit in an `accessory`, an `element` or an `elements` list.
pub(all) enum BlockElement {
Button(ButtonElement)
WorkflowButton(WorkflowButtonElement)
Image(ImageElement)
Select(SelectElement)
Picker(PickerElement)
Options(OptionsElement)
PlainTextInput(PlainTextInputElement)
RichTextInput(RichTextInputElement)
/// A text object appearing where an element is expected. Context blocks
/// accept these directly, which is why they are elements rather than a
/// special case of the context block.
Text(TextObject)
/// Anything this version does not model, kept verbatim so it round-trips and
/// so `validate` can name it under `Strict`.
Unknown(type_~ : String, raw~ : Json)
} derive(Eq, Debug)
///|
/// Always succeeds: an unrecognised element becomes `Unknown`.
pub fn BlockElement::from_json(j : Json) -> BlockElement {
let tag = type_tag_of(j)
guard j is Object(o) else { return Unknown(type_=tag, raw=j) }
if tag == "plain_text" || tag == "mrkdwn" {
if TextObject::from_json(j) is Some(t) {
return Text(t)
}
}
let rest = fields_of(o)
rest.remove("type")
if SelectKind::of_name(tag) is Some(kind) {
return Select(SelectElement::of_fields(kind, rest))
}
if OptionsKind::of_name(tag) is Some(kind) {
return Options({
kind,
action_id: take_str(rest, "action_id"),
options: take_arr(rest, "options", OptionObject::from_json),
initial_option: take_obj(rest, "initial_option", OptionObject::from_json),
initial_options: take_arr(
rest,
"initial_options",
OptionObject::from_json,
),
confirm: take_obj(rest, "confirm", ConfirmationDialogObject::from_json),
focus_on_load: take_bool(rest, "focus_on_load"),
extra: rest,
})
}
if PickerKind::of_name(tag) is Some(kind) {
return Picker({
kind,
action_id: take_str(rest, "action_id"),
placeholder: take_obj(rest, "placeholder", TextObject::from_json),
confirm: take_obj(rest, "confirm", ConfirmationDialogObject::from_json),
focus_on_load: take_bool(rest, "focus_on_load"),
initial_date: take_str(rest, "initial_date"),
initial_time: take_str(rest, "initial_time"),
initial_date_time: take_int(rest, "initial_date_time"),
timezone: take_str(rest, "timezone"),
extra: rest,
})
}
match tag {
"button" =>
Button({
text: take_obj(rest, "text", TextObject::from_json),
action_id: take_str(rest, "action_id"),
url: take_str(rest, "url"),
value: take_str(rest, "value"),
style: take_str(rest, "style"),
confirm: take_obj(rest, "confirm", ConfirmationDialogObject::from_json),
accessibility_label: take_str(rest, "accessibility_label"),
extra: rest,
})
"workflow_button" =>
WorkflowButton({
text: take_obj(rest, "text", TextObject::from_json),
action_id: take_str(rest, "action_id"),
style: take_str(rest, "style"),
accessibility_label: take_str(rest, "accessibility_label"),
workflow: take_json(rest, "workflow"),
extra: rest,
})
"image" =>
Image({
image_url: take_str(rest, "image_url"),
slack_file: take_obj(rest, "slack_file", SlackFileObject::from_json),
alt_text: take_str(rest, "alt_text"),
extra: rest,
})
"plain_text_input" =>
PlainTextInput({
action_id: take_str(rest, "action_id"),
placeholder: take_obj(rest, "placeholder", TextObject::from_json),
initial_value: take_str(rest, "initial_value"),
multiline: take_bool(rest, "multiline"),
min_length: take_int(rest, "min_length"),
max_length: take_int(rest, "max_length"),
dispatch_action_config: take_obj(
rest,
"dispatch_action_config",
DispatchActionConfigObject::from_json,
),
focus_on_load: take_bool(rest, "focus_on_load"),
extra: rest,
})
"rich_text_input" =>
RichTextInput({
action_id: take_str(rest, "action_id"),
placeholder: take_obj(rest, "placeholder", TextObject::from_json),
initial_value: take_json(rest, "initial_value"),
dispatch_action_config: take_obj(
rest,
"dispatch_action_config",
DispatchActionConfigObject::from_json,
),
focus_on_load: take_bool(rest, "focus_on_load"),
min_lines: take_int(rest, "min_lines"),
max_lines: take_int(rest, "max_lines"),
extra: rest,
})
_ => Unknown(type_=tag, raw=j)
}
}
///|
pub fn BlockElement::to_json(self : Self) -> Json {
match self {
Button(e) => e.to_json()
WorkflowButton(e) => e.to_json()
Image(e) => e.to_json()
Select(e) => e.to_json()
Picker(e) => e.to_json()
Options(e) => e.to_json()
PlainTextInput(e) => e.to_json()
RichTextInput(e) => e.to_json()
Text(t) => t.to_json()
Unknown(raw~, ..) => raw
}
}
///|
pub fn BlockElement::type_name(self : Self) -> String {
match self {
Button(_) => "button"
WorkflowButton(_) => "workflow_button"
Image(_) => "image"
Select(e) => e.kind.type_name()
Picker(e) => e.kind.type_name()
Options(e) => e.kind.type_name()
PlainTextInput(_) => "plain_text_input"
RichTextInput(_) => "rich_text_input"
Text(t) => t.type_name()
Unknown(type_=t, ..) => t
}
}
///|
/// Raise on anything unmodelled inside this element.
///
/// `in_context` picks the message java-slack-sdk would produce: an unknown
/// element inside a context block reports differently from one in an
/// `element` slot, and its BlockKitTest asserts on both strings.
pub fn BlockElement::validate(
self : Self,
in_context? : Bool = false,
) -> Unit raise BlockParseError {
match self {
Unknown(type_=t, ..) =>
if in_context {
raise UnknownContextBlockElement(t)
} else {
raise UnknownBlockElement(t)
}
Text(t) => t.validate()
Button(e) => {
validate_text_opt(e.text)
validate_confirm(e.confirm)
}
WorkflowButton(e) => validate_text_opt(e.text)
Image(_) => ()
Select(e) => {
validate_text_opt(e.placeholder)
validate_confirm(e.confirm)
validate_options(e.options)
validate_options(e.initial_options)
validate_option_opt(e.initial_option)
if e.option_groups is Some(groups) {
for group in groups {
group.validate()
}
}
}
Picker(e) => {
validate_text_opt(e.placeholder)
validate_confirm(e.confirm)
}
Options(e) => {
validate_confirm(e.confirm)
validate_options(e.options)
validate_options(e.initial_options)
validate_option_opt(e.initial_option)
}
PlainTextInput(e) => validate_text_opt(e.placeholder)
RichTextInput(e) => validate_text_opt(e.placeholder)
}
}
///|
fn validate_text_opt(t : TextObject?) -> Unit raise BlockParseError {
if t is Some(text) {
text.validate()
}
}
///|
fn validate_confirm(
c : ConfirmationDialogObject?,
) -> Unit raise BlockParseError {
if c is Some(dialog) {
dialog.validate()
}
}
///|
fn validate_option_opt(o : OptionObject?) -> Unit raise BlockParseError {
if o is Some(option) {
option.validate()
}
}
///|
fn validate_options(
options : Array[OptionObject]?,
) -> Unit raise BlockParseError {
if options is Some(list) {
for option in list {
option.validate()
}
}
}