block-kit 1.0.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.
Files changed (120) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +183 -0
  6. data/Rakefile +8 -0
  7. data/lib/block_kit/base.rb +190 -0
  8. data/lib/block_kit/blocks.rb +50 -0
  9. data/lib/block_kit/composition/confirmation_dialog.rb +48 -0
  10. data/lib/block_kit/composition/conversation_filter.rb +42 -0
  11. data/lib/block_kit/composition/dispatch_action_config.rb +33 -0
  12. data/lib/block_kit/composition/input_parameter.rb +19 -0
  13. data/lib/block_kit/composition/mrkdwn.rb +15 -0
  14. data/lib/block_kit/composition/option.rb +41 -0
  15. data/lib/block_kit/composition/option_group.rb +27 -0
  16. data/lib/block_kit/composition/overflow_option.rb +21 -0
  17. data/lib/block_kit/composition/plain_text.rb +15 -0
  18. data/lib/block_kit/composition/slack_file.rb +36 -0
  19. data/lib/block_kit/composition/text.rb +29 -0
  20. data/lib/block_kit/composition/trigger.rb +33 -0
  21. data/lib/block_kit/composition/workflow.rb +19 -0
  22. data/lib/block_kit/composition.rb +19 -0
  23. data/lib/block_kit/concerns/confirmable.rb +21 -0
  24. data/lib/block_kit/concerns/conversation_selection.rb +26 -0
  25. data/lib/block_kit/concerns/dispatchable.rb +24 -0
  26. data/lib/block_kit/concerns/dsl_generation.rb +76 -0
  27. data/lib/block_kit/concerns/external.rb +19 -0
  28. data/lib/block_kit/concerns/focusable_on_load.rb +17 -0
  29. data/lib/block_kit/concerns/has_initial_option.rb +23 -0
  30. data/lib/block_kit/concerns/has_initial_options.rb +23 -0
  31. data/lib/block_kit/concerns/has_option_groups.rb +37 -0
  32. data/lib/block_kit/concerns/has_options.rb +28 -0
  33. data/lib/block_kit/concerns/has_placeholder.rb +21 -0
  34. data/lib/block_kit/concerns/has_rich_text_elements.rb +83 -0
  35. data/lib/block_kit/concerns/plain_text_emoji_assignment.rb +39 -0
  36. data/lib/block_kit/concerns.rb +18 -0
  37. data/lib/block_kit/elements/base.rb +23 -0
  38. data/lib/block_kit/elements/base_button.rb +45 -0
  39. data/lib/block_kit/elements/button.rb +27 -0
  40. data/lib/block_kit/elements/channels_select.rb +22 -0
  41. data/lib/block_kit/elements/checkboxes.rb +14 -0
  42. data/lib/block_kit/elements/conversations_select.rb +24 -0
  43. data/lib/block_kit/elements/date_picker.rb +21 -0
  44. data/lib/block_kit/elements/datetime_picker.rb +21 -0
  45. data/lib/block_kit/elements/email_text_input.rb +24 -0
  46. data/lib/block_kit/elements/external_select.rb +21 -0
  47. data/lib/block_kit/elements/file_input.rb +33 -0
  48. data/lib/block_kit/elements/image.rb +53 -0
  49. data/lib/block_kit/elements/multi_channels_select.rb +31 -0
  50. data/lib/block_kit/elements/multi_conversations_select.rb +33 -0
  51. data/lib/block_kit/elements/multi_external_select.rb +21 -0
  52. data/lib/block_kit/elements/multi_select.rb +21 -0
  53. data/lib/block_kit/elements/multi_static_select.rb +12 -0
  54. data/lib/block_kit/elements/multi_users_select.rb +31 -0
  55. data/lib/block_kit/elements/number_input.rb +52 -0
  56. data/lib/block_kit/elements/overflow.rb +23 -0
  57. data/lib/block_kit/elements/plain_text_input.rb +66 -0
  58. data/lib/block_kit/elements/radio_buttons.rb +14 -0
  59. data/lib/block_kit/elements/rich_text_input.rb +26 -0
  60. data/lib/block_kit/elements/select.rb +18 -0
  61. data/lib/block_kit/elements/static_select.rb +12 -0
  62. data/lib/block_kit/elements/time_picker.rb +43 -0
  63. data/lib/block_kit/elements/url_text_input.rb +24 -0
  64. data/lib/block_kit/elements/users_select.rb +17 -0
  65. data/lib/block_kit/elements/workflow_button.rb +18 -0
  66. data/lib/block_kit/elements.rb +36 -0
  67. data/lib/block_kit/fixers/associated.rb +27 -0
  68. data/lib/block_kit/fixers/base.rb +30 -0
  69. data/lib/block_kit/fixers/null_value.rb +42 -0
  70. data/lib/block_kit/fixers/truncate.rb +35 -0
  71. data/lib/block_kit/fixers.rb +11 -0
  72. data/lib/block_kit/layout/actions.rb +84 -0
  73. data/lib/block_kit/layout/base.rb +23 -0
  74. data/lib/block_kit/layout/context.rb +48 -0
  75. data/lib/block_kit/layout/divider.rb +9 -0
  76. data/lib/block_kit/layout/file.rb +19 -0
  77. data/lib/block_kit/layout/header.rb +22 -0
  78. data/lib/block_kit/layout/image.rb +61 -0
  79. data/lib/block_kit/layout/input.rb +119 -0
  80. data/lib/block_kit/layout/markdown.rb +19 -0
  81. data/lib/block_kit/layout/rich_text/elements/broadcast.rb +22 -0
  82. data/lib/block_kit/layout/rich_text/elements/channel.rb +21 -0
  83. data/lib/block_kit/layout/rich_text/elements/color.rb +20 -0
  84. data/lib/block_kit/layout/rich_text/elements/date.rb +34 -0
  85. data/lib/block_kit/layout/rich_text/elements/emoji.rb +27 -0
  86. data/lib/block_kit/layout/rich_text/elements/link.rb +28 -0
  87. data/lib/block_kit/layout/rich_text/elements/mention_style.rb +27 -0
  88. data/lib/block_kit/layout/rich_text/elements/text.rb +23 -0
  89. data/lib/block_kit/layout/rich_text/elements/text_style.rb +23 -0
  90. data/lib/block_kit/layout/rich_text/elements/user.rb +21 -0
  91. data/lib/block_kit/layout/rich_text/elements/usergroup.rb +21 -0
  92. data/lib/block_kit/layout/rich_text/elements.rb +35 -0
  93. data/lib/block_kit/layout/rich_text/list.rb +41 -0
  94. data/lib/block_kit/layout/rich_text/preformatted.rb +19 -0
  95. data/lib/block_kit/layout/rich_text/quote.rb +19 -0
  96. data/lib/block_kit/layout/rich_text/section.rb +11 -0
  97. data/lib/block_kit/layout/rich_text.rb +53 -0
  98. data/lib/block_kit/layout/section.rb +152 -0
  99. data/lib/block_kit/layout/video.rb +71 -0
  100. data/lib/block_kit/layout.rb +35 -0
  101. data/lib/block_kit/surfaces/base.rb +173 -0
  102. data/lib/block_kit/surfaces/home.rb +40 -0
  103. data/lib/block_kit/surfaces/message.rb +140 -0
  104. data/lib/block_kit/surfaces/modal.rb +74 -0
  105. data/lib/block_kit/surfaces.rb +11 -0
  106. data/lib/block_kit/typed_array.rb +114 -0
  107. data/lib/block_kit/typed_set.rb +78 -0
  108. data/lib/block_kit/types/array.rb +45 -0
  109. data/lib/block_kit/types/blocks.rb +37 -0
  110. data/lib/block_kit/types/generic.rb +45 -0
  111. data/lib/block_kit/types/option.rb +48 -0
  112. data/lib/block_kit/types/set.rb +35 -0
  113. data/lib/block_kit/types/text.rb +80 -0
  114. data/lib/block_kit/types.rb +17 -0
  115. data/lib/block_kit/validators/array_inclusion_validator.rb +55 -0
  116. data/lib/block_kit/validators/associated_validator.rb +61 -0
  117. data/lib/block_kit/validators.rb +8 -0
  118. data/lib/block_kit/version.rb +5 -0
  119. data/lib/block_kit.rb +38 -0
  120. metadata +190 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class ConversationFilter < Base
