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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +171 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/slack_valid_block_kit/builder/compositions.rb +59 -0
- data/lib/slack_valid_block_kit/builder/elements.rb +202 -0
- data/lib/slack_valid_block_kit/builder/layouts.rb +69 -0
- data/lib/slack_valid_block_kit/builder/method_generator.rb +86 -0
- data/lib/slack_valid_block_kit/builder/surfaces.rb +35 -0
- data/lib/slack_valid_block_kit/builder.rb +15 -0
- data/lib/slack_valid_block_kit/config.rb +13 -0
- data/lib/slack_valid_block_kit/validator/base.rb +164 -0
- data/lib/slack_valid_block_kit/validator/composition.rb +82 -0
- data/lib/slack_valid_block_kit/validator/element.rb +128 -0
- data/lib/slack_valid_block_kit/validator/error.rb +15 -0
- data/lib/slack_valid_block_kit/validator/layout.rb +92 -0
- data/lib/slack_valid_block_kit/validator/multi_select_menu.rb +104 -0
- data/lib/slack_valid_block_kit/validator/select_menu.rb +100 -0
- data/lib/slack_valid_block_kit/validator/surface.rb +52 -0
- data/lib/slack_valid_block_kit/validator/uniq.rb +23 -0
- data/lib/slack_valid_block_kit/validator.rb +32 -0
- data/lib/slack_valid_block_kit/version.rb +3 -0
- data/lib/slack_valid_block_kit.rb +73 -0
- data/slack_valid_block_kit.gemspec +29 -0
- metadata +114 -0
@@ -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,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
|