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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class NumberInput < Base
6
+ self.type = :number_input
7
+
8
+ include Concerns::Dispatchable
9
+ include Concerns::FocusableOnLoad
10
+ include Concerns::HasPlaceholder
11
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
12
+
13
+ attribute :is_decimal_allowed, :boolean
14
+ attribute :min_value, :decimal
15
+ attribute :max_value, :decimal
16
+ attribute :initial_value, :decimal
17
+
18
+ validates :min_value, numericality: {less_than_or_equal_to: :max_value}, allow_nil: true, if: :max_value
19
+ validates :max_value, numericality: {greater_than_or_equal_to: :min_value}, allow_nil: true, if: :min_value
20
+ validates :initial_value, numericality: {greater_than_or_equal_to: :min_value}, allow_nil: true, if: :min_value
21
+ validates :initial_value, numericality: {less_than_or_equal_to: :max_value}, allow_nil: true, if: :max_value
22
+
23
+ fixes :initial_value, null_value: {error_types: [:greater_than_or_equal_to, :less_than_or_equal_to]}
24
+
25
+ def is_decimal_allowed?
26
+ !!is_decimal_allowed
27
+ end
28
+ alias_method :decimal_allowed?, :is_decimal_allowed?
29
+
30
+ def min_value
31
+ decimal_allowed? ? super : super&.to_i
32
+ end
33
+
34
+ def max_value
35
+ decimal_allowed? ? super : super&.to_i
36
+ end
37
+
38
+ def initial_value
39
+ decimal_allowed? ? super : super&.to_i
40
+ end
41
+
42
+ def as_json(*)
43
+ super.merge(
44
+ is_decimal_allowed: is_decimal_allowed,
45
+ initial_value: initial_value.respond_to?(:to_digits) ? initial_value.to_digits : initial_value&.to_i,
46
+ min_value: min_value.respond_to?(:to_digits) ? min_value.to_digits : min_value&.to_i,
47
+ max_value: max_value.respond_to?(:to_digits) ? max_value.to_digits : max_value&.to_i
48
+ ).compact
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class Overflow < Base
6
+ self.type = :overflow
7
+
8
+ MAX_OPTIONS = 5
9
+
10
+ include Concerns::Confirmable
11
+
12
+ attribute :options, Types::Array.of(Composition::OverflowOption)
13
+ validates :options, presence: true, length: {maximum: MAX_OPTIONS, message: "is too long (maximum is %{count} options)"}, "block_kit/validators/associated": true
14
+ fixes :options, truncate: {maximum: MAX_OPTIONS, dangerous: true}, associated: true
15
+
16
+ dsl_method :options, as: :option, required_fields: [:text, :value], yields: false
17
+
18
+ def as_json(*)
19
+ super.merge(options: options&.map(&:as_json))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/inclusion"
4
+ require "uri"
5
+
6
+ module BlockKit
7
+ module Elements
8
+ class PlainTextInput < Base
9
+ self.type = :plain_text_input
10
+
11
+ include Concerns::Dispatchable
12
+ include Concerns::FocusableOnLoad
13
+ include Concerns::HasPlaceholder
14
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
15
+
16
+ attribute :initial_value, :string
17
+ attribute :min_length, :integer
18
+ attribute :max_length, :integer
19
+ attribute :multiline, :boolean
20
+
21
+ validates :min_length, presence: true, numericality: {only_integer: true}, allow_nil: true
22
+ validates :max_length, presence: true, numericality: {only_integer: true}, allow_nil: true
23
+ validate :min_and_max_lengths_are_valid
24
+
25
+ validates :initial_value, presence: true, length: {
26
+ minimum: ->(input) { input.min_length || 0 },
27
+ maximum: ->(input) { input.max_length || 3000 }
28
+ }, allow_nil: true
29
+
30
+ fixes :initial_value,
31
+ truncate: {maximum: ->(input) { input.max_length || 3000 }},
32
+ null_value: {error_types: [:blank, :too_short]}
33
+
34
+ def as_json(*)
35
+ super.merge(
36
+ initial_value: initial_value,
37
+ min_length: min_length,
38
+ max_length: max_length,
39
+ multiline: multiline
40
+ ).compact
41
+ end
42
+
43
+ private
44
+
45
+ # These validations are more complex than, say, the `number_input` validations due to
46
+ # the fact that `min_length` can be 0 but `max_length` must be at least 1.
47
+ def min_and_max_lengths_are_valid
48
+ if min_length && max_length
49
+ if min_length > max_length
50
+ errors.add(:min_length, "must be less than or equal to max_length")
51
+ elsif max_length < min_length
52
+ errors.add(:max_length, "must be greater than or equal to min_length")
53
+ end
54
+ end
55
+
56
+ if min_length && !min_length.in?(0..3000)
57
+ errors.add(:min_length, "must be in 0..3000")
58
+ end
59
+
60
+ if max_length && !max_length.in?(1..3000)
61
+ errors.add(:max_length, "must be in 1..3000")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class RadioButtons < Base
6
+ self.type = :radio_buttons
7
+
8
+ include Concerns::Confirmable
9
+ include Concerns::HasOptions.new(limit: 10)
10
+ include Concerns::HasInitialOption
11
+ include Concerns::FocusableOnLoad
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Elements
7
+ class RichTextInput < Base
8
+ self.type = :rich_text_input
9
+
10
+ include Concerns::Dispatchable
11
+ include Concerns::FocusableOnLoad
12
+ include Concerns::HasPlaceholder
13
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
14
+
15
+ attribute :initial_value, Types::Generic.of_type(Layout::RichText)
16
+ validates :initial_value, "block_kit/validators/associated": true, allow_nil: true
17
+ fixes :initial_value, associated: true
18
+
19
+ dsl_method :initial_value
20
+
21
+ def as_json(*)
22
+ super.merge(initial_value: initial_value&.as_json).compact
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class Select < Base
6
+ include Concerns::Confirmable
7
+ include Concerns::FocusableOnLoad
8
+ include Concerns::HasPlaceholder
9
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
10
+
11
+ def initialize(attributes = {})
12
+ raise NotImplementedError, "#{self.class} is an abstract class and can't be instantiated." if instance_of?(Select)
13
+
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class StaticSelect < Select
6
+ self.type = :static_select
7
+
8
+ include Concerns::HasOptionGroups.new(limit: 100, options_limit: 100)
9
+ include Concerns::HasInitialOption
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class TimePicker < Base
6
+ self.type = :timepicker
7
+
8
+ include Concerns::Confirmable
9
+ include Concerns::FocusableOnLoad
10
+ include Concerns::HasPlaceholder
11
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
12
+
13
+ attribute :initial_time, :time, precision: 0
14
+ attribute :timezone, :string
15
+
16
+ validate :timezone_is_valid
17
+ fixes :timezone, null_value: {error_types: [:blank, :invalid]}
18
+
19
+ def timezone
20
+ ActiveSupport::TimeZone[super] if super
21
+ end
22
+
23
+ def as_json(*)
24
+ super.merge(
25
+ initial_time: initial_time&.strftime("%H:%M"),
26
+ timezone: timezone&.name
27
+ ).compact
28
+ end
29
+
30
+ private
31
+
32
+ def timezone_is_valid
33
+ return if attributes["timezone"].nil?
34
+
35
+ if attributes["timezone"].blank?
36
+ errors.add(:timezone, :blank)
37
+ else
38
+ errors.add(:timezone, :invalid, message: "is not a valid timezone") unless ActiveSupport::TimeZone[attributes["timezone"]]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Elements
7
+ class URLTextInput < Base
8
+ self.type = :url_text_input
9
+
10
+ include Concerns::Dispatchable
11
+ include Concerns::FocusableOnLoad
12
+ include Concerns::HasPlaceholder
13
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
14
+
15
+ attribute :initial_value, :string
16
+ validates :initial_value, presence: true, format: {with: URI::DEFAULT_PARSER.make_regexp, message: "is not a valid URI", allow_blank: true}, allow_nil: true
17
+ fixes :initial_value, null_value: {error_types: [:blank, :invalid]}
18
+
19
+ def as_json(*)
20
+ super.merge(initial_value: initial_value).compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class UsersSelect < Select
6
+ self.type = :users_select
7
+
8
+ attribute :initial_user, :string
9
+ validates :initial_user, presence: true, allow_nil: true
10
+ fixes :initial_user, null_value: {error_types: [:blank]}
11
+
12
+ def as_json(*)
13
+ super.merge(initial_user: initial_user).compact
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class WorkflowButton < BaseButton
6
+ self.type = :workflow_button
7
+
8
+ attribute :workflow, Types::Generic.of_type(Composition::Workflow)
9
+ validates :workflow, presence: true, "block_kit/validators/associated": true
10
+
11
+ dsl_method :workflow
12
+
13
+ def as_json(*)
14
+ super.merge(workflow: workflow&.as_json).compact
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ autoload :Base, "block_kit/elements/base"
6
+ autoload :BaseButton, "block_kit/elements/base_button"
7
+ autoload :Select, "block_kit/elements/select"
8
+ autoload :MultiSelect, "block_kit/elements/multi_select"
9
+ autoload :NumberInput, "block_kit/elements/number_input"
10
+
11
+ autoload :Button, "block_kit/elements/button"
12
+ autoload :ChannelsSelect, "block_kit/elements/channels_select"
13
+ autoload :Checkboxes, "block_kit/elements/checkboxes"
14
+ autoload :ConversationsSelect, "block_kit/elements/conversations_select"
15
+ autoload :DatePicker, "block_kit/elements/date_picker"
16
+ autoload :DatetimePicker, "block_kit/elements/datetime_picker"
17
+ autoload :EmailTextInput, "block_kit/elements/email_text_input"
18
+ autoload :ExternalSelect, "block_kit/elements/external_select"
19
+ autoload :FileInput, "block_kit/elements/file_input"
20
+ autoload :Image, "block_kit/elements/image"
21
+ autoload :MultiChannelsSelect, "block_kit/elements/multi_channels_select"
22
+ autoload :MultiConversationsSelect, "block_kit/elements/multi_conversations_select"
23
+ autoload :MultiExternalSelect, "block_kit/elements/multi_external_select"
24
+ autoload :MultiStaticSelect, "block_kit/elements/multi_static_select"
25
+ autoload :MultiUsersSelect, "block_kit/elements/multi_users_select"
26
+ autoload :Overflow, "block_kit/elements/overflow"
27
+ autoload :PlainTextInput, "block_kit/elements/plain_text_input"
28
+ autoload :RadioButtons, "block_kit/elements/radio_buttons"
29
+ autoload :RichTextInput, "block_kit/elements/rich_text_input"
30
+ autoload :StaticSelect, "block_kit/elements/static_select"
31
+ autoload :TimePicker, "block_kit/elements/time_picker"
32
+ autoload :URLTextInput, "block_kit/elements/url_text_input"
33
+ autoload :UsersSelect, "block_kit/elements/users_select"
34
+ autoload :WorkflowButton, "block_kit/elements/workflow_button"
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Fixers
5
+ class Associated < Base
6
+ def fix(model, fixing_dangerously: false)
7
+ model.validate
8
+ errors = errors_for(model)
9
+
10
+ return unless errors.any?
11
+
12
+ associated = model.public_send(attribute)
13
+
14
+ if associated.is_a?(Enumerable)
15
+ error = errors.find { |e| e.type == :invalid }
16
+ return unless error
17
+
18
+ error.options[:invalid_values].each do |associated|
19
+ associated.fix_validation_errors(dangerous: fixing_dangerously)
20
+ end
21
+ else
22
+ associated&.fix_validation_errors(dangerous: fixing_dangerously)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Fixers
5
+ class Base
6
+ include ActiveSupport::Callbacks
7
+
8
+ def initialize(attribute:, **options)
9
+ @attribute = attribute.to_sym
10
+ @dangerous = options.delete(:dangerous)
11
+ end
12
+
13
+ def fix(model, fixing_dangerously: false)
14
+ raise NotImplementedError, "#{self.class} must implement `fix'"
15
+ end
16
+
17
+ def dangerous?
18
+ !!@dangerous
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :attribute
24
+
25
+ def errors_for(model)
26
+ Array(model.errors.group_by_attribute[attribute])
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/inclusion"
4
+
5
+ module BlockKit
6
+ module Fixers
7
+ class NullValue < Base
8
+ def initialize(attribute:, **options)
9
+ super
10
+
11
+ @error_types = options.delete(:error_types) { [:blank] }
12
+ end
13
+
14
+ def fix(model, fixing_dangerously: false)
15
+ return if dangerous? && !fixing_dangerously
16
+
17
+ model.validate
18
+ errors = errors_for(model)
19
+
20
+ return unless errors.any? { |e| error_types.include?(e.type) }
21
+
22
+ value = model.attributes[attribute.to_s]
23
+
24
+ # First, check if the attribute is an array and if we have an `inclusion` error.
25
+ # If so, we'll remove invalid values from the array. If the new value is empty
26
+ # and this fixer is configured to handle `blank` errors, we'll nullify it.
27
+ if value.is_a?(Enumerable) && (error = errors.find { |e| e.type == :inclusion })
28
+ new_value = value - error.options[:invalid_values]
29
+ new_value = new_value.presence if error_types.include?(:blank)
30
+
31
+ model.assign_attributes(attribute => new_value)
32
+ else
33
+ model.assign_attributes(attribute => nil)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :error_types
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Fixers
5
+ class Truncate < Base
6
+ def initialize(attribute:, **options)
7
+ super
8
+
9
+ @maximum = options.delete(:maximum)
10
+ @omission = options.delete(:omission)
11
+ end
12
+
13
+ def fix(model, fixing_dangerously: false)
14
+ return if dangerous? && !fixing_dangerously
15
+
16
+ model.validate
17
+ errors = errors_for(model)
18
+
19
+ return unless errors.any? { |e| e.type == :too_long }
20
+
21
+ value = model.attributes[attribute.to_s]
22
+ maximum = @maximum.call(model) if @maximum.is_a?(Proc)
23
+ maximum ||= @maximum
24
+
25
+ new_value = if value.is_a?(Enumerable)
26
+ value.first(maximum)
27
+ else
28
+ value.truncate(maximum, omission: @omission)
29
+ end
30
+
31
+ model.assign_attributes(attribute => new_value)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Fixers
5
+ autoload :Base, "block_kit/fixers/base"
6
+
7
+ autoload :Associated, "block_kit/fixers/associated"
8
+ autoload :NullValue, "block_kit/fixers/null_value"
9
+ autoload :Truncate, "block_kit/fixers/truncate"
10
+ end
11
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Layout
5
+ class Actions < Base
6
+ self.type = :actions
7
+
8
+ MAX_ELEMENTS = 25
9
+ SUPPORTED_ELEMENTS = [
10
+ Elements::Button,
11
+ Elements::ChannelsSelect,
12
+ Elements::Checkboxes,
13
+ Elements::ConversationsSelect,
14
+ Elements::DatePicker,
15
+ Elements::DatetimePicker,
16
+ Elements::ExternalSelect,
17
+ Elements::MultiChannelsSelect,
18
+ Elements::MultiConversationsSelect,
19
+ Elements::MultiExternalSelect,
20
+ Elements::MultiStaticSelect,
21
+ Elements::MultiUsersSelect,
22
+ Elements::Overflow,
23
+ Elements::RadioButtons,
24
+ Elements::RichTextInput,
25
+ Elements::StaticSelect,
26
+ Elements::TimePicker,
27
+ Elements::UsersSelect,
28
+ Elements::WorkflowButton
29
+ ]
30
+
31
+ def initialize(attributes = {})
32
+ attributes = attributes.with_indifferent_access
33
+ attributes[:elements] ||= []
34
+
35
+ super
36
+ end
37
+
38
+ attribute :elements, Types::Array.of(Types::Blocks.new(*SUPPORTED_ELEMENTS))
39
+ validates :elements, presence: true, length: {maximum: MAX_ELEMENTS, message: "is too long (maximum is %{count} elements)"}, "block_kit/validators/associated": true
40
+ fixes :elements, truncate: {maximum: MAX_ELEMENTS, dangerous: true}, associated: true
41
+
42
+ dsl_method :elements, as: :button, type: Elements::Button, required_fields: [:text]
43
+ dsl_method :elements, as: :channels_select, type: Elements::ChannelsSelect
44
+ dsl_method :elements, as: :checkboxes, type: Elements::Checkboxes
45
+ dsl_method :elements, as: :conversations_select, type: Elements::ConversationsSelect
46
+ dsl_method :elements, as: :datepicker, type: Elements::DatePicker
47
+ dsl_method :elements, as: :datetimepicker, type: Elements::DatetimePicker
48
+ dsl_method :elements, as: :external_select, type: Elements::ExternalSelect
49
+ dsl_method :elements, as: :multi_channels_select, type: Elements::MultiChannelsSelect
50
+ dsl_method :elements, as: :multi_conversations_select, type: Elements::MultiConversationsSelect
51
+ dsl_method :elements, as: :multi_external_select, type: Elements::MultiExternalSelect
52
+ dsl_method :elements, as: :multi_static_select, type: Elements::MultiStaticSelect, mutually_exclusive_fields: [:options, :option_groups]
53
+ dsl_method :elements, as: :multi_users_select, type: Elements::MultiUsersSelect
54
+ dsl_method :elements, as: :overflow, type: Elements::Overflow
55
+ dsl_method :elements, as: :radio_buttons, type: Elements::RadioButtons
56
+ dsl_method :elements, as: :rich_text_input, type: Elements::RichTextInput
57
+ dsl_method :elements, as: :static_select, type: Elements::StaticSelect, mutually_exclusive_fields: [:options, :option_groups]
58
+ dsl_method :elements, as: :timepicker, type: Elements::TimePicker
59
+ dsl_method :elements, as: :users_select, type: Elements::UsersSelect
60
+ dsl_method :elements, as: :workflow_button, type: Elements::WorkflowButton, required_fields: [:text]
61
+
62
+ alias_method :channel_select, :channels_select
63
+ alias_method :conversation_select, :conversations_select
64
+ alias_method :date_picker, :datepicker
65
+ alias_method :datetime_picker, :datetimepicker
66
+ alias_method :multi_channel_select, :multi_channels_select
67
+ alias_method :multi_conversation_select, :multi_conversations_select
68
+ alias_method :multi_user_select, :multi_users_select
69
+ alias_method :overflow_menu, :overflow
70
+ alias_method :time_picker, :timepicker
71
+ alias_method :user_select, :users_select
72
+
73
+ def append(element)
74
+ elements << element
75
+
76
+ self
77
+ end
78
+
79
+ def as_json(*)
80
+ super.merge(elements: elements&.map(&:as_json)).compact
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Layout
5
+ class Base < BlockKit::Base
6
+ MAX_BLOCK_ID_LENGTH = 255
7
+
8
+ attribute :block_id, :string
9
+ validates :block_id, presence: true, length: {maximum: MAX_BLOCK_ID_LENGTH}, allow_nil: true
10
+ fixes :block_id, truncate: {maximum: MAX_BLOCK_ID_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
11
+
12
+ def initialize(attributes = {})
13
+ raise NotImplementedError, "#{self.class} is an abstract class and can't be instantiated." if instance_of?(Base)
14
+
15
+ super
16
+ end
17
+
18
+ def as_json(*)
19
+ super.merge(block_id: block_id).compact
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Layout
5
+ class Context < Base
6
+ self.type = :context
7
+
8
+ MAX_ELEMENTS = 10
9
+ SUPPORTED_ELEMENTS = [
10
+ Elements::Image,
11
+ Composition::Mrkdwn,
12
+ Composition::PlainText
13
+ ].freeze
14
+
15
+ attribute :elements, Types::Array.of(Types::Blocks.new(*SUPPORTED_ELEMENTS))
16
+ validates :elements, presence: true, length: {maximum: MAX_ELEMENTS, message: "is too long (maximum is %{count} elements)"}, "block_kit/validators/associated": true
17
+ fixes :elements, truncate: {maximum: MAX_ELEMENTS, dangerous: true}, associated: true
18
+
19
+ dsl_method :elements, as: :mrkdwn, type: Composition::Mrkdwn, required_fields: [:text], yields: false
20
+ dsl_method :elements, as: :plain_text, type: Composition::PlainText, required_fields: [:text], yields: false
21
+
22
+ def initialize(attributes = {})
23
+ attributes = attributes.with_indifferent_access
24
+ attributes[:elements] ||= []
25
+
26
+ super
27
+ end
28
+
29
+ def image(alt_text:, image_url: nil, slack_file: nil)
30
+ if (image_url.nil? && slack_file.nil?) || (image_url && slack_file)
31
+ raise ArgumentError, "Must provide either image_url or slack_file, but not both."
32
+ end
33
+
34
+ append(Elements::Image.new(alt_text: alt_text, image_url: image_url, slack_file: slack_file))
35
+ end
36
+
37
+ def append(element)
38
+ elements << element
39
+
40
+ self
41
+ end
42
+
43
+ def as_json(*)
44
+ super.merge(elements: elements&.map(&:as_json)).compact
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Layout
5
+ class Divider < Base
6
+ self.type = :divider
7
+ end
8
+ end
9
+ end