slackblocks 2.8.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1750c4befde87d1390aa1aa1ff75ba3a6b13c529d2712e256fbf989939a2a708
4
- data.tar.gz: 3c5bbd48537ecbfb320e79b2e852ca990c3361cfd63c4217495fe00dacb7f61b
3
+ metadata.gz: 2854904b3e2e3ecbdf99f451ce5fa572336b2f9ed1946d18b9a5f6089d6ea889
4
+ data.tar.gz: 2fc7f9e644c71a5554b4095e2896374a9be3d8cd7366d7c700a9bbf4899fc604
5
5
  SHA512:
6
- metadata.gz: e305ad435bd8b3d35045d9e6923bdfa8c441340e26041776411db4129b351943c6f3dc72eac31f88384ae495a29a7dd0c965b38d845e75f5a288f2e3bf9051e4
7
- data.tar.gz: 38c8a914471ffe715f6be0126980481d189ec2645ed1be6f701c264580549466daddf9454b5006ef821dc2aba1f9dae6008942e07ea563ab5120ab23f899c40b
6
+ metadata.gz: 7e63f0c5e302a0ee9841fcd18cf143cb9995ab9a6314063efc5f8111a792423dec1c8dbdec3503602155823b71257d37a856953f973bf1e3d2a5c6a5eddec618
7
+ data.tar.gz: 3a30a4e84adb15803679cc90b2a7d1b1fc24d687b6a56e558f5278414067e962bc55782c6efdcb75324192ed5be45e2d113ef7bd129443e6352bdfdf0ba5e7c3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.1.0] — 2026-10-28
4
+
5
+ - Coordinate this release with the new Haskell implementation and ten-language documentation. Existing runtime support remains unchanged.
6
+
7
+ ## [3.0.0] — 2026-10-24
8
+
9
+ - Add native button `agent_prompt` and `agent_prompt_display` fields, Unicode prompt-length validation and the display/prompt dependency (spec 1.4.0). Document interaction acknowledgement regardless of Slackbot AI availability. Existing button calls remain compatible.
10
+
11
+ - Release the coordinated 3.0.0 API across all nine implementations; supported language/runtime minimums remain unchanged.
12
+ - Breaking: number-input bounds are strings in stored values and getters; numeric construction conveniences remain. See the migration guide before upgrading.
13
+ - Freeze the 2.8.0 documentation and fix shared language/version selector artwork and arrows across documentation versions.
14
+
15
+ - SB-006: reject messages with more than two data visualization blocks after composition, including attachment charts. The rule does not impose a two-chart limit on views.
16
+
17
+ - SB-003: reject explicitly supplied `focus_on_load` on external multi-selects, including false. Omit this deprecated parameter; other selectors retain focus support.
18
+ - Reject negative external-query thresholds, empty supplied static options/groups and malformed date/time strings, matching the retained Slack structural probes. Date shape checks do not add a calendar-validity guarantee; empty image alt text remains accepted (SB-013).
19
+
20
+ - Reject a plain-text input whose supplied `min_length` exceeds `max_length` (`out-of-range`); equal, zero-minimum and omitted bounds remain supported (SB-013).
21
+
22
+ - Correct number-input bounds to string wire values, retain native numeric convenience arguments, and reject nonfinite/inverted bounds (SB-001, spec 1.3.0). Bound getters now return strings; reflection/signature consumers must migrate.
23
+
24
+ - Clarify that `submit_disabled`, including explicit false, is configuration-modal-only; ordinary modal examples omit it (SB-015).
25
+
3
26
  ## [2.8.0] — 2026-10-18
4
27
 
5
28
  - Coordinate the first Elixir implementation release; existing language support remains unchanged.
@@ -8,8 +31,6 @@
8
31
 
9
32
  - Add the PHP implementation to the coordinated release and documentation.
10
33
 
11
- ## [Unreleased]
12
-
13
34
  ## [2.6.0] — 2026-10-09
14
35
 
15
36
  - Keep application metadata outside message-wide Block Kit text totals.
@@ -1,7 +1,7 @@
1
1
  # Generated from spec/limits.json. Do not edit by hand.
2
2
  module Slackblocks
3
3
  module Limits
