slack_valid_block_kit 0.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.
@@ -0,0 +1,86 @@
1
+ module SlackValidBlockKit
2
+ module Builder
3
+ module MethodGenerator
4
+ def self.generate(name, is_typed, required: [], optional: [])
5
+ args = required.map { |p| "#{p}:" } + optional.map { |p| "#{p}: nil" }
6
+ first_hash = is_typed ? "{ type: '#{name}' }" : "{}"
7
+ set_stmts = required.map { |p| " hash[:#{p}] = #{p}" }
8
+ set_stmts += optional.map { |p| " hash[:#{p}] = #{p} unless #{p}.nil?" }
9
+ <<~EOD
10
+ def #{name}(#{args.join(', ')})
11
+ hash = #{first_hash}
12
+ #{set_stmts.join("\n")}
13
+ hash
14
+ end
15
+ EOD
16
+ end
17
+
18
+ def self.generate_surfaces(indent_level = 0)
19
+ indent = " " * indent_level
20
+ arr = []
21
+ arr << generate("modal", true, required: %w(title blocks), optional: %w(close submit private_metadata callback_id clear_on_close notify_on_close external_id submit_disabled))
22
+ arr << generate("home", true, required: %w(blocks), optional: %w(private_metadata callback_id external_id))
23
+ arr << generate("blocks", false, required: %w(blocks), optional: [])
24
+ indent + arr.join("\n#{indent}")
25
+ end
26
+
27
+ def self.generate_layouts(indent_level = 0)
28
+ indent = " " * indent_level
29
+ arr = []
30
+ arr << generate("actions", true, required: %w(elements), optional: %w(block_id))
31
+ arr << generate("context", true, required: %w(elements), optional: %w(block_id))
32
+ arr << generate("divider", true, required: [], optional: %w(block_id))
33
+ arr << generate("file", true, required: %w(elements source), optional: %w(block_id))
34
+ arr << generate("header", true, required: %w(text), optional: %w(block_id))
35
+ arr << generate("image", true, required: %w(image_url alt_text), optional: %w(title block_id))
36
+ arr << generate("input", true, required: %w(label element), optional: %w(dispatch_action block_id hint optional))
37
+ arr << generate("section", true, required: [], optional: %w(text block_id fields accessory))
38
+
39
+ indent + arr.join("\n#{indent}")
40
+ end
41
+
42
+ def self.generate_elements(indent_level = 0)
43
+ indent = " " * indent_level
44
+ arr = []
45
+ arr << generate("button", true, required: %w(text action_id), optional: %w(url value style confirm accessibility_label))
46
+ arr << generate("checkboxes", true, required: %w(action_id options), optional: %w(initial_options confirm focus_on_load))
47
+ arr << generate("datepicker", true, required: %w(action_id), optional: %w(placeholder initial_date confirm focus_on_load))
48
+ arr << generate("image", true, required: %w(image_url alt_text), optional: [])
49
+
50
+ arr << generate("multi_static_select", true, required: %w(placeholder action_id options), optional: %w(option_groups initial_options confirm max_selected_items focus_on_load))
51
+ arr << generate("multi_external_select", true, required: %w(placeholder action_id), optional: %w(min_query_length initial_options confirm max_selected_items focus_on_load))
52
+ arr << generate("multi_users_select", true, required: %w(placeholder action_id), optional: %w(initial_users confirm max_selected_items focus_on_load))
53
+ arr << generate("multi_conversations_select", true, required: %w(placeholder action_id), optional: %w(initial_conversations default_to_current_conversation confirm max_selected_items filter focus_on_load))
54
+ arr << generate("multi_channels_select", true, required: %w(placeholder action_id), optional: %w(initial_channels confirm max_selected_items focus_on_load))
55
+
56
+ arr << generate("overflow", true, required: %w(options action_id), optional: %w(confirm))
57
+ arr << generate("plain_text_input", true, required: %w(action_id), optional: %w(placeholder initial_value multiline min_length max_length dispatch_action_config focus_on_load))
58
+ arr << generate("radio_buttons", true, required: %w(options action_id), optional: %w(initial_option confirm focus_on_load))
59
+
60
+ arr << generate("static_select", true, required: %w(placeholder action_id options), optional: %w(option_groups initial_option confirm focus_on_load))
61
+ arr << generate("external_select", true, required: %w(placeholder action_id), optional: %w(min_query_length initial_option confirm focus_on_load))
62
+ arr << generate("users_select", true, required: %w(placeholder action_id), optional: %w(initial_user confirm focus_on_load))
63
+ arr << generate("conversations_select", true, required: %w(placeholder action_id), optional: %w(initial_conversation default_to_current_conversation confirm response_url_enabled filter focus_on_load))
64
+ arr << generate("channels_select", true, required: %w(placeholder action_id), optional: %w(initial_channel confirm response_url_enabled focus_on_load))
65
+
66
+ arr << generate("timepicker", true, required: %w(action_id), optional: %w(placeholder initial_time confirm focus_on_load))
67
+
68
+ indent + arr.join("\n#{indent}")
69
+ end
70
+
71
+ def self.generate_composition(indent_level = 0)
72
+ indent = " " * indent_level
73
+ arr = []
74
+ arr << generate("plain_text", true, required: %w(text), optional: %w(emoji))
75
+ arr << generate("mrkdwn", true, required: %w(text), optional: %w(verbatim))
76
+ arr << generate("confirmation", false, required: %w(title text confirm deny), optional: %w(style))
77
+ arr << generate("option", false, required: %w(text value), optional: %w(description url))
78
+ arr << generate("option_group", false, required: %w(label options), optional: [])
79
+ arr << generate("dispatch_action_configuration", false, required: [], optional: %w(trigger_actions_on))
80
+ arr << generate("filter", false, required: [], optional: %w(include exclude_external_shared_channels exclude_bot_users))
81
+
82
+ indent + arr.join("\n#{indent}")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,35 @@
1
+ module SlackValidBlockKit
2
+ module Builder
3
+ module Surfaces
4
+ def modal(title:, blocks:, close: nil, submit: nil, private_metadata: nil, callback_id: nil, clear_on_close: nil, notify_on_close: nil, external_id: nil, submit_disabled: nil)
5
+ hash = { type: 'modal' }
6
+ hash[:title] = title
7
+ hash[:blocks] = blocks
8
+ hash[:close] = close unless close.nil?
9
+ hash[:submit] = submit unless submit.nil?
10
+ hash[:private_metadata] = private_metadata unless private_metadata.nil?
11
+ hash[:callback_id] = callback_id unless callback_id.nil?
12
+ hash[:clear_on_close] = clear_on_close unless clear_on_close.nil?
13
+ hash[:notify_on_close] = notify_on_close unless notify_on_close.nil?
14
+ hash[:external_id] = external_id unless external_id.nil?
15
+ hash[:submit_disabled] = submit_disabled unless submit_disabled.nil?
16
+ hash
17
+ end
18
+
19
+ def home(blocks:, private_metadata: nil, callback_id: nil, external_id: nil)
20
+ hash = { type: 'home' }
21
+ hash[:blocks] = blocks
22
+ hash[:private_metadata] = private_metadata unless private_metadata.nil?
23
+ hash[:callback_id] = callback_id unless callback_id.nil?
24
+ hash[:external_id] = external_id unless external_id.nil?
25
+ hash
26
+ end
27
+
28
+ def blocks(blocks:)
29
+ hash = {}
30
+ hash[:blocks] = blocks
31
+ hash
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ require "slack_valid_block_kit/builder/compositions"
2
+ require "slack_valid_block_kit/builder/elements"
3
+ require "slack_valid_block_kit/builder/layouts"
4
+ require "slack_valid_block_kit/builder/surfaces"
5
+
6
+ module SlackValidBlockKit
7
+ module Builder
8
+ class Runner
9
+ include ::SlackValidBlockKit::Builder::Surfaces
10
+ include ::SlackValidBlockKit::Builder::Layouts
11
+ include ::SlackValidBlockKit::Builder::Elements
12
+ include ::SlackValidBlockKit::Builder::Compositions
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module SlackValidBlockKit
2
+ class Config
3
+ class << self
4
+ def skip_validation=(v)
5
+ @_skip_validation = v
6
+ end
7
+
8
+ def skip_validation
9
+ @_skip_validation
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,164 @@
1
+ module SlackValidBlockKit::Validator
2
+ module Base
3
+ MULTI_SELECT_TYPES = %w(multi_static_select multi_external_select multi_users_select multi_conversations_select multi_channels_select)
4
+ SELECT_TYPES = %w(static_select external_select users_select conversations_select channels_select)
5
+ ACTION_ELEMENT_TYPES = %w(button overflow datepicker radio_buttons checkboxes timepicker) + SELECT_TYPES + MULTI_SELECT_TYPES
6
+ INPUT_ELEMENT_TYPES = %w(checkboxes datepicker plain_text_input radio_buttons timepicker) + SELECT_TYPES + MULTI_SELECT_TYPES
7
+ ALL_ELEMENT_TYPES = %w(button overflow datepicker radio_buttons checkboxes timepicker plain_text_input image) + SELECT_TYPES + MULTI_SELECT_TYPES
8
+ LAYOUT_BLOCKS_FOR_MODAL = %w(actions context divider header image input section)
9
+ LAYOUT_BLOCKS_FOR_HOME = LAYOUT_BLOCKS_FOR_MODAL
10
+ LAYOUT_BLOCKS_FOR_MESSAGE = LAYOUT_BLOCKS_FOR_MODAL + %w(file)
11
+
12
+ BLOCK_KIT_GROUPS = {
13
+ action_elements: ACTION_ELEMENT_TYPES,
14
+ input_elements: INPUT_ELEMENT_TYPES,
15
+ all_element_types: ALL_ELEMENT_TYPES,
16
+ plain_text: ["plain_text"],
17
+ text_objects: %w(plain_text mrkdwn),
18
+
19
+ image_or_text: %(plain_text mrkdwn image),
20
+
21
+ layout_blocks_for_modal: LAYOUT_BLOCKS_FOR_MODAL,
22
+ layout_blocks_for_home: LAYOUT_BLOCKS_FOR_HOME,
23
+ layout_blocks_for_message: LAYOUT_BLOCKS_FOR_MESSAGE,
24
+
25
+ modal: ["modal"],
26
+ top: %w(modal home blocks)
27
+ }
28
+
29
+ def validate(obj, path, types, options = nil)
30
+ return if obj.nil?
31
+ type = obj[:type]
32
+ return unless validate_for_types(type, "#{path}.type", types)
33
+ self.send("validate_#{type}", obj, "#{path}(#{type})", options)
34
+ errors_by_path
35
+ end
36
+
37
+ def validate_for(obj, path, clazz_type, required, only: nil, max: nil, min: nil)
38
+ if obj.nil?
39
+ add_error(path, :required) if required
40
+ return
41
+ end
42
+
43
+ if blank?(obj)
44
+ add_error(path, :no_item)
45
+ return
46
+ end
47
+
48
+ if clazz_type == Array
49
+ if !obj.is_a?(Array)
50
+ add_error(path, :clazz_type, Array)
51
+ end
52
+ elsif clazz_type == :bool
53
+ if !obj.is_a?(TrueClass) && !obj.is_a?(FalseClass)
54
+ add_error(path, :clazz_type, :bool)
55
+ end
56
+ elsif clazz_type == :array_of_string
57
+ if !obj.is_a?(Array) || obj.reject { |v| v.class == String }.size > 0
58
+ add_error(path, :clazz_type, :array_of_string)
59
+ end
60
+ else
61
+ if !obj.is_a?(clazz_type)
62
+ add_error(path, :clazz_type, clazz_type)
63
+ end
64
+ end
65
+
66
+ if !only.nil?
67
+ if obj.is_a?(Array)
68
+ add_error(path, :only, only) if (obj - only).size > 0
69
+ else
70
+ add_error(path, :only, only) if !only.include?(obj)
71
+ end
72
+ end
73
+
74
+ if !max.nil?
75
+ if obj.is_a?(Numeric)
76
+ add_error(path, :max, max) if obj > max
77
+ else
78
+ add_error(path, :max_size, max) if obj.size > max
79
+ end
80
+ end
81
+
82
+ if !min.nil?
83
+ if obj.is_a?(Numeric)
84
+ add_error(path, :min, min) if obj < min
85
+ else
86
+ add_error(path, :min_size, min) if obj.size < min
87
+ end
88
+ end
89
+ end
90
+
91
+ def blank?(v)
92
+ v.nil? || (countable?(v) ? v.size.zero? : false)
93
+ end
94
+
95
+ def present?(v)
96
+ v.respond_to?(:size) ? !v.size.zero? : !v.nil?
97
+ end
98
+
99
+ def countable?(v)
100
+ v.is_a?(Array) || v.is_a?(Hash) || v.is_a?(String)
101
+ end
102
+
103
+ def validate_for_types(type, path, types)
104
+ if type.nil?
105
+ add_error(path, :required)
106
+ return false
107
+ end
108
+
109
+ return true if types.nil?
110
+
111
+ if !BLOCK_KIT_GROUPS[types].include?(type.to_s)
112
+ add_error(path, :invalid_type)
113
+ return false
114
+ end
115
+ true
116
+ end
117
+
118
+ def validate_for_properties(obj, path, properties)
119
+ other_properties = obj.keys - properties
120
+ if present?(other_properties)
121
+ add_error(path, :invalid_properties, other_properties)
122
+ false
123
+ else
124
+ true
125
+ end
126
+ end
127
+
128
+ def validate_for_block_id(block_id, path)
129
+ validate_for(block_id, path, String, false, max: 255)
130
+ unless block_id.nil?
131
+ path_by_block_id[block_id] ||= []
132
+ path_by_block_id[block_id] << path
133
+ end
134
+ end
135
+
136
+ def validate_for_action_id(action_id, path)
137
+ validate_for(action_id, path, String, true, max: 255)
138
+ unless action_id.nil?
139
+ path_by_action_id[action_id] ||= []
140
+ path_by_action_id[action_id] << path
141
+ end
142
+ end
143
+
144
+ def validate_for_plain_text(obj, path, required, size)
145
+ validate_for(obj, path, Hash, required)
146
+ validate(obj, path, :plain_text, { text_size: size })
147
+ end
148
+
149
+ def validate_for_text_objects(obj, path, required, size)
150
+ validate_for(obj, path, Hash, required)
151
+ validate(obj, path, :text_objects, { text_size: size }) if present?(obj)
152
+ end
153
+
154
+ def validate_for_focus_on_load(obj, path)
155
+ validate_for(obj, path, :bool, false)
156
+ focus_on_load_by_path[path] = true if obj
157
+ end
158
+
159
+ private def add_error(path, kind, option = nil)
160
+ errors_by_path[path] ||= []
161
+ errors_by_path[path] << ::SlackValidBlockKit::Validator::Error.new(kind, option)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,82 @@
1
+ module SlackValidBlockKit::Validator
2
+ module Composition
3
+ PLAIN_TEXT_PROPERTIES = %i(type text emoji)
4
+ MRKDWN_PROPERTIES = %i(type text verbatim)
5
+ CONFIRMATION_PROPERTIES = %i(title text confirm deny style)
6
+ OPTION_PROPERTIES = %i(text value description)
7
+ OPTION_OF_OVERFLOW_PROPERTIES = %i(text value description url)
8
+ OPTION_GROUP_PROPERTIES = %i(label options)
9
+ FILTER_PROPERTIES = %i(include exclude_external_shared_channels exclude_bot_users)
10
+ DISPATCH_ACTION_CONFIGURATION_PROPERTIES = %i(trigger_actions_on)
11
+
12
+ def validate_plain_text(hash, path, options = nil)
13
+ validate_for_properties(hash, path, PLAIN_TEXT_PROPERTIES)
14
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["plain_text"])
15
+ max = options && options[:text_size]
16
+ validate_for(hash[:text], "#{path}.text", String, true, max: max)
17
+ validate_for(hash[:emoji], "#{path}.emoji", :bool, false)
18
+ end
19
+
20
+ def validate_mrkdwn(hash, path, options = nil)
21
+ validate_for_properties(hash, path, MRKDWN_PROPERTIES)
22
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["mrkdwn"])
23
+ max = options && options[:text_size]
24
+ validate_for(hash[:text], "#{path}.text", String, true, max: max)
25
+ validate_for(hash[:verbatim], "#{path}.verbatim", :bool, false)
26
+ end
27
+
28
+ def validate_confirmation(hash, path, options = nil)
29
+ validate_for_properties(hash, path, CONFIRMATION_PROPERTIES)
30
+
31
+ validate_for_plain_text(hash[:title], "#{path}.title", true, 100)
32
+ validate_for_text_objects(hash[:text], "#{path}.text", true, 300)
33
+ validate_for_plain_text(hash[:confirm], "#{path}.confirm", true, 30)
34
+ validate_for_plain_text(hash[:deny], "#{path}.deny", true, 30)
35
+
36
+ validate_for(hash[:style], "#{path}.style", String, false, only: %w(danger primary))
37
+ end
38
+
39
+ def validate_option(hash, path, options)
40
+ if options[:actoin_type] == "overflow"
41
+ validate_for_properties(hash, path, OPTION_OF_OVERFLOW_PROPERTIES)
42
+ else
43
+ validate_for_properties(hash, path, OPTION_PROPERTIES)
44
+ end
45
+
46
+ if options[:text_type] == :text_objects
47
+ validate_for_text_objects(hash[:text], "#{path}.text", true, 75)
48
+ else
49
+ validate_for_plain_text(hash[:text], "#{path}.text", true, 75)
50
+ end
51
+ validate_for(hash[:value], "#{path}.value", String, true, max: 75)
52
+ validate_for_plain_text(hash[:description], "#{path}.description", false, 75)
53
+ validate_for_plain_text(hash[:url], "#{path}.url", false, 3000)
54
+ end
55
+
56
+ def validate_option_group(hash, path, options = nil)
57
+ validate_for_properties(hash, path, OPTION_GROUP_PROPERTIES)
58
+
59
+ validate_for_plain_text(hash[:label], "#{path}.label", true, 75)
60
+ validate_for(hash[:options], "#{path}.options", Array, true, max: 100)
61
+ Array(hash[:options]).each_with_index do |v, i|
62
+ validate_option(v, "#{path}.options[#{i}]")
63
+ end
64
+ end
65
+
66
+ def validate_dispatch_action_config(hash, path, options = nil)
67
+ validate_for_properties(hash, path, DISPATCH_ACTION_CONFIGURATION_PROPERTIES)
68
+ validate_for(hash[:trigger_actions_on], "#{path}.trigger_actions_on", :array_of_string, false, only: %w(on_enter_pressed on_character_entered))
69
+ end
70
+
71
+ def validate_filter(hash, path, options = nil)
72
+ validate_for_properties(hash, path, FILTER_PROPERTIES)
73
+ validate_for(hash[:include], "#{path}.include", Array, false, only: %w(im mpim private public))
74
+ if hash[:include].is_a?(Array) && hash[:include].size == 0
75
+ add_error("#{path}.include", :empty)
76
+ end
77
+
78
+ validate_for(hash[:exclude_external_shared_channels], "#{path}.exclude_external_shared_channels", :bool, false)
79
+ validate_for(hash[:exclude_bot_users], "#{path}.exclude_bot_users", :bool, false)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,128 @@
1
+ module SlackValidBlockKit::Validator
2
+ module Element
3
+ BUTTON_PROPERTIES = %i(type text action_id url value style confirm accessibility_label)
4
+ CHECKBOXES_PROPERTIES = %i(type action_id options initial_options confirm focus_on_load)
5
+ DATEPICKER_PROPERTIES = %i(type action_id placeholder initial_date confirm focus_on_load)
6
+ IMAGE_PROPERTIES = %i(type image_url alt_text)
7
+
8
+ OVERFLOW_PROPERTIES = %i(type action_id options confirm)
9
+ PLAIN_TEXT_INPUT_PROPERTIES = %i(type action_id placeholder initial_value multiline min_length max_length dispatch_action_config focus_on_load)
10
+ RADIO_BUTTONS_PROPERTIES = %i(type action_id options initial_option confirm focus_on_load)
11
+ TIMEPICKER_PROPERTIES = %i(type action_id placeholder initial_time confirm focus_on_load)
12
+
13
+ def validate_button(hash, path, options = nil)
14
+ validate_for_properties(hash, path, BUTTON_PROPERTIES)
15
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["button"])
16
+ validate_for_plain_text(hash[:text], "#{path}.text", true, 75)
17
+ validate_for_action_id(hash[:action_id], "#{path}.action_id")
18
+ validate_for(hash[:url], "#{path}.url", String, false, max: 3000)
19
+ validate_for(hash[:value], "#{path}.value", String, false, max: 2000)
20
+ validate_for(hash[:style], "#{path}.style", String, false, only: %w(primary danger))
21
+ validate_for(hash[:confirm], "#{path}.confirm", Hash, false)
22
+ validate_confirmation(hash[:confirm], "#{path}.confirm") unless hash[:confirm].nil?
23
+ validate_for(hash[:accessibility_label], "#{path}.accessibility_label", String, false, max: 75)
24
+ end
25
+
26
+ def validate_checkboxes(hash, path, options = nil)
27
+ validate_for_properties(hash, path, CHECKBOXES_PROPERTIES)
28
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["checkboxes"])
29
+
30
+ validate_for(hash[:options], "#{path}.options", Array, true, max: 10)
31
+ options = Array(hash[:options])
32
+ options.each_with_index do |e, i|
33
+ validate_option(e, "#{path}.options[#{i}]", { text_type: :text_objects })
34
+ end
35
+ validate_for(hash[:initial_options], "#{path}.initial_options", Array, false, max: options.size, only: options)
36
+
37
+ validate_common_for_input(hash, path, options)
38
+ end
39
+
40
+ def validate_datepicker(hash, path, options = nil)
41
+ validate_for_properties(hash, path, DATEPICKER_PROPERTIES)
42
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["datepicker"])
43
+
44
+ validate_for_plain_text(hash[:placeholder], "#{path}.placeholder", false, 150)
45
+ validate_for(hash[:initial_date], "#{path}.initial_date", String, false)
46
+
47
+ if hash[:initial_date].is_a?(String) && !hash[:initial_date] =~ /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1])$/
48
+ add_error("#{path}.initial_date", :invalid_format)
49
+ end
50
+
51
+ validate_common_for_input(hash, path, options)
52
+ end
53
+
54
+ def validate_image(hash, path, options = nil)
55
+ validate_for_properties(hash, path, IMAGE_PROPERTIES)
56
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["image"])
57
+
58
+ validate_for(hash[:image_url], "#{path}.image_url", String, true)
59
+ validate_for(hash[:alt_text], "#{path}.alt_text", String, true)
60
+ end
61
+
62
+ def validate_overflow(hash, path, options = nil)
63
+ validate_for_properties(hash, path, OVERFLOW_PROPERTIES)
64
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["overflow"])
65
+ validate_for_action_id(hash[:action_id], "#{path}.action_id")
66
+
67
+ validate_for(hash[:options], "#{path}.options", Array, true, max: 5, min: 2)
68
+ options = Array(hash[:options])
69
+ options.each_with_index do |e, i|
70
+ validate_option(e, "#{path}.options[#{i}]", { text_type: :text_objects })
71
+ end
72
+
73
+ validate_for(hash[:confirm], "#{path}.confirm", Hash, false)
74
+ validate_confirmation(hash[:confirm], "#{path}.confirm") unless hash[:confirm].nil?
75
+ end
76
+
77
+ def validate_plain_text_input(hash, path, options = nil)
78
+ validate_for_properties(hash, path, PLAIN_TEXT_INPUT_PROPERTIES)
79
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["plain_text_input"])
80
+ validate_for_action_id(hash[:action_id], "#{path}.action_id")
81
+ validate_for_plain_text(hash[:placeholder], "#{path}.placeholder", false, 150)
82
+ validate_for(hash[:initial_value], "#{path}.initial_value", String, false)
83
+ validate_for(hash[:multiline], "#{path}.multiline", :bool, false)
84
+ validate_for(hash[:min_length], "#{path}.min_length", Integer, false, max: 3000)
85
+ validate_for(hash[:max_length], "#{path}.max_length", Integer, false)
86
+
87
+ validate_for(hash[:dispatch_action_config], "#{path}.dispatch_action_config", Hash, false)
88
+ validate_dispatch_action_config(hash[:dispatch_action_config], "#{path}.dispatch_action_config") unless hash[:dispatch_action_config].nil?
89
+
90
+ validate_for_focus_on_load(hash[:focus_on_load], "#{path}.focus_on_load")
91
+ end
92
+
93
+ def validate_radio_buttons(hash, path, options = nil)
94
+ validate_for_properties(hash, path, RADIO_BUTTONS_PROPERTIES)
95
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["radio_buttons"])
96
+
97
+ validate_for(hash[:options], "#{path}.options", Array, true, max: 10)
98
+ options = Array(hash[:options])
99
+ options.each_with_index do |e, i|
100
+ validate_option(e, "#{path}.options[#{i}]", { text_type: :text_objects })
101
+ end
102
+ validate_for(hash[:initial_options], "#{path}.initial_options", Array, false, max: options.size, only: options)
103
+
104
+ validate_common_for_input(hash, path, options)
105
+ end
106
+
107
+ def validate_timepicker(hash, path, options = nil)
108
+ validate_for_properties(hash, path, TIMEPICKER_PROPERTIES)
109
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["timepicker"])
110
+
111
+ validate_for_plain_text(hash[:placeholder], "#{path}.placeholder", false, 150)
112
+ validate_for(hash[:initial_time], "#{path}.initial_time", String, false)
113
+ if hash[:initial_time].is_a?(String) && !hash[:initial_time] =~ /^(0\d|1[0-2])-[0-5]\d$/
114
+ add_error("#{path}.initial_time", :invalid_format)
115
+ end
116
+
117
+ validate_common_for_input(hash, path, options)
118
+ end
119
+
120
+ private def validate_common_for_input(hash, path, options = nil)
121
+ validate_for_action_id(hash[:action_id], "#{path}.action_id")
122
+
123
+ validate_for(hash[:confirm], "#{path}.confirm", Hash, false)
124
+ validate_confirmation(hash[:confirm], "#{path}.confirm") unless hash[:confirm].nil?
125
+ validate_for_focus_on_load(hash[:focus_on_load], "#{path}.focus_on_load")
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,15 @@
1
+ module SlackValidBlockKit::Validator
2
+ class Error
3
+ attr_reader :kind, :option
4
+ def initialize(kind, option = nil)
5
+ @kind = kind
6
+ @option = option
7
+ end
8
+
9
+ def self.messages(errors_by_path)
10
+ errors_by_path.map { |path, errors|
11
+ errors.map { |e| "#{path} : #{e.kind}" }
12
+ }.flatten
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,92 @@
1
+ module SlackValidBlockKit::Validator
2
+ module Layout
3
+ ACTION_PROPERTIES = %i(type elements block_id)
4
+ CONTEXT_PROPERTIES = %i(type elements block_id)
5
+ DIVIDER_PROPERTIES = %i(type block_id)
6
+ FILE_PROPERTIES = %i(type external_id source block_id)
7
+ HEADER_PROPERTIES = %i(type text block_id)
8
+ IMAGE_PROPERTIES = %i(type image_url alt_text title block_id)
9
+ INPUT_PROPERTIES = %i(type label element dispatch_action block_id hint optional)
10
+ SECTION_PROPERTIES = %i(type text block_id fields accessory)
11
+
12
+ def validate_actions(hash, path, options = nil)
13
+ validate_for_properties(hash, path, ACTION_PROPERTIES)
14
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["actions"])
15
+ validate_for(hash[:elements], "#{path}.elements", Array, true, max: 25)
16
+ Array(hash[:elements]).each_with_index do |e, i|
17
+ validate(e, "#{path}.elements[#{i}]", :action_elements)
18
+ end
19
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
20
+ end
21
+
22
+ def validate_context(hash, path, options = nil)
23
+ validate_for_properties(hash, path, CONTEXT_PROPERTIES)
24
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["context"])
25
+ validate_for(hash[:elements], "#{path}.elements", Array, true, max: 10)
26
+ Array(hash[:elements]).each_with_index do |e, i|
27
+ validate(e, "#{path}.elements[#{i}]", :image_or_text)
28
+ end
29
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
30
+ end
31
+
32
+ def validate_divider(hash, path, options = nil)
33
+ validate_for_properties(hash, path, DIVIDER_PROPERTIES)
34
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["divider"])
35
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
36
+ end
37
+
38
+ def validate_file(hash, path, options = nil)
39
+ validate_for_properties(hash, path, FILE_PROPERTIES)
40
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["file"])
41
+ validate_for(hash[:external_id], "#{path}.external_id", String, true)
42
+ validate_for(hash[:source], "#{path}.source", String, true, only: ["remote"])
43
+
44
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
45
+ end
46
+
47
+ def validate_header(hash, path, options = nil)
48
+ validate_for_properties(hash, path, HEADER_PROPERTIES)
49
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["header"])
50
+ validate_for_plain_text(hash[:text], "#{path}.text", true, 150)
51
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
52
+ end
53
+
54
+ def validate_image(hash, path, options = nil)
55
+ validate_for_properties(hash, path, IMAGE_PROPERTIES)
56
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["image"])
57
+ validate_for(hash[:image_url], "#{path}.image_url", String, true, max: 3000)
58
+ validate_for(hash[:alt_text], "#{path}.alt_text", String, true, max: 2000)
59
+ validate_for_plain_text(hash[:title], "#{path}.title", false, 2000)
60
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
61
+ end
62
+
63
+ def validate_input(hash, path, options = nil)
64
+ validate_for_properties(hash, path, INPUT_PROPERTIES)
65
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["input"])
66
+ validate_for_plain_text(hash[:label], "#{path}.label", true, 2000)
67
+ validate_for(hash[:element], "#{path}.element", Hash, true)
68
+ validate(hash[:element], "#{path}.element", :input_elements)
69
+ validate_for(hash[:dispatch_action], "#{path}.dispatch_action", :bool, false)
70
+ validate_for_plain_text(hash[:hint], "#{path}.hint", false, 2000)
71
+ validate_for(hash[:optional], "#{path}.optional", :bool, false)
72
+
73
+ validate_for_block_id(hash[:block_id], "#{path}.block_id")
74
+ end
75
+
76
+ def validate_section(hash, path, options = nil)
77
+ validate_for_properties(hash, path, SECTION_PROPERTIES)
78
+ validate_for(hash[:type], "#{path}.type", String, true, only: ["section"])
79
+ validate_for_text_objects(hash[:text], "#{path}.text", false, 3000)
80
+ validate_for(hash[:fields], "#{path}.fields", Array, false, max: 10)
81
+ Array(hash[:fields]).each_with_index do |e, i|
82
+ validate_for_text_objects(e, "#{path}.fields[#{i}]", true, 2000)
83
+ end
84
+
85
+ if hash[:text].nil? && hash[:fields].nil?
86
+ add_error(path, :require_text_or_fields)
87
+ end
88
+ validate_for(hash[:accessory], "#{path}.accessory", Hash, false)
89
+ validate(hash[:accessory], "#{path}.accessory", :all_element_types)
90
+ end
91
+ end
92
+ end