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.
- checksums.yaml +7 -0
- data/.rspec +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +8 -0
- data/lib/block_kit/base.rb +190 -0
- data/lib/block_kit/blocks.rb +50 -0
- data/lib/block_kit/composition/confirmation_dialog.rb +48 -0
- data/lib/block_kit/composition/conversation_filter.rb +42 -0
- data/lib/block_kit/composition/dispatch_action_config.rb +33 -0
- data/lib/block_kit/composition/input_parameter.rb +19 -0
- data/lib/block_kit/composition/mrkdwn.rb +15 -0
- data/lib/block_kit/composition/option.rb +41 -0
- data/lib/block_kit/composition/option_group.rb +27 -0
- data/lib/block_kit/composition/overflow_option.rb +21 -0
- data/lib/block_kit/composition/plain_text.rb +15 -0
- data/lib/block_kit/composition/slack_file.rb +36 -0
- data/lib/block_kit/composition/text.rb +29 -0
- data/lib/block_kit/composition/trigger.rb +33 -0
- data/lib/block_kit/composition/workflow.rb +19 -0
- data/lib/block_kit/composition.rb +19 -0
- data/lib/block_kit/concerns/confirmable.rb +21 -0
- data/lib/block_kit/concerns/conversation_selection.rb +26 -0
- data/lib/block_kit/concerns/dispatchable.rb +24 -0
- data/lib/block_kit/concerns/dsl_generation.rb +76 -0
- data/lib/block_kit/concerns/external.rb +19 -0
- data/lib/block_kit/concerns/focusable_on_load.rb +17 -0
- data/lib/block_kit/concerns/has_initial_option.rb +23 -0
- data/lib/block_kit/concerns/has_initial_options.rb +23 -0
- data/lib/block_kit/concerns/has_option_groups.rb +37 -0
- data/lib/block_kit/concerns/has_options.rb +28 -0
- data/lib/block_kit/concerns/has_placeholder.rb +21 -0
- data/lib/block_kit/concerns/has_rich_text_elements.rb +83 -0
- data/lib/block_kit/concerns/plain_text_emoji_assignment.rb +39 -0
- data/lib/block_kit/concerns.rb +18 -0
- data/lib/block_kit/elements/base.rb +23 -0
- data/lib/block_kit/elements/base_button.rb +45 -0
- data/lib/block_kit/elements/button.rb +27 -0
- data/lib/block_kit/elements/channels_select.rb +22 -0
- data/lib/block_kit/elements/checkboxes.rb +14 -0
- data/lib/block_kit/elements/conversations_select.rb +24 -0
- data/lib/block_kit/elements/date_picker.rb +21 -0
- data/lib/block_kit/elements/datetime_picker.rb +21 -0
- data/lib/block_kit/elements/email_text_input.rb +24 -0
- data/lib/block_kit/elements/external_select.rb +21 -0
- data/lib/block_kit/elements/file_input.rb +33 -0
- data/lib/block_kit/elements/image.rb +53 -0
- data/lib/block_kit/elements/multi_channels_select.rb +31 -0
- data/lib/block_kit/elements/multi_conversations_select.rb +33 -0
- data/lib/block_kit/elements/multi_external_select.rb +21 -0
- data/lib/block_kit/elements/multi_select.rb +21 -0
- data/lib/block_kit/elements/multi_static_select.rb +12 -0
- data/lib/block_kit/elements/multi_users_select.rb +31 -0
- data/lib/block_kit/elements/number_input.rb +52 -0
- data/lib/block_kit/elements/overflow.rb +23 -0
- data/lib/block_kit/elements/plain_text_input.rb +66 -0
- data/lib/block_kit/elements/radio_buttons.rb +14 -0
- data/lib/block_kit/elements/rich_text_input.rb +26 -0
- data/lib/block_kit/elements/select.rb +18 -0
- data/lib/block_kit/elements/static_select.rb +12 -0
- data/lib/block_kit/elements/time_picker.rb +43 -0
- data/lib/block_kit/elements/url_text_input.rb +24 -0
- data/lib/block_kit/elements/users_select.rb +17 -0
- data/lib/block_kit/elements/workflow_button.rb +18 -0
- data/lib/block_kit/elements.rb +36 -0
- data/lib/block_kit/fixers/associated.rb +27 -0
- data/lib/block_kit/fixers/base.rb +30 -0
- data/lib/block_kit/fixers/null_value.rb +42 -0
- data/lib/block_kit/fixers/truncate.rb +35 -0
- data/lib/block_kit/fixers.rb +11 -0
- data/lib/block_kit/layout/actions.rb +84 -0
- data/lib/block_kit/layout/base.rb +23 -0
- data/lib/block_kit/layout/context.rb +48 -0
- data/lib/block_kit/layout/divider.rb +9 -0
- data/lib/block_kit/layout/file.rb +19 -0
- data/lib/block_kit/layout/header.rb +22 -0
- data/lib/block_kit/layout/image.rb +61 -0
- data/lib/block_kit/layout/input.rb +119 -0
- data/lib/block_kit/layout/markdown.rb +19 -0
- data/lib/block_kit/layout/rich_text/elements/broadcast.rb +22 -0
- data/lib/block_kit/layout/rich_text/elements/channel.rb +21 -0
- data/lib/block_kit/layout/rich_text/elements/color.rb +20 -0
- data/lib/block_kit/layout/rich_text/elements/date.rb +34 -0
- data/lib/block_kit/layout/rich_text/elements/emoji.rb +27 -0
- data/lib/block_kit/layout/rich_text/elements/link.rb +28 -0
- data/lib/block_kit/layout/rich_text/elements/mention_style.rb +27 -0
- data/lib/block_kit/layout/rich_text/elements/text.rb +23 -0
- data/lib/block_kit/layout/rich_text/elements/text_style.rb +23 -0
- data/lib/block_kit/layout/rich_text/elements/user.rb +21 -0
- data/lib/block_kit/layout/rich_text/elements/usergroup.rb +21 -0
- data/lib/block_kit/layout/rich_text/elements.rb +35 -0
- data/lib/block_kit/layout/rich_text/list.rb +41 -0
- data/lib/block_kit/layout/rich_text/preformatted.rb +19 -0
- data/lib/block_kit/layout/rich_text/quote.rb +19 -0
- data/lib/block_kit/layout/rich_text/section.rb +11 -0
- data/lib/block_kit/layout/rich_text.rb +53 -0
- data/lib/block_kit/layout/section.rb +152 -0
- data/lib/block_kit/layout/video.rb +71 -0
- data/lib/block_kit/layout.rb +35 -0
- data/lib/block_kit/surfaces/base.rb +173 -0
- data/lib/block_kit/surfaces/home.rb +40 -0
- data/lib/block_kit/surfaces/message.rb +140 -0
- data/lib/block_kit/surfaces/modal.rb +74 -0
- data/lib/block_kit/surfaces.rb +11 -0
- data/lib/block_kit/typed_array.rb +114 -0
- data/lib/block_kit/typed_set.rb +78 -0
- data/lib/block_kit/types/array.rb +45 -0
- data/lib/block_kit/types/blocks.rb +37 -0
- data/lib/block_kit/types/generic.rb +45 -0
- data/lib/block_kit/types/option.rb +48 -0
- data/lib/block_kit/types/set.rb +35 -0
- data/lib/block_kit/types/text.rb +80 -0
- data/lib/block_kit/types.rb +17 -0
- data/lib/block_kit/validators/array_inclusion_validator.rb +55 -0
- data/lib/block_kit/validators/associated_validator.rb +61 -0
- data/lib/block_kit/validators.rb +8 -0
- data/lib/block_kit/version.rb +5 -0
- data/lib/block_kit.rb +38 -0
- 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
|