4
- VALUES = {"action_id.max_length" => 255, "actions.elements.max_items" => 25, "alert.text.max_length" => 200, "block_id.max_length" => 255, "button.accessibility_label.max_length" => 75, "button.text.max_length" => 75, "button.url.max_length" => 3000, "button.value.max_length" => 2000, "card.actions.max_items" => 3, "card.body.max_length" => 200, "card.subtext.max_length" => 200, "card.subtitle.max_length" => 150, "card.title.max_length" => 150, "carousel.elements.max_items" => 10, "carousel.elements.min_items" => 1, "checkboxes.options.max_items" => 10, "checkboxes.options.min_items" => 1, "confirmation.confirm.max_length" => 30, "confirmation.deny.max_length" => 30, "confirmation.text.max_length" => 300, "confirmation.title.max_length" => 100, "container.child_blocks.max_items" => 10, "container.child_blocks.min_items" => 1, "container.subtitle.max_length" => 150, "container.title.max_length" => 150, "context.elements.max_items" => 10, "context_actions.elements.max_items" => 5, "conversation_filter.include.min_items" => 1, "data_table.cell_text.min_length" => 1, "data_table.columns.max_items" => 20, "data_table.columns.min_items" => 1, "data_table.content.max_length" => 20000, "data_table.page_size.max" => 100, "data_table.page_size.min" => 1, "data_table.row_header_column_index.min" => 0, "data_table.rows.max_items" => 201, "data_table.rows.min_items" => 2, "data_table.total_content.max_length" => 20000, "data_visualization.axis_label.max_length" => 50, "data_visualization.categories.max_items" => 20, "data_visualization.categories.min_items" => 1, "data_visualization.category_label.max_length" => 20, "data_visualization.data.max_items" => 20, "data_visualization.data.min_items" => 1, "data_visualization.point_label.max_length" => 20, "data_visualization.segment.label.max_length" => 20, "data_visualization.segment.value.exclusive_min" => 0, "data_visualization.segments.max_items" => 12, "data_visualization.segments.min_items" => 1, "data_visualization.series.max_items" => 12, "data_visualization.series.min_items" => 1, "data_visualization.series_name.max_length" => 20, "data_visualization.title.max_length" => 50, "date_picker.placeholder.max_length" => 150, "dispatch_action_configuration.trigger_actions_on.max_items" => 2, "dispatch_action_configuration.trigger_actions_on.min_items" => 1, "email_input.placeholder.max_length" => 150, "feedback_button.accessibility_label.max_length" => 75, "feedback_button.text.max_length" => 75, "feedback_button.value.max_length" => 2000, "file_input.max_files.max" => 10, "file_input.max_files.min" => 1, "header.text.max_length" => 150, "icon_button.accessibility_label.max_length" => 75, "icon_button.value.max_length" => 2000, "icon_button.visible_to_user_ids.max_items" => 10, "image.alt_text.max_length" => 2000, "image.image_url.max_length" => 3000, "image.title.max_length" => 2000, "image_element.alt_text.max_length" => 2000, "image_element.image_url.max_length" => 3000, "input.hint.max_length" => 2000, "input.label.max_length" => 2000, "markdown.text.max_length" => 12000, "markdown.total_text.max_length" => 12000, "message.attachments.max_items" => 100, "message.blocks.max_items" => 50, "message.channel.min_length" => 1, "multi_select.max_selected_items.min" => 1, "number_input.placeholder.max_length" => 150, "option.description.max_length" => 75, "option.text.max_length" => 75, "option.url.max_length" => 3000, "option.value.max_length" => 150, "option_group.label.max_length" => 75, "option_group.options.max_items" => 100, "option_group.options.min_items" => 1, "overflow.options.max_items" => 5, "overflow.options.min_items" => 1, "plain_text_input.max_length.max" => 3000, "plain_text_input.max_length.min" => 1, "plain_text_input.min_length.max" => 3000, "plain_text_input.min_length.min" => 0, "plain_text_input.placeholder.max_length" => 150, "plan.tasks.max_items" => 50, "radio_buttons.options.max_items" => 10, "radio_buttons.options.min_items" => 1, "rich_text_input.max_lines.max" => 100, "rich_text_input.max_lines.min" => 1, "rich_text_input.min_lines.max" => 100, "rich_text_input.min_lines.min" => 1, "rich_text_input.placeholder.max_length" => 150, "rich_text_list.border.max" => 1, "rich_text_list.border.min" => 0, "rich_text_list.indent.max" => 8, "rich_text_list.indent.min" => 0, "rich_text_list.offset.min" => 0, "rich_text_preformatted.border.max" => 1, "rich_text_preformatted.border.min" => 0, "rich_text_quote.border.max" => 1, "rich_text_quote.border.min" => 0, "section.fields.item_max_length" => 2000, "section.fields.max_items" => 10, "section.text.max_length" => 3000, "select.option_groups.max_items" => 100, "select.options.max_items" => 100, "select.placeholder.max_length" => 150, "table.column_settings.max_items" => 20, "table.columns.max_items" => 20, "table.rows.max_items" => 100, "text.max_length" => 3000, "text.min_length" => 1, "time_picker.placeholder.max_length" => 150, "url_input.placeholder.max_length" => 150, "video.alt_text.max_length" => 2000, "video.author_name.max_length" => 50, "video.description.max_length" => 200, "video.provider_icon_url.max_length" => 2000, "video.provider_name.max_length" => 50, "video.thumbnail_url.max_length" => 3000, "video.title.max_length" => 200, "video.title_url.max_length" => 2000, "video.video_url.max_length" => 3000, "view.blocks.max_items" => 100, "view.callback_id.max_length" => 255, "view.close.max_length" => 24, "view.external_id.max_length" => 255, "view.private_metadata.max_length" => 3000, "view.submit.max_length" => 24, "view.title.max_length" => 24, "workflow_button.accessibility_label.max_length" => 75, "workflow_button.text.max_length" => 75}.freeze
4
+ VALUES = {"action_id.max_length" => 255, "actions.elements.max_items" => 25, "alert.text.max_length" => 200, "block_id.max_length" => 255, "button.accessibility_label.max_length" => 75, "button.agent_prompt.max_length" => 4000, "button.text.max_length" => 75, "button.url.max_length" => 3000, "button.value.max_length" => 2000, "card.actions.max_items" => 3, "card.body.max_length" => 200, "card.subtext.max_length" => 200, "card.subtitle.max_length" => 150, "card.title.max_length" => 150, "carousel.elements.max_items" => 10, "carousel.elements.min_items" => 1, "checkboxes.options.max_items" => 10, "checkboxes.options.min_items" => 1, "confirmation.confirm.max_length" => 30, "confirmation.deny.max_length" => 30, "confirmation.text.max_length" => 300, "confirmation.title.max_length" => 100, "container.child_blocks.max_items" => 10, "container.child_blocks.min_items" => 1, "container.subtitle.max_length" => 150, "container.title.max_length" => 150, "context.elements.max_items" => 10, "context_actions.elements.max_items" => 5, "conversation_filter.include.min_items" => 1, "data_table.cell_text.min_length" => 1, "data_table.columns.max_items" => 20, "data_table.columns.min_items" => 1, "data_table.content.max_length" => 20000, "data_table.page_size.max" => 100, "data_table.page_size.min" => 1, "data_table.row_header_column_index.min" => 0, "data_table.rows.max_items" => 201, "data_table.rows.min_items" => 2, "data_table.total_content.max_length" => 20000, "data_visualization.axis_label.max_length" => 50, "data_visualization.categories.max_items" => 20, "data_visualization.categories.min_items" => 1, "data_visualization.category_label.max_length" => 20, "data_visualization.data.max_items" => 20, "data_visualization.data.min_items" => 1, "data_visualization.point_label.max_length" => 20, "data_visualization.segment.label.max_length" => 20, "data_visualization.segment.value.exclusive_min" => 0, "data_visualization.segments.max_items" => 12, "data_visualization.segments.min_items" => 1, "data_visualization.series.max_items" => 12, "data_visualization.series.min_items" => 1, "data_visualization.series_name.max_length" => 20, "data_visualization.title.max_length" => 50, "date_picker.placeholder.max_length" => 150, "dispatch_action_configuration.trigger_actions_on.max_items" => 2, "dispatch_action_configuration.trigger_actions_on.min_items" => 1, "email_input.placeholder.max_length" => 150, "feedback_button.accessibility_label.max_length" => 75, "feedback_button.text.max_length" => 75, "feedback_button.value.max_length" => 2000, "file_input.max_files.max" => 10, "file_input.max_files.min" => 1, "header.text.max_length" => 150, "icon_button.accessibility_label.max_length" => 75, "icon_button.value.max_length" => 2000, "icon_button.visible_to_user_ids.max_items" => 10, "image.alt_text.max_length" => 2000, "image.image_url.max_length" => 3000, "image.title.max_length" => 2000, "image_element.alt_text.max_length" => 2000, "image_element.image_url.max_length" => 3000, "input.hint.max_length" => 2000, "input.label.max_length" => 2000, "markdown.text.max_length" => 12000, "markdown.total_text.max_length" => 12000, "message.attachments.max_items" => 100, "message.blocks.max_items" => 50, "message.channel.min_length" => 1, "message.data_visualizations.max_items" => 2, "multi_select.max_selected_items.min" => 1, "number_input.placeholder.max_length" => 150, "option.description.max_length" => 75, "option.text.max_length" => 75, "option.url.max_length" => 3000, "option.value.max_length" => 150, "option_group.label.max_length" => 75, "option_group.options.max_items" => 100, "option_group.options.min_items" => 1, "overflow.options.max_items" => 5, "overflow.options.min_items" => 1, "plain_text_input.max_length.max" => 3000, "plain_text_input.max_length.min" => 1, "plain_text_input.min_length.max" => 3000, "plain_text_input.min_length.min" => 0, "plain_text_input.placeholder.max_length" => 150, "plan.tasks.max_items" => 50, "radio_buttons.options.max_items" => 10, "radio_buttons.options.min_items" => 1, "rich_text_input.max_lines.max" => 100, "rich_text_input.max_lines.min" => 1, "rich_text_input.min_lines.max" => 100, "rich_text_input.min_lines.min" => 1, "rich_text_input.placeholder.max_length" => 150, "rich_text_list.border.max" => 1, "rich_text_list.border.min" => 0, "rich_text_list.indent.max" => 8, "rich_text_list.indent.min" => 0, "rich_text_list.offset.min" => 0, "rich_text_preformatted.border.max" => 1, "rich_text_preformatted.border.min" => 0, "rich_text_quote.border.max" => 1, "rich_text_quote.border.min" => 0, "section.fields.item_max_length" => 2000, "section.fields.max_items" => 10, "section.text.max_length" => 3000, "select.min_query_length.min" => 0, "select.option_groups.max_items" => 100, "select.option_groups.min_items" => 1, "select.options.max_items" => 100, "select.options.min_items" => 1, "select.placeholder.max_length" => 150, "table.column_settings.max_items" => 20, "table.columns.max_items" => 20, "table.rows.max_items" => 100, "text.max_length" => 3000, "text.min_length" => 1, "time_picker.placeholder.max_length" => 150, "url_input.placeholder.max_length" => 150, "video.alt_text.max_length" => 2000, "video.author_name.max_length" => 50, "video.description.max_length" => 200, "video.provider_icon_url.max_length" => 2000, "video.provider_name.max_length" => 50, "video.thumbnail_url.max_length" => 3000, "video.title.max_length" => 200, "video.title_url.max_length" => 2000, "video.video_url.max_length" => 3000, "view.blocks.max_items" => 100, "view.callback_id.max_length" => 255, "view.close.max_length" => 24, "view.external_id.max_length" => 255, "view.private_metadata.max_length" => 3000, "view.submit.max_length" => 24, "view.title.max_length" => 24, "workflow_button.accessibility_label.max_length" => 75, "workflow_button.text.max_length" => 75}.freeze
5
5
  def self.[](path)
