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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module BlockKit
6
+ module Surfaces
7
+ class Modal < Base
8
+ self.type = :modal
9
+
10
+ MAX_TITLE_LENGTH = 24
11
+ MAX_BUTTON_LENGTH = 24
12
+ SUPPORTED_ELEMENTS = [
13
+ Elements::Button,
14
+ Elements::ChannelsSelect,
15
+ Elements::Checkboxes,
16
+ Elements::ConversationsSelect,
17
+ Elements::DatePicker,
18
+ Elements::DatetimePicker,
19
+ Elements::EmailTextInput,
20
+ Elements::ExternalSelect,
21
+ Elements::FileInput,
22
+ Elements::Image,
23
+ Elements::MultiChannelsSelect,
24
+ Elements::MultiConversationsSelect,
25
+ Elements::MultiExternalSelect,
26
+ Elements::MultiStaticSelect,
27
+ Elements::MultiUsersSelect,
28
+ Elements::NumberInput,
29
+ Elements::Overflow,
30
+ Elements::PlainTextInput,
31
+ Elements::RadioButtons,
32
+ Elements::RichTextInput,
33
+ Elements::StaticSelect,
34
+ Elements::TimePicker,
35
+ Elements::URLTextInput,
36
+ Elements::UsersSelect
37
+ ].freeze
38
+
39
+ plain_text_attribute :title
40
+ plain_text_attribute :close
41
+ plain_text_attribute :submit
42
+ attribute :clear_on_close, :boolean
43
+ attribute :notify_on_close, :boolean
44
+ attribute :submit_disabled, :boolean
45
+
46
+ validates :title, presence: true, length: {maximum: MAX_TITLE_LENGTH}
47
+ fixes :title, truncate: {maximum: MAX_TITLE_LENGTH}
48
+
49
+ validates :close, presence: true, length: {maximum: MAX_BUTTON_LENGTH}, allow_nil: true
50
+ fixes :close, truncate: {maximum: MAX_BUTTON_LENGTH}, null_value: {error_types: [:blank]}
51
+
52
+ validates :submit, presence: true, length: {maximum: MAX_BUTTON_LENGTH}, allow_nil: true
53
+ fixes :submit, truncate: {maximum: MAX_BUTTON_LENGTH}, null_value: {error_types: [:blank]}
54
+
55
+ def initialize(attributes = {})
56
+ attributes = attributes.with_indifferent_access
57
+ attributes[:blocks] ||= []
58
+
59
+ super
60
+ end
61
+
62
+ def as_json(*)
63
+ super.merge(
64
+ title: title&.as_json,
65
+ close: close&.as_json,
66
+ submit: submit&.as_json,
67
+ clear_on_close: clear_on_close,
68
+ notify_on_close: notify_on_close,
69
+ submit_disabled: submit_disabled
70
+ ).compact
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Surfaces
5
+ autoload :Base, "block_kit/surfaces/base"
6
+
7
+ autoload :Home, "block_kit/surfaces/home"
8
+ autoload :Message, "block_kit/surfaces/message"
9
+ autoload :Modal, "block_kit/surfaces/modal"
10
+ end
11
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ # Custom array class that maintains type constraints during mutation
5
+ class TypedArray < ::Array
6
+ attr_reader :item_type
7
+
8
+ def initialize(item_type, *args)
9
+ @item_type = item_type
10
+
11
+ # Array.new supports three different argument signatures:
12
+ #
13
+ # 1. Array.new(size = nil, object = nil): creates an array of the given size
14
+ # filled with the given object
15
+ # 2. Array.new(array): creates a new array that is a copy of the given array
16
+ # 3. Array.new(size, &block): creates a new array of the given size, passing
17
+ # each index to the block and using the block's return value to fill the array
18
+ #
19
+ # We need to handle all three cases, as they all result in filling the array.
20
+ # Calling `super(*)` and then `map!` does not work, as it does not end up
21
+ # modifying the array for some reason.
22
+ if args.size == 1 && args.first.is_a?(::Array)
23
+ super(args.first.map { |item| item_type.cast(item) }.compact)
24
+ elsif args.size == 2
25
+ super(args[0], item_type.cast(item)).compact!
26
+ elsif args.size == 1 && block_given?
27
+ super(args[0]) { |index| item_type.cast(yield(index)) }
28
+ end
29
+ end
30
+
31
+ # Override methods that modify the array to ensure type constraints
32
+ def <<(item)
33
+ super(item_type.cast(item)).tap(&:compact!)
34
+ end
35
+
36
+ def push(*items)
37
+ super(*items.map { |item| item_type.cast(item) }.compact)
38
+ end
39
+
40
+ def unshift(*items)
41
+ super(*items.map { |item| item_type.cast(item) }.compact)
42
+ end
43
+
44
+ def insert(index, *items)
45
+ super(index, *items.map { |item| item_type.cast(item) }.compact)
46
+ end
47
+
48
+ def []=(position, *args)
49
+ if args.length == 0
50
+ # array[1] = value
51
+ super(position, item_type.cast(position))
52
+ elsif args.length == 1
53
+ if position.is_a?(Range)
54
+ # array[1..3] = [x, y, z]
55
+ value = args[0]
56
+ if value.is_a?(::Array)
57
+ super(position, value.map { |item| item_type.cast(item) })
58
+ else
59
+ super(position, item_type.cast(value))
60
+ end
61
+ else
62
+ # array[1] = value
63
+ super(position, item_type.cast(args[0]))
64
+ end
65
+ else
66
+ # array[1, 2] = [x, y]
67
+ length, value = args
68
+ if value.is_a?(::Array)
69
+ super(position, length, value.map { |item| item_type.cast(item) })
70
+ else
71
+ super(position, length, item_type.cast(value))
72
+ end
73
+ end
74
+
75
+ compact!
76
+ end
77
+
78
+ def replace(other_array)
79
+ super(other_array.map { |item| item_type.cast(item) }.compact)
80
+ end
81
+
82
+ def fill(*args, &block)
83
+ if block_given?
84
+ # If a block is given, we need to intercept its results
85
+ modified_block = proc { |*block_args| item_type.cast(yield(*block_args)) }
86
+ super(*args, &modified_block)
87
+ elsif args.size > 0
88
+ # No block, so the last argument is the value to fill with
89
+ value = args.pop
90
+ super(*args, item_type.cast(value))
91
+ else
92
+ super
93
+ end
94
+ end
95
+
96
+ def map!(&block)
97
+ super { |item| item_type.cast(yield(item)) }.compact
98
+ end
99
+
100
+ def collect!(&block)
101
+ super { |item| item_type.cast(yield(item)) }.compact
102
+ end
103
+
104
+ def concat(other_array)
105
+ super(other_array.map { |item| item_type.cast(item) }).compact
106
+ end
107
+
108
+ # Note: methods that return new arrays (like `+`, `&`, `|`, etc.) are not
109
+ # overridden, as it's not necessary to enforce type constraints on the
110
+ # resulting array. If the resulting array is later assigned back to the
111
+ # attribute backing the original array, type constraints will be enforced
112
+ # at that time.
113
+ end
114
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ # Custom array class that maintains type constraints during mutation
5
+ class TypedSet < Set
6
+ attr_reader :item_type
7
+
8
+ def initialize(item_type, enum = nil)
9
+ # Only support scalar item types for now
10
+ unless item_type.respond_to?(:type) && !item_type.type.start_with?("block_kit_")
11
+ raise ArgumentError, "Only scalar item types are supported"
12
+ end
13
+
14
+ @item_type = item_type
15
+
16
+ super(enum&.map { |item| item_type.cast(item) }&.compact)
17
+ end
18
+
19
+ # Override methods that modify the set to ensure type constraints.
20
+ def add(item)
21
+ super(item_type.cast(item)).tap(&:compact!)
22
+ end
23
+ alias_method :<<, :add
24
+
25
+ def replace(other)
26
+ super(other.map { |item| item_type.cast(item) }.compact)
27
+ end
28
+
29
+ def map!(&block)
30
+ super { |item| item_type.cast(yield(item)) }.compact
31
+ end
32
+
33
+ def collect!(&block)
34
+ super { |item| item_type.cast(yield(item)) }.compact
35
+ end
36
+
37
+ def merge(other)
38
+ super(other.map { |item| item_type.cast(item) }).compact
39
+ end
40
+
41
+ def &(other)
42
+ n = self.class.new(item_type)
43
+ if other.is_a?(Set)
44
+ each { |item| n.add(item) if other.include?(item) }
45
+ else
46
+ do_with_enum(other) { |item| n.add(item) if include?(item) }
47
+ end
48
+
49
+ n.compact
50
+ end
51
+ alias_method :intersection, :&
52
+
53
+ def compact!
54
+ delete(nil) { return nil }
55
+ self
56
+ end
57
+
58
+ def compact
59
+ dup.tap(&:compact!)
60
+ end
61
+
62
+ def inspect
63
+ super.tap { |str| str.sub!("Set", "TypedSet[#{item_type.type}]") }
64
+ end
65
+
66
+ private
67
+
68
+ def do_with_enum(enum, &block)
69
+ if enum.respond_to?(:each_entry)
70
+ enum.each_entry(&block) if block
71
+ elsif enum.respond_to?(:each)
72
+ enum.each(&block) if block
73
+ else
74
+ raise ArgumentError, "value must be enumerable"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module BlockKit
6
+ module Types
7
+ # Allows declaring ActiveModel attributes that are arrays of specific types,
8
+ # powered by an internal TypedArray class that enforces type constraints on
9
+ # array elements any time the array is modified.
10
+ class Array < ActiveModel::Type::Value
11
+ attr_reader :item_type
12
+
13
+ def self.of(item_type)
14
+ new(item_type)
15
+ end
16
+
17
+ def initialize(item_type)
18
+ item_type = ActiveModel::Type.lookup(item_type) if item_type.is_a?(Symbol)
19
+ item_type = Types::Generic.of_type(item_type) if item_type.is_a?(Class) && item_type < BlockKit::Base
20
+
21
+ @item_type = item_type
22
+ end
23
+
24
+ def type
25
+ :array
26
+ end
27
+
28
+ def cast(value)
29
+ return nil if value.nil?
30
+
31
+ TypedArray.new(item_type, Array(value))
32
+ end
33
+
34
+ def serialize(value)
35
+ return nil if value.nil?
36
+
37
+ cast(value)
38
+ end
39
+
40
+ def changed_in_place?(raw_old_value, new_value)
41
+ cast(raw_old_value) != cast(new_value)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module BlockKit
6
+ module Types
7
+ # Provides a way to generate an ActiveModel type for multiple block types.
8
+ class Blocks < ActiveModel::Type::Value
9
+ attr_reader :block_classes, :block_types
10
+
11
+ def initialize(*block_classes)
12
+ @block_classes = block_classes
13
+ @block_types = block_classes.map { |block_class| Types::Generic.of_type(block_class) }
14
+ end
15
+
16
+ def type
17
+ :block_kit_block
18
+ end
19
+
20
+ def cast(value)
21
+ case value
22
+ when *@block_classes
23
+ value
24
+ when Hash
25
+ value = value.with_indifferent_access
26
+ type_name = value[:type]&.to_sym
27
+
28
+ matching_type = @block_types.find do |type_class|
29
+ type_class.respond_to?(:type) && type_class.type == :"block_kit_#{type_name}"
30
+ end
31
+
32
+ matching_type&.cast(value)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module BlockKit
6
+ module Types
7
+ # Provides a way to generate generic ActiveModel types for individual block types.
8
+ # Most block types can simply be cast from an object of the same type or a Hash with
9
+ # the object's attributes.
10
+ class Generic < ActiveModel::Type::Value
11
+ class_attribute :instances, default: {}
12
+
13
+ def self.new(block_class)
14
+ instances[block_class] ||= super
15
+ end
16
+
17
+ class << self
18
+ alias_method :of_type, :new
19
+ end
20
+
21
+ # These block types have special casting logic.
22
+ instances[Composition::Text] = Text.instance
23
+ instances[Composition::PlainText] = PlainText.instance
24
+ instances[Composition::Mrkdwn] = Mrkdwn.instance
25
+ instances[Composition::Option] = Option.instance
26
+ instances[Composition::OverflowOption] = OverflowOption.instance
27
+
28
+ attr_reader :type, :block_class
29
+
30
+ def initialize(block_class)
31
+ @block_class = block_class
32
+ @type = :"block_kit_#{block_class.type}"
33
+ end
34
+
35
+ def cast(value)
36
+ case value
37
+ when block_class
38
+ value
39
+ when Hash
40
+ block_class.new(**value.with_indifferent_access.slice(*block_class.attribute_names).symbolize_keys)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "singleton"
5
+
6
+ module BlockKit
7
+ module Types
8
+ class Option < ActiveModel::Type::Value
9
+ def block_class = Composition::Option
10
+
11
+ include Singleton
12
+
13
+ def type
14
+ :block_kit_option
15
+ end
16
+
17
+ def cast(value)
18
+ case value
19
+ when Composition::OverflowOption
20
+ block_class.new(text: value.text, value: value.value)
21
+ when block_class
22
+ value
23
+ when Hash
24
+ block_class.new(**value.with_indifferent_access.slice(*block_class.attribute_names).symbolize_keys)
25
+ end
26
+ end
27
+ end
28
+
29
+ class OverflowOption < Option
30
+ def block_class = Composition::OverflowOption
31
+
32
+ def type
33
+ :block_kit_overflow_option
34
+ end
35
+
36
+ def cast(value)
37
+ case value
38
+ when block_class
39
+ value
40
+ when Composition::Option
41
+ block_class.new(text: value.text, value: value.value)
42
+ when Hash
43
+ block_class.new(**value.with_indifferent_access.slice(*block_class.attribute_names).symbolize_keys)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module BlockKit
6
+ module Types
7
+ # Allows declaring ActiveModel attributes that are Sets of specific types,
8
+ # powered by an internal TypedSet class that enforces type constraints on
9
+ # set members any time the set is modified or any time set operations are
10
+ # performed against other collections.
11
+ class Set < Array
12
+ attr_reader :item_type
13
+
14
+ def type
15
+ :set
16
+ end
17
+
18
+ def cast(value)
19
+ return nil if value.nil?
20
+
21
+ TypedSet.new(item_type, Array(value))
22
+ end
23
+
24
+ def serialize(value)
25
+ return nil if value.nil?
26
+
27
+ cast(value)
28
+ end
29
+
30
+ def changed_in_place?(raw_old_value, new_value)
31
+ cast(raw_old_value) != cast(new_value)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "singleton"
5
+
6
+ module BlockKit
7
+ module Types
8
+ class Text < ActiveModel::Type::Value
9
+ include Singleton
10
+
11
+ def type
12
+ :block_kit_text
13
+ end
14
+
15
+ def cast(value)
16
+ case value
17
+ when Composition::PlainText, Composition::Mrkdwn, NilClass
18
+ value
19
+ when Hash
20
+ # Check for a `:type` key, otherwise prefer Mrkdwn over PlainText except
21
+ # in the explicit case where only the `:text` and `:emoji` keys are provided
22
+ type = value[:type]&.to_sym
23
+ if type == :mrkdwn
24
+ Mrkdwn.instance.cast(value)
25
+ elsif type == :plain_text
26
+ PlainText.instance.cast(value)
27
+ elsif value.key?(:verbatim) || !value.key?(:emoji)
28
+ Mrkdwn.instance.cast(value)
29
+ else
30
+ PlainText.instance.cast(value)
31
+ end
32
+ else
33
+ Mrkdwn.instance.cast(value)
34
+ end
35
+ end
36
+ end
37
+
38
+ class PlainText < Text
39
+ def block_class = Composition::PlainText
40
+
41
+ def type
42
+ :block_kit_plain_text
43
+ end
44
+
45
+ def cast(value)
46
+ case value
47
+ when block_class, NilClass
48
+ value
49
+ when Composition::Mrkdwn
50
+ block_class.new(text: value.text)
51
+ when Hash
52
+ block_class.new(**value.with_indifferent_access.slice(*block_class.attribute_names).symbolize_keys)
53
+ else
54
+ block_class.new(text: value)
55
+ end
56
+ end
57
+ end
58
+
59
+ class Mrkdwn < Text
60
+ def block_class = Composition::Mrkdwn
61
+
62
+ def type
63
+ :block_kit_mrkdwn
64
+ end
65
+
66
+ def cast(value)
67
+ case value
68
+ when block_class, NilClass
69
+ value
70
+ when Composition::PlainText
71
+ block_class.new(text: value.text)
72
+ when Hash
73
+ block_class.new(**value.with_indifferent_access.slice(*block_class.attribute_names).symbolize_keys)
74
+ else
75
+ block_class.new(text: value)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Types
5
+ autoload :Generic, "block_kit/types/generic"
6
+ autoload :Blocks, "block_kit/types/blocks"
7
+
8
+ autoload :Array, "block_kit/types/array"
9
+ autoload :Mrkdwn, "block_kit/types/text"
10
+ autoload :NumberInput, "block_kit/types/number_input"
11
+ autoload :Option, "block_kit/types/option"
12
+ autoload :OverflowOption, "block_kit/types/option"
13
+ autoload :PlainText, "block_kit/types/text"
14
+ autoload :Set, "block_kit/types/set"
15
+ autoload :Text, "block_kit/types/text"
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Source: https://github.com/sciencehistory/kithe/blob/master/app/validators/array_inclusion_validator.rb
4
+ #
5
+ # Like the default Rails inclusion validator, but the built-in Rails
6
+ # validator won't work on an _array_ of things.
7
+ #
8
+ # So if you have an array of primitive values, you can use this to
9
+ # validate that all elements of the array are in the inclusion list.
10
+ #
11
+ # Or that all the elements of the array are whatever you want,
12
+ # by supplying a proc that returns false for bad values.
13
+ #
14
+ # Empty arrays are always allowed, add a presence validator if you don't
15
+ # want to allow them, eg `validates :genre, presence: true, array_inclusion: { in: whatever }`
16
+ #
17
+ # @example
18
+ # class Work < Kithe::Work
19
+ # attr_json :genre, :string, array: true
20
+ # validates :genre, array_inclusion: { in: ALLOWED_GENRES }
21
+ # validates :genre, array_inclusion: { proc: ->(val) { val =~ /\d+/ } }
22
+ # #...
23
+ #
24
+ # Custom message can interpolate `rejected_values` value. (Should also work for i18n)
25
+ #
26
+ # Note: There isn't currently a great way to show primitive array validation errors on
27
+ # a form for an invalid edit, the validation error can only be shown as if for the entire
28
+ # array field, not the individual invalid edit. You might consider modelling as a compound
29
+ # Model with only one attribute instead of as a primitive.
30
+ #
31
+ # @example
32
+ # validates :genre, array_inclusion: { in: ALLOWED_GENRES, message: "option %{rejected_values} not allowed" }
33
+ module BlockKit
34
+ module Validators
35
+ class ArrayInclusionValidator < ActiveModel::EachValidator
36
+ def validate_each(record, attribute, value)
37
+ values = Array(value) || []
38
+ not_allowed_values = []
39
+
40
+ if options[:in]
41
+ not_allowed_values.concat(values - options[:in])
42
+ end
43
+
44
+ if options[:proc]
45
+ not_allowed_values.concat(values.find_all { |v| !options[:proc].call(v) })
46
+ end
47
+
48
+ unless not_allowed_values.blank?
49
+ formatted_rejected = not_allowed_values.uniq.collect(&:inspect).join(",")
50
+ record.errors.add(attribute, :inclusion, **options.except(:in).merge!(rejected_values: formatted_rejected, value: value, invalid_values: not_allowed_values))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end