6
+ self.type = :conversation_filter
7
+
8
+ VALID_INCLUDES = [
9
+ IM = "im",
10
+ MPIM = "mpim",
11
+ PUBLIC = "public",
12
+ PRIVATE = "private"
13
+ ].freeze
14
+
15
+ attribute :include, Types::Set.of(:string)
16
+ attribute :exclude_external_shared_channels, :boolean
17
+ attribute :exclude_bot_users, :boolean
18
+
19
+ validates :include, presence: true, "block_kit/validators/array_inclusion": {in: VALID_INCLUDES, message: "contains invalid values: %{rejected_values}"}, allow_nil: true
20
+ fixes :include, null_value: {error_types: [:blank, :inclusion]}
21
+
22
+ VALID_INCLUDES.each do |value|
23
+ define_method(:"include_#{value}?") do
24
+ !!include&.member?(value)
25
+ end
26
+
27
+ define_method(:"include_#{value}!") do
28
+ self.include ||= []
29
+ self.include.add(value)
30
+ end
31
+ end
32
+
33
+ def as_json(*)
34
+ super().except(:type).merge(
35
+ include: include&.to_a,
36
+ exclude_external_shared_channels: exclude_external_shared_channels,
37
+ exclude_bot_users: exclude_bot_users
38
+ ).compact
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class DispatchActionConfig < Base
6
+ self.type = :dispatch_action_config
7
+
8
+ VALID_TRIGGERS = [
9
+ ON_ENTER_PRESSED = "on_enter_pressed",
10
+ ON_CHARACTER_ENTERED = "on_character_entered"
11
+ ].freeze
12
+
13
+ attribute :trigger_actions_on, Types::Set.of(:string)
14
+ validates :trigger_actions_on, presence: true, "block_kit/validators/array_inclusion": {in: VALID_TRIGGERS}
15
+ fixes :trigger_actions_on, null_value: {error_types: [:inclusion]}
16
+
17
+ VALID_TRIGGERS.each do |value|
18
+ define_method(:"trigger_actions_on_#{value}?") do
19
+ !!trigger_actions_on&.member?(value)
20
+ end
21
+
22
+ define_method(:"trigger_actions_on_#{value}!") do
23
+ self.trigger_actions_on ||= []
24
+ self.trigger_actions_on.add(value)
25
+ end
26
+ end
27
+
28
+ def as_json(*)
29
+ super().except(:type).merge(trigger_actions_on: trigger_actions_on&.to_a).compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class InputParameter < Base
6
+ self.type = :input_parameter
7
+
8
+ attribute :name, :string
9
+ attribute :value, :string
10
+
11
+ validates :name, presence: true
12
+ validates :value, presence: true
13
+
14
+ def as_json(*)
15
+ super().except(:type).merge(name: name, value: value).compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class Mrkdwn < Text
6
+ self.type = :mrkdwn
7
+
8
+ attribute :verbatim, :boolean
9
+
10
+ def as_json(*)
11
+ super.merge(verbatim: verbatim).compact
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class Option < Base
6
+ self.type = :option
7
+
8
+ MAX_TEXT_LENGTH = 75
9
+ MAX_VALUE_LENGTH = 150
10
+ MAX_DESCRIPTION_LENGTH = 75
11
+
12
+ plain_text_attribute :text
13
+ attribute :value, :string
14
+ plain_text_attribute :description
15
+ attribute :initial, :boolean
16
+
17
+ include Concerns::PlainTextEmojiAssignment.new(:text, :description)
18
+
19
+ validates :text, presence: true, length: {maximum: MAX_TEXT_LENGTH}
20
+ fixes :text, truncate: {maximum: MAX_TEXT_LENGTH}
21
+
22
+ validates :value, presence: true, length: {maximum: MAX_VALUE_LENGTH}
23
+ fixes :value, truncate: {maximum: MAX_VALUE_LENGTH, dangerous: true}
24
+
25
+ validates :description, presence: true, length: {maximum: MAX_DESCRIPTION_LENGTH}, allow_nil: true
26
+ fixes :description, truncate: {maximum: MAX_DESCRIPTION_LENGTH}, null_value: [:blank]
27
+
28
+ def initial?
29
+ !!initial
30
+ end
31
+
32
+ def as_json(*)
33
+ super().except(:type).merge(
34
+ text: text&.as_json,
35
+ value: value,
36
+ description: description&.as_json
37
+ ).compact
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class OptionGroup < Base
6
+ self.type = :option_group
7
+
8
+ MAX_LABEL_LENGTH = 75
9
+ MAX_OPTIONS = 100
10
+
11
+ include Concerns::HasOptions.new(limit: MAX_OPTIONS)
12
+
13
+ plain_text_attribute :label
14
+ validates :label, presence: true, length: {maximum: MAX_LABEL_LENGTH}
15
+ fixes :label, truncate: {maximum: MAX_LABEL_LENGTH}
16
+
17
+ dsl_method :options, as: :option, required_fields: [:text, :value], yields: false
18
+
19
+ def as_json(*)
20
+ super().except(:type).merge(
21
+ label: label&.as_json,
22
+ options: options&.map(&:as_json)
23
+ ).compact
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Composition
7
+ class OverflowOption < Option
8
+ self.type = :overflow_option
9
+
10
+ MAX_URL_LENGTH = 3000
11
+
12
+ attribute :url, :string
13
+ validates :url, presence: true, format: {with: URI::DEFAULT_PARSER.make_regexp, message: "is not a valid URI", allow_blank: true}, length: {maximum: MAX_URL_LENGTH}, allow_nil: true
14
+ fixes :url, truncate: {maximum: MAX_URL_LENGTH, dangerous: true, omission: ""}, null_value: [:blank]
15
+
16
+ def as_json(*)
17
+ super.merge(url: url).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class PlainText < Text
6
+ self.type = :plain_text
7
+
8
+ attribute :emoji, :boolean
9
+
10
+ def as_json(*)
11
+ super.merge(emoji: emoji).compact
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Composition
7
+ class SlackFile < Base
8
+ self.type = :slack_file
9
+
10
+ MAX_URL_LENGTH = 3000
11
+
12
+ attribute :id, :string
13
+ attribute :url, :string
14
+
15
+ validates :id, presence: true, format: {with: /\AF[A-Z0-9]{8,}\z/, allow_blank: true}, allow_nil: true
16
+ fixes :id, null_value: [:blank]
17
+
18
+ validates :url, presence: true, length: {maximum: MAX_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: "is not a valid URI", allow_blank: true}, allow_nil: true
19
+ fixes :url, null_value: [:blank]
20
+
21
+ validate :id_or_url_present
22
+
23
+ def as_json(*)
24
+ super().except(:type).merge(id: id, url: url).compact
25
+ end
26
+
27
+ private
28
+
29
+ def id_or_url_present
30
+ if (id.blank? && url.blank?) || (id.present? && url.present?)
31
+ errors.add(:base, "must have either an id or url, but not both")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class Text < Base
6
+ MAX_LENGTH = 3000
7
+
8
+ attribute :text, :string
9
+ validates :text, presence: true, length: {maximum: MAX_LENGTH}
10
+ fixes :text, truncate: {maximum: MAX_LENGTH}
11
+
12
+ delegate :blank?, :present?, to: :text
13
+
14
+ def length
15
+ text&.length || 0
16
+ end
17
+
18
+ def truncate(*)
19
+ dup.tap do |copy|
20
+ copy.text = text.truncate(*)
21
+ end
22
+ end
23
+
24
+ def as_json(*)
25
+ super.merge(text: text)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Composition
7
+ class Trigger < Base
8
+ self.type = :trigger
9
+
10
+ MAX_URL_LENGTH = 3000
11
+
12
+ attribute :url, :string
13
+ attribute :customizable_input_parameters, Types::Array.of(Composition::InputParameter)
14
+
15
+ validates :url, presence: true, length: {maximum: MAX_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: "is not a valid URI", allow_blank: true}
16
+ validates :customizable_input_parameters, presence: true, "block_kit/validators/associated": true, allow_nil: true
17
+ fixes :customizable_input_parameters, null_value: [:blank]
18
+
19
+ dsl_method :customizable_input_parameters, as: :customizable_input_parameter, required_fields: [:name, :value], yields: false
20
+
21
+ alias_method :input_parameter, :customizable_input_parameter
22
+ alias_method :customizable_input_param, :customizable_input_parameter
23
+ alias_method :input_param, :customizable_input_parameter
24
+
25
+ def as_json(*)
26
+ super().except(:type).merge(
27
+ url: url,
28
+ customizable_input_parameters: customizable_input_parameters&.map(&:as_json)
29
+ ).compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ class Workflow < Base
6
+ self.type = :workflow
7
+
8
+ attribute :trigger, Types::Generic.of_type(Composition::Trigger)
9
+ validates :trigger, presence: true, "block_kit/validators/associated": true
10
+ fixes :trigger, associated: true
11
+
12
+ dsl_method :trigger
13
+
14
+ def as_json(*)
15
+ super().except(:type).merge(trigger: trigger&.as_json).compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Composition
5
+ autoload :ConfirmationDialog, "block_kit/composition/confirmation_dialog"
6
+ autoload :ConversationFilter, "block_kit/composition/conversation_filter"
7
+ autoload :DispatchActionConfig, "block_kit/composition/dispatch_action_config"
8
+ autoload :InputParameter, "block_kit/composition/input_parameter"
9
+ autoload :Mrkdwn, "block_kit/composition/mrkdwn"
10
+ autoload :OptionGroup, "block_kit/composition/option_group"
11
+ autoload :Option, "block_kit/composition/option"
12
+ autoload :OverflowOption, "block_kit/composition/overflow_option"
13
+ autoload :PlainText, "block_kit/composition/plain_text"
14
+ autoload :SlackFile, "block_kit/composition/slack_file"
15
+ autoload :Text, "block_kit/composition/text"
16
+ autoload :Trigger, "block_kit/composition/trigger"
17
+ autoload :Workflow, "block_kit/composition/workflow"
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module Confirmable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute :confirm, Types::Generic.of_type(Composition::ConfirmationDialog)
10
+ validates :confirm, "block_kit/validators/associated": true, allow_nil: true
11
+ fixes :confirm, associated: true
12
+
13
+ dsl_method :confirm, required_fields: [:title, :text, :confirm, :deny], yields: false
14
+ end
15
+
16
+ def as_json(*)
17
+ super.merge(confirm: confirm&.as_json).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module ConversationSelection
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute :default_to_current_conversation, :boolean
10
+ attribute :filter, Types::Generic.of_type(Composition::ConversationFilter)
11
+
12
+ validates :filter, "block_kit/validators/associated": true, allow_nil: true
13
+ fixes :filter, associated: true
14
+
15
+ dsl_method :filter
16
+ end
17
+
18
+ def as_json(*)
19
+ super.merge(
20
+ default_to_current_conversation: default_to_current_conversation,
21
+ filter: filter&.as_json
22
+ ).compact
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module Dispatchable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute :dispatch_action_config, Types::Generic.of_type(Composition::DispatchActionConfig)
10
+
11
+ validates :dispatch_action_config, presence: true, "block_kit/validators/associated": true, allow_nil: true
12
+ fixes :dispatch_action_config, associated: true
13
+
14
+ dsl_method :dispatch_action_config
15
+ alias_method :dispatch_action_configuration, :dispatch_action_config
16
+ alias_attribute :dispatch_action_configuration, :dispatch_action_config
17
+ end
18
+
19
+ def as_json(*)
20
+ super.merge(dispatch_action_config: dispatch_action_config&.as_json).compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module DSLGeneration
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ private
10
+
11
+ def dsl_method(attribute, as: nil, type: nil, required_fields: [], mutually_exclusive_fields: [], yields: true)
12
+ type ||= attribute_types[attribute.to_s]
13
+ type = Types::Generic.of_type(type) if type.is_a?(Class) && type < BlockKit::Base
14
+ raise ArgumentError, "attribute #{attribute} does not exist" if type.instance_of?(ActiveModel::Type::Value)
15
+
16
+ is_array_attribute = false
17
+
18
+ if type.instance_of?(Types::Array)
19
+ is_array_attribute = true
20
+ type = type.item_type
21
+ end
22
+
23
+ is_array_attribute ||= attribute_types[attribute.to_s].type == :array
24
+
25
+ fields = type.block_class.attribute_names.map(&:to_sym)
26
+ plain_text_fields = type.block_class.attribute_types.select { |_, v| v.respond_to?(:block_class) && v.block_class == Composition::PlainText }.keys.map(&:to_sym)
27
+
28
+ define_method(as || attribute) do |args = {}, &block|
29
+ args = args.symbolize_keys
30
+
31
+ # Return the existing attribute if no args or block are passed and we're not in a custom-named method
32
+ return super() if args.blank? && block.nil? && as.nil?
33
+
34
+ if (missing_required_fields = (required_fields - args.keys)) && missing_required_fields.any?
35
+ message = "missing keyword#{"s" if missing_required_fields.size > 1}: #{missing_required_fields.map(&:inspect).join(", ")}"
36
+ raise ArgumentError, message
37
+ end
38
+
39
+ unknown_fields = (args.keys - fields)
40
+ unknown_fields.delete(:emoji) if plain_text_fields.any?
41
+
42
+ if unknown_fields.any?
43
+ message = "unknown keyword#{"s" if unknown_fields.size > 1}: #{unknown_fields.map(&:inspect).join(", ")}"
44
+ raise ArgumentError, message
45
+ end
46
+
47
+ mutually_exclusive = mutually_exclusive_fields & args.keys
48
+ if mutually_exclusive.length > 1
49
+ message = "mutually exclusive keywords: #{mutually_exclusive.map(&:inspect).join(", ")}"
50
+ raise ArgumentError, message
51
+ end
52
+
53
+ new_value = if is_array_attribute
54
+ public_send(:"#{attribute}=", []) if public_send(attribute).nil?
55
+ public_send(attribute) << type.cast(args)
56
+ public_send(attribute).last
57
+ else
58
+ public_send(:"#{attribute}=", type.cast(args))
59
+ public_send(attribute)
60
+ end
61
+
62
+ plain_text_fields.each do |field|
63
+ next unless args.key?(field)
64
+
65
+ new_value.public_send(field).emoji = args[:emoji] if args.key?(:emoji)
66
+ end
67
+
68
+ block&.call(new_value) if yields
69
+
70
+ self
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module External
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute :min_query_length, :integer
10
+ validates :min_query_length, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}, allow_nil: true
11
+ fixes :min_query_length, null_value: {error_types: [:greater_than_or_equal_to]}
12
+ end
13
+
14
+ def as_json(*)
15
+ super.merge(min_query_length: min_query_length).compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module FocusableOnLoad
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute :focus_on_load, :boolean
10
+ end
11
+
12
+ def as_json(*)
13
+ super.merge(focus_on_load: focus_on_load).compact
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module HasInitialOption
6
+ def initial_option
7
+ all_options&.reverse&.find(&:initial?)
8
+ end
9
+
10
+ def as_json(*)
11
+ super.merge(initial_option: initial_option&.as_json).compact
12
+ end
13
+
14
+ private
15
+
16
+ def all_options
17
+ opts = Array(options)
18
+ opts += Array(option_groups).flat_map(&:options) if respond_to?(:option_groups)
19
+ opts
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module HasInitialOptions
6
+ def initial_options
7
+ all_options&.select(&:initial?)
8
+ end
9
+
10
+ def as_json(*)
11
+ super.merge(initial_options: initial_options&.map(&:as_json).presence).compact
12
+ end
13
+
14
+ private
15
+
16
+ def all_options
17
+ opts = Array(options)
18
+ opts += Array(option_groups).flat_map(&:options) if respond_to?(:option_groups)
19
+ opts
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module HasOptionGroups
6
+ def self.new(limit:, options_limit:)
7
+ Module.new do
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include HasOptions.new(limit: options_limit)
12
+
13
+ attribute :option_groups, Types::Array.of(Composition::OptionGroup)
14
+ validates :option_groups, length: {maximum: limit, message: "is too long (maximum is %{count} groups)"}, "block_kit/validators/associated": true
15
+ fixes :option_groups, truncate: {maximum: limit, dangerous: true}, associated: true
16
+
17
+ validate :options_or_option_groups
18
+
19
+ dsl_method :option_groups, as: :option_group, required_fields: [:label]
20
+ end
21
+
22
+ def as_json(*)
23
+ super.merge(option_groups: option_groups&.map(&:as_json)).compact
24
+ end
25
+
26
+ private
27
+
28
+ def options_or_option_groups
29
+ if (options.blank? && option_groups.blank?) || (options.present? && option_groups.present?)
30
+ errors.add(:base, "must have either options or option groups, not both")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module HasOptions
6
+ def self.new(limit:)
7
+ Module.new do
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ attribute :options, Types::Array.of(Composition::Option)
12
+ validates :options,
13
+ presence: {unless: ->(block) { block.respond_to?(:option_groups) }},
14
+ length: {maximum: limit, message: "is too long (maximum is %{count} options)"},
15
+ "block_kit/validators/associated": true
16
+ fixes :options, truncate: {maximum: limit, dangerous: true}, associated: true
17
+
18
+ dsl_method :options, as: :option, required_fields: [:text, :value], yields: false
19
+ end
20
+
21
+ def as_json(*)
22
+ super.merge(options: options&.map(&:as_json))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end