6
6
  VALUES.fetch(path)
7
7
  end
@@ -295,10 +295,10 @@ module Slackblocks
295
295
  class ButtonElement
296
296
  include Value
297
297
  include Element
298
- attr_reader :text, :action_id, :url, :value, :style, :confirm, :accessibility_label, :additional_fields
298
+ attr_reader :text, :action_id, :url, :value, :style, :confirm, :accessibility_label, :agent_prompt, :agent_prompt_display, :additional_fields
299
299
  end
300
300
  ButtonElement.const_set(:WIRE_TYPE, "button")
301
- ButtonElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "text", "wire" => "text", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "button.text", "required" => true}, {"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "url", "wire" => "url", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.url", "required" => false}, {"name" => "value", "wire" => "value", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.value", "required" => false}, {"name" => "style", "wire" => "style", "kind" => "enum", "target" => "ButtonStyle", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "confirm", "wire" => "confirm", "kind" => "object", "target" => "Confirmation", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "accessibility_label", "wire" => "accessibility_label", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.accessibility_label", "required" => false}]))
301
+ ButtonElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "text", "wire" => "text", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "button.text", "required" => true}, {"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "url", "wire" => "url", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.url", "required" => false}, {"name" => "value", "wire" => "value", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.value", "required" => false}, {"name" => "style", "wire" => "style", "kind" => "enum", "target" => "ButtonStyle", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "confirm", "wire" => "confirm", "kind" => "object", "target" => "Confirmation", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "accessibility_label", "wire" => "accessibility_label", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.accessibility_label", "required" => false}, {"name" => "agent_prompt", "wire" => "agent_prompt", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "button.agent_prompt", "required" => false}, {"name" => "agent_prompt_display", "wire" => "agent_prompt_display", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}]))
302
302
  ButtonElement.const_set(:DEFAULTS, Value.deep_freeze({}))
303
303
  ButtonElement.singleton_class.prepend(KeywordOnly)
304
304
 
@@ -397,7 +397,7 @@ module Slackblocks
397
397
  attr_reader :action_id, :min_query_length, :initial_options, :confirm, :max_selected_items, :focus_on_load, :placeholder, :additional_fields
398
398
  end
399
399
  ExternalMultiSelectElement.const_set(:WIRE_TYPE, "multi_external_select")
400
- ExternalMultiSelectElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "min_query_length", "wire" => "min_query_length", "kind" => "int", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "initial_options", "wire" => "initial_options", "kind" => "list", "target" => "Option", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "confirm", "wire" => "confirm", "kind" => "object", "target" => "Confirmation", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "max_selected_items", "wire" => "max_selected_items", "kind" => "int", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "multi_select.max_selected_items", "required" => false}, {"name" => "focus_on_load", "wire" => "focus_on_load", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "placeholder", "wire" => "placeholder", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "select.placeholder", "required" => false}]))
400
+ ExternalMultiSelectElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "min_query_length", "wire" => "min_query_length", "kind" => "int", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "select.min_query_length", "required" => false}, {"name" => "initial_options", "wire" => "initial_options", "kind" => "list", "target" => "Option", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "confirm", "wire" => "confirm", "kind" => "object", "target" => "Confirmation", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "max_selected_items", "wire" => "max_selected_items", "kind" => "int", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "multi_select.max_selected_items", "required" => false}, {"name" => "focus_on_load", "wire" => "focus_on_load", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "placeholder", "wire" => "placeholder", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "select.placeholder", "required" => false}]))
401
401
  ExternalMultiSelectElement.const_set(:DEFAULTS, Value.deep_freeze({}))
402
402
  ExternalMultiSelectElement.singleton_class.prepend(KeywordOnly)
403
403
 
@@ -408,7 +408,7 @@ module Slackblocks
408
408
  attr_reader :action_id, :min_query_length, :initial_option, :confirm, :focus_on_load, :placeholder, :additional_fields
409
409
  end
410
410
  ExternalSelectElement.const_set(:WIRE_TYPE, "external_select")
411
- ExternalSelectElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "min_query_length", "wire" => "min_query_length", "kind" => "int", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "initial_option", "wire" => "initial_option", "kind" => "object", "target" => "Option", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "confirm", "wire" => "confirm", "kind" => "object", "target" => "Confirmation", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "focus_on_load", "wire" => "focus_on_load", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "placeholder", "wire" => "placeholder", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "select.placeholder", "required" => false}]))
411
+ ExternalSelectElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "min_query_length", "wire" => "min_query_length", "kind" => "int", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "select.min_query_length", "required" => false}, {"name" => "initial_option", "wire" => "initial_option", "kind" => "object", "target" => "Option", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "confirm", "wire" => "confirm", "kind" => "object", "target" => "Confirmation", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "focus_on_load", "wire" => "focus_on_load", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "placeholder", "wire" => "placeholder", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "select.placeholder", "required" => false}]))
412
412
  ExternalSelectElement.const_set(:DEFAULTS, Value.deep_freeze({}))
413
413
  ExternalSelectElement.singleton_class.prepend(KeywordOnly)
414
414
 
@@ -463,7 +463,7 @@ module Slackblocks
463
463
  attr_reader :action_id, :is_decimal_allowed, :initial_value, :min_value, :max_value, :dispatch_action_config, :focus_on_load, :placeholder, :additional_fields
464
464
  end
465
465
  NumberInputElement.const_set(:WIRE_TYPE, "number_input")
466
- NumberInputElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "is_decimal_allowed", "wire" => "is_decimal_allowed", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => true}, {"name" => "initial_value", "wire" => "initial_value", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "min_value", "wire" => "min_value", "kind" => "double", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "max_value", "wire" => "max_value", "kind" => "double", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "dispatch_action_config", "wire" => "dispatch_action_config", "kind" => "object", "target" => "DispatchActionConfiguration", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "focus_on_load", "wire" => "focus_on_load", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "placeholder", "wire" => "placeholder", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "number_input.placeholder", "required" => false}]))
466
+ NumberInputElement.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "action_id", "wire" => "action_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "action_id", "required" => false}, {"name" => "is_decimal_allowed", "wire" => "is_decimal_allowed", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => true}, {"name" => "initial_value", "wire" => "initial_value", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "min_value", "wire" => "min_value", "kind" => "string", "target" => nil, "coerce" => "decimal_string", "flags" => nil, "limits" => nil, "required" => false}, {"name" => "max_value", "wire" => "max_value", "kind" => "string", "target" => nil, "coerce" => "decimal_string", "flags" => nil, "limits" => nil, "required" => false}, {"name" => "dispatch_action_config", "wire" => "dispatch_action_config", "kind" => "object", "target" => "DispatchActionConfiguration", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "focus_on_load", "wire" => "focus_on_load", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "placeholder", "wire" => "placeholder", "kind" => "text", "target" => "PlainText", "coerce" => "plain_text", "flags" => nil, "limits" => "number_input.placeholder", "required" => false}]))
467
467
  NumberInputElement.const_set(:DEFAULTS, Value.deep_freeze({}))
468
468
  NumberInputElement.singleton_class.prepend(KeywordOnly)
469
469
 
@@ -40,6 +40,12 @@ module Slackblocks
40
40
 
41
41
  def self.special(klass, wire, path)
42
42
  case klass.name.split("::").last
43
+ when "ButtonElement"
44
+ if wire.key?("agent_prompt_display") && !wire.key?("agent_prompt")
45
+ fail!("missing-required", "#{path}.agent_prompt", "required when agent_prompt_display is supplied")
46
+ end
47
+ when "ExternalMultiSelectElement"
48
+ fail!("invalid-usage", "#{path}.focus_on_load", "unsupported for external multi-select menus; omit this field") if wire.key?("focus_on_load")
43
49
  when "SlackIcon"
44
50
  unless Vocabulary::ICON_NAMES.include?(wire["name"])
45
51
  fail!("type-mismatch", "#{path}.name", "unknown Slack icon")
@@ -99,8 +105,24 @@ module Slackblocks
99
105
  end
100
106
  when "DispatchActionConfiguration"
101
107
  fail!("missing-required", "#{path}.trigger_actions_on", "required") unless wire.key?("trigger_actions_on")
108
+ when "DatePickerElement"
109
+ if wire.key?("initial_date") && !wire["initial_date"].match?(/\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/)
110
+ fail!("type-mismatch", "#{path}.initial_date", "expected YYYY-MM-DD")
111
+ end
112
+ when "TimePickerElement"
113
+ if wire.key?("initial_time") && !wire["initial_time"].match?(/\A([01][0-9]|2[0-3]):[0-5][0-9]\z/)
114
+ fail!("type-mismatch", "#{path}.initial_time", "expected HH:mm in 24-hour time")
115
+ end
116
+ when "PlainTextInputElement"
117
+ if wire["min_length"] && wire["max_length"] && wire["min_length"] > wire["max_length"]
118
+ fail!("out-of-range", "#{path}.min_length", "cannot exceed max_length")
119
+ end
102
120
  when "NumberInputElement"
103
- if wire["min_value"] && wire["max_value"] && wire["min_value"] > wire["max_value"]
121
+ minimum, maximum = %w[min_value max_value].map do |field|
122
+ text = wire[field]
123
+ decimal_bound(text, "#{path}.#{field}") unless text.nil?
124
+ end
125
+ if minimum && maximum && compare_decimal_bounds(minimum, maximum).positive?
104
126
  fail!("out-of-range", path, "min_value exceeds max_value")
105
127
  end
106
128
  when "DataTableBlock", "TableBlock"
@@ -131,6 +153,32 @@ module Slackblocks
131
153
  end
132
154
  end
133
155
 
156
+ # Compare the significand and decimal rank, without rounding or expanding an exponent.
157
+ def self.decimal_bound(text, path)
158
+ unless text.match?(/\A[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\z/)
159
+ fail!("type-mismatch", path, "expected a numeric string")
160
+ end
161
+
162
+ parts = text.sub(/\A[+-]/, "").split(/[eE]/)
163
+ mantissa = parts.fetch(0).split(".", -1)
164
+ fraction = mantissa.fetch(1, "")
165
+ digits = (mantissa.fetch(0) + fraction).sub(/\A0+/, "")
166
+ return [0, 0, ""] if digits.empty?
167
+
168
+ rank = Integer(parts.fetch(1, "0"), 10) + digits.length - fraction.length
169
+ [text.start_with?("-") ? -1 : 1, rank, digits.sub(/0+\z/, "")]
170
+ end
171
+
172
+ def self.compare_decimal_bounds(left, right)
173
+ sign = left[0] <=> right[0]
174
+ return sign unless sign.zero?
175
+ return 0 if left[0].zero?
176
+
177
+ rank = left[1] <=> right[1]
178
+ width = [left[2].length, right[2].length].max
179
+ left[0] * (rank.zero? ? left[2].ljust(width, "0") <=> right[2].ljust(width, "0") : rank)
180
+ end
181
+
134
182
  def self.surface(blocks, name, path)
135
183
  allowed = Vocabulary::SURFACE_BLOCK_TYPES.fetch(name)
136
184
  blocks.each_with_index do |block, index|
@@ -158,9 +206,10 @@ module Slackblocks
158
206
 
159
207
  def self.message(wire, path)
160
208
  surface(wire["blocks"] || [], "message", "#{path}.blocks")
161
- totals = {"markdown" => 0, "data_table" => 0}
209
+ totals = {"markdown" => 0, "data_table" => 0, "data_visualization" => 0}
162
210
  tally(wire["blocks"], totals)
163
211
  (wire["attachments"] || []).each { |attachment| tally(attachment["blocks"], totals) }
212
+ fail!("length-exceeded", path, "a message may contain at most two data visualization blocks") if totals["data_visualization"] > limit("message.data_visualizations.max_items")
164
213
  if totals["markdown"] > limit("markdown.total_text.max_length")
165
214
  fail!("length-exceeded", path, "message markdown total exceeded")
166
215
  end
@@ -174,6 +223,8 @@ module Slackblocks
174
223
  when Hash
175
224
  if node["type"] == "markdown"
176
225
  totals["markdown"] += node.fetch("text").length
226
+ elsif node["type"] == "data_visualization"
227
+ totals["data_visualization"] += 1
177
228
  elsif node["type"] == "data_table"
178
229
  totals["data_table"] += characters(node.fetch("rows"))
179
230
  elsif node["type"] == "container"
@@ -88,6 +88,12 @@ module Slackblocks
88
88
  def self.normalize_field(field, value, path)
89
89
  kind = field.fetch("kind")
90
90
  target = field["target"] && Slackblocks.const_get(field["target"], false)
91
+ if field["coerce"] == "decimal_string"
92
+ unless value.is_a?(String) || value.is_a?(Integer) || value.instance_of?(Float)
93
+ fail!("type-mismatch", path, "expected a numeric string or number")
94
+ end
95
+ return string(value.to_s, path)
96
+ end
91
97
  case kind
92
98
  when "string"
93
99
  string(value, path)
@@ -1,4 +1,4 @@
1
1
  module Slackblocks
2
- VERSION = "2.8.0"
3
- SPEC_VERSION = "1.2.0"
2
+ VERSION = "3.1.0"
3
+ SPEC_VERSION = "1.4.0"
4
4
  end
@@ -367,10 +367,12 @@ module Slackblocks
367
367
  attr_reader style: (:primary | :danger)?
368
368
  attr_reader confirm: (Confirmation)?
369
369
  attr_reader accessibility_label: (String)?
370
+ attr_reader agent_prompt: (String)?
371
+ attr_reader agent_prompt_display: (String)?
370
372
  attr_reader additional_fields: Hash[String, untyped]
371
- def initialize: (text: String | PlainText, ?action_id: (String)?, ?url: (String)?, ?value: (String)?, ?style: (:primary | :danger)?, ?confirm: (Confirmation)?, ?accessibility_label: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
372
- def self.new: (text: String | PlainText, ?action_id: (String)?, ?url: (String)?, ?value: (String)?, ?style: (:primary | :danger)?, ?confirm: (Confirmation)?, ?accessibility_label: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> ButtonElement
373
- def self.[]: (text: String | PlainText, ?action_id: (String)?, ?url: (String)?, ?value: (String)?, ?style: (:primary | :danger)?, ?confirm: (Confirmation)?, ?accessibility_label: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> ButtonElement
373
+ def initialize: (text: String | PlainText, ?action_id: (String)?, ?url: (String)?, ?value: (String)?, ?style: (:primary | :danger)?, ?confirm: (Confirmation)?, ?accessibility_label: (String)?, ?agent_prompt: (String)?, ?agent_prompt_display: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
374
+ def self.new: (text: String | PlainText, ?action_id: (String)?, ?url: (String)?, ?value: (String)?, ?style: (:primary | :danger)?, ?confirm: (Confirmation)?, ?accessibility_label: (String)?, ?agent_prompt: (String)?, ?agent_prompt_display: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> ButtonElement
375
+ def self.[]: (text: String | PlainText, ?action_id: (String)?, ?url: (String)?, ?value: (String)?, ?style: (:primary | :danger)?, ?confirm: (Confirmation)?, ?accessibility_label: (String)?, ?agent_prompt: (String)?, ?agent_prompt_display: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> ButtonElement
374
376
  end
375
377
 
376
378
  class ChannelMultiSelectElement
@@ -596,15 +598,15 @@ module Slackblocks
596
598
  attr_reader action_id: (String)?
597
599
  attr_reader is_decimal_allowed: bool
598
600
  attr_reader initial_value: (String)?
599
- attr_reader min_value: (Integer | Float)?
600
- attr_reader max_value: (Integer | Float)?
601
+ attr_reader min_value: (String)?
602
+ attr_reader max_value: (String)?
601
603
  attr_reader dispatch_action_config: (DispatchActionConfiguration)?
602
604
  attr_reader focus_on_load: (bool)?
603
605
  attr_reader placeholder: (PlainText)?
604
606
  attr_reader additional_fields: Hash[String, untyped]
605
- def initialize: (is_decimal_allowed: bool, ?action_id: (String)?, ?initial_value: (String)?, ?min_value: (Integer | Float)?, ?max_value: (Integer | Float)?, ?dispatch_action_config: (DispatchActionConfiguration)?, ?focus_on_load: (bool)?, ?placeholder: (String | PlainText)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
606
- def self.new: (is_decimal_allowed: bool, ?action_id: (String)?, ?initial_value: (String)?, ?min_value: (Integer | Float)?, ?max_value: (Integer | Float)?, ?dispatch_action_config: (DispatchActionConfiguration)?, ?focus_on_load: (bool)?, ?placeholder: (String | PlainText)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> NumberInputElement
607
- def self.[]: (is_decimal_allowed: bool, ?action_id: (String)?, ?initial_value: (String)?, ?min_value: (Integer | Float)?, ?max_value: (Integer | Float)?, ?dispatch_action_config: (DispatchActionConfiguration)?, ?focus_on_load: (bool)?, ?placeholder: (String | PlainText)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> NumberInputElement
607
+ def initialize: (is_decimal_allowed: bool, ?action_id: (String)?, ?initial_value: (String)?, ?min_value: (String | Integer | Float)?, ?max_value: (String | Integer | Float)?, ?dispatch_action_config: (DispatchActionConfiguration)?, ?focus_on_load: (bool)?, ?placeholder: (String | PlainText)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
608
+ def self.new: (is_decimal_allowed: bool, ?action_id: (String)?, ?initial_value: (String)?, ?min_value: (String | Integer | Float)?, ?max_value: (String | Integer | Float)?, ?dispatch_action_config: (DispatchActionConfiguration)?, ?focus_on_load: (bool)?, ?placeholder: (String | PlainText)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> NumberInputElement
609
+ def self.[]: (is_decimal_allowed: bool, ?action_id: (String)?, ?initial_value: (String)?, ?min_value: (String | Integer | Float)?, ?max_value: (String | Integer | Float)?, ?dispatch_action_config: (DispatchActionConfiguration)?, ?focus_on_load: (bool)?, ?placeholder: (String | PlainText)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> NumberInputElement
608
610
  end
609
611
 
610
612
  class OverflowElement
data/sig/slackblocks.rbs CHANGED
@@ -45,6 +45,8 @@ module Slackblocks
45
45
  def self.text_limit: (untyped value, String key, String path) -> untyped
46
46
  def self.max_items: (untyped items, String key, String path) -> untyped
47
47
  def self.special: (untyped klass, Hash[String, untyped] wire, String path) -> untyped
48
+ def self.decimal_bound: (String text, String path) -> [Integer, Integer, String]
49
+ def self.compare_decimal_bounds: ([Integer, Integer, String] left, [Integer, Integer, String] right) -> Integer
48
50
  def self.surface: (Array[untyped] blocks, String name, String path) -> untyped
49
51
  def self.standalone_task: (untyped value, String path) -> untyped
50
52
  def self.message: (Hash[String, untyped] wire, String path) -> untyped
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slackblocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas Lambourne