diff --git a/lib/slack/block_kit.rb b/lib/slack/block_kit.rb index 2dbb5b3..69c2a14 100644 --- a/lib/slack/block_kit.rb +++ b/lib/slack/block_kit.rb @@ -10,6 +10,17 @@ module Layout; end module_function + def configuration + @configuration ||= Configuration.new + + yield(@configuration) if block_given? + + @configuration + end + class << self + alias config configuration + end + def blocks blocks = Blocks.new diff --git a/lib/slack/block_kit/configuration.rb b/lib/slack/block_kit/configuration.rb new file mode 100644 index 0000000..10996cb --- /dev/null +++ b/lib/slack/block_kit/configuration.rb @@ -0,0 +1,15 @@ +module Slack + module BlockKit + class Configuration + attr_accessor :autofix_invalid_blocks + + def initialize + @autofix_invalid_blocks = false + end + + def autofix_invalid_blocks? + !!@autofix_invalid_blocks + end + end + end +end diff --git a/lib/slack/block_kit/layout/actions.rb b/lib/slack/block_kit/layout/actions.rb index 37fe192..a30b5eb 100644 --- a/lib/slack/block_kit/layout/actions.rb +++ b/lib/slack/block_kit/layout/actions.rb @@ -7,6 +7,7 @@ module Layout # # https://api.slack.com/reference/messaging/blocks#actions class Actions + prepend Limiters::BlockId TYPE = 'actions' attr_accessor :elements diff --git a/lib/slack/block_kit/layout/context.rb b/lib/slack/block_kit/layout/context.rb index 1973ff3..607f367 100644 --- a/lib/slack/block_kit/layout/context.rb +++ b/lib/slack/block_kit/layout/context.rb @@ -7,6 +7,7 @@ module Layout # # https://api.slack.com/reference/messaging/blocks#context class Context + prepend Limiters::BlockId TYPE = 'context' attr_accessor :elements diff --git a/lib/slack/block_kit/layout/divider.rb b/lib/slack/block_kit/layout/divider.rb index 2850590..b606f61 100644 --- a/lib/slack/block_kit/layout/divider.rb +++ b/lib/slack/block_kit/layout/divider.rb @@ -8,6 +8,7 @@ module Layout # # https://api.slack.com/reference/messaging/blocks#divider class Divider + prepend Limiters::BlockId TYPE = 'divider' def initialize(block_id: nil) diff --git a/lib/slack/block_kit/layout/header.rb b/lib/slack/block_kit/layout/header.rb index 2ecd8ce..4f65769 100644 --- a/lib/slack/block_kit/layout/header.rb +++ b/lib/slack/block_kit/layout/header.rb @@ -9,6 +9,7 @@ module Layout # # https://api.slack.com/reference/block-kit/blocks#header class Header + prepend Limiters::BlockId TYPE = 'header' def initialize(text:, block_id: nil, emoji: nil) diff --git a/lib/slack/block_kit/layout/image.rb b/lib/slack/block_kit/layout/image.rb index 5474290..fcc5171 100644 --- a/lib/slack/block_kit/layout/image.rb +++ b/lib/slack/block_kit/layout/image.rb @@ -7,6 +7,7 @@ module Layout # # https://api.slack.com/reference/messaging/blocks#context class Image + prepend Limiters::BlockId TYPE = 'image' def initialize(url:, alt_text:, title: nil, block_id: nil, emoji: nil) diff --git a/lib/slack/block_kit/layout/input.rb b/lib/slack/block_kit/layout/input.rb index 469bc51..c0aa146 100644 --- a/lib/slack/block_kit/layout/input.rb +++ b/lib/slack/block_kit/layout/input.rb @@ -9,6 +9,7 @@ module Layout # # https://api.slack.com/reference/block-kit/blocks#input class Input # rubocop:disable Metrics/ClassLength + prepend Limiters::BlockId TYPE = 'input' attr_accessor :label, :element, :block_id, :hint, :optional, :emoji diff --git a/lib/slack/block_kit/layout/rich_text.rb b/lib/slack/block_kit/layout/rich_text.rb index e8fb40f..0be1829 100644 --- a/lib/slack/block_kit/layout/rich_text.rb +++ b/lib/slack/block_kit/layout/rich_text.rb @@ -11,6 +11,7 @@ module Layout # # https://api.slack.com/reference/block-kit/blocks#rich_text class RichText + prepend Limiters::BlockId TYPE = 'rich_text' attr_accessor :elements diff --git a/lib/slack/block_kit/layout/section.rb b/lib/slack/block_kit/layout/section.rb index 48f00ca..38e1cdf 100644 --- a/lib/slack/block_kit/layout/section.rb +++ b/lib/slack/block_kit/layout/section.rb @@ -9,6 +9,7 @@ module Layout # # https://api.slack.com/reference/messaging/blocks#section class Section + prepend Limiters::BlockId include Section::MultiSelectElements TYPE = 'section' diff --git a/lib/slack/block_kit/layout/video.rb b/lib/slack/block_kit/layout/video.rb index ae10a86..9ecb118 100644 --- a/lib/slack/block_kit/layout/video.rb +++ b/lib/slack/block_kit/layout/video.rb @@ -9,6 +9,7 @@ module Layout # # https://api.slack.com/reference/messaging/blocks#context class Video + prepend Limiters::BlockId TYPE = 'video' def initialize(alt_text:, thumbnail_url:, video_url:, title:, description:, **optional_args) diff --git a/lib/slack/block_kit/limiters/block_id.rb b/lib/slack/block_kit/limiters/block_id.rb new file mode 100644 index 0000000..ce5fa55 --- /dev/null +++ b/lib/slack/block_kit/limiters/block_id.rb @@ -0,0 +1,19 @@ +module Slack + module BlockKit + module Limiters + module BlockId + BLOCK_ID_LIMIT = 255 + + def as_json + json = super + + if Slack::BlockKit.config.autofix_invalid_blocks? && json[:block_id] + json[:block_id] = Utils.truncate(json[:block_id].to_s, length: BLOCK_ID_LIMIT, omission: "") + end + + json + end + end + end + end +end diff --git a/lib/slack/block_kit/utils.rb b/lib/slack/block_kit/utils.rb new file mode 100644 index 0000000..9c567b3 --- /dev/null +++ b/lib/slack/block_kit/utils.rb @@ -0,0 +1,23 @@ +module Slack + module BlockKit + module Utils + # Truncate a string to a certain length, adding an optional omission + # string if the string is truncated. This is generally used to ensure + # that certain block limits do not exceed their maximum length when + # autofixing is enabled. + # + # See: https://api.rubyonrails.org/classes/String.html#method-i-truncate + def self.truncate(text, length:, omission: "...", separator: nil) + return text.dup unless text.length > length + + omission ||= "" + length_with_room_for_omission = length - omission.length + + stop = text.rindex(separator, length_with_room_for_omission) if separator + stop ||= length_with_room_for_omission + + "#{text[0, stop]}#{omission}" + end + end + end +end diff --git a/spec/lib/slack/block_kit/layout/actions_spec.rb b/spec/lib/slack/block_kit/layout/actions_spec.rb index dfecbea..f85eb93 100644 --- a/spec/lib/slack/block_kit/layout/actions_spec.rb +++ b/spec/lib/slack/block_kit/layout/actions_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::Actions do subject(:actions_json) { instance.as_json } @@ -20,6 +21,8 @@ expect(actions_json).to eq(expected_json) end + it_behaves_like 'a block that handles block_id length limits' + describe '#button' do let(:expected_element_json) do { diff --git a/spec/lib/slack/block_kit/layout/context_spec.rb b/spec/lib/slack/block_kit/layout/context_spec.rb index 95e1a63..02fa4e4 100644 --- a/spec/lib/slack/block_kit/layout/context_spec.rb +++ b/spec/lib/slack/block_kit/layout/context_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::Context do subject(:context_json) { instance.as_json } @@ -19,6 +20,8 @@ expect(context_json).to eq(expected_json) end + it_behaves_like 'a block that handles block_id length limits' + describe '#image' do let(:expected_json) do { diff --git a/spec/lib/slack/block_kit/layout/divider_spec.rb b/spec/lib/slack/block_kit/layout/divider_spec.rb new file mode 100644 index 0000000..02879fb --- /dev/null +++ b/spec/lib/slack/block_kit/layout/divider_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../limiters/block_id_helpers' + +RSpec.describe Slack::BlockKit::Layout::Divider do + let(:instance) { described_class.new(**params) } + + it_behaves_like 'a block that handles block_id length limits' + + describe '#as_json' do + subject { instance.as_json } + + let(:params) { {} } + let(:expected_json) { { type: 'divider' } } + + it { is_expected.to eq expected_json } + + context 'with block_id' do + let(:params) { { block_id: '1123' } } + let(:expected_json) do + { + type: 'divider', + block_id: '1123' + } + end + + it { is_expected.to eq expected_json } + end + end +end diff --git a/spec/lib/slack/block_kit/layout/header_spec.rb b/spec/lib/slack/block_kit/layout/header_spec.rb index 757be10..438b969 100644 --- a/spec/lib/slack/block_kit/layout/header_spec.rb +++ b/spec/lib/slack/block_kit/layout/header_spec.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::Header do let(:instance) { described_class.new(**params) } + it_behaves_like 'a block that handles block_id length limits', text: '__TEXT__' + describe '#as_json' do subject { instance.as_json } diff --git a/spec/lib/slack/block_kit/layout/image_spec.rb b/spec/lib/slack/block_kit/layout/image_spec.rb new file mode 100644 index 0000000..49796bb --- /dev/null +++ b/spec/lib/slack/block_kit/layout/image_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../limiters/block_id_helpers' + +RSpec.describe Slack::BlockKit::Layout::Image do + let(:instance) { described_class.new(**params) } + + it_behaves_like 'a block that handles block_id length limits', url: '__URL__', alt_text: '__ALT_TEXT__' + + describe '#as_json' do + subject { instance.as_json } + + let(:params) do + { + url: '__URL__', + alt_text: '__ALT_TEXT__' + } + end + let(:expected_json) do + { + type: 'image', + image_url: '__URL__', + alt_text: '__ALT_TEXT__' + } + end + + it { is_expected.to eq expected_json } + + context 'with all arguments' do + let(:params) do + { + url: '__URL__', + alt_text: '__ALT_TEXT__', + block_id: '1123', + title: 'This is a title', + } + end + let(:expected_json) do + { + type: 'image', + image_url: '__URL__', + alt_text: '__ALT_TEXT__', + block_id: '1123', + title: { + type: 'plain_text', + text: 'This is a title' + } + } + end + + it { is_expected.to eq expected_json } + end + end +end diff --git a/spec/lib/slack/block_kit/layout/input_spec.rb b/spec/lib/slack/block_kit/layout/input_spec.rb index d03b406..596a9f7 100644 --- a/spec/lib/slack/block_kit/layout/input_spec.rb +++ b/spec/lib/slack/block_kit/layout/input_spec.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::Input do subject(:input_json) { instance.as_json } + it_behaves_like 'a block that handles block_id length limits', label: 'Name', element: Slack::BlockKit::Element::PlainTextInput.new(action_id: '__ACTION_ID__') + let(:instance) { described_class.new(**params) } let(:optional) { false } let(:hint) { 'Your Name' } diff --git a/spec/lib/slack/block_kit/layout/rich_text_spec.rb b/spec/lib/slack/block_kit/layout/rich_text_spec.rb index 859fb63..0ac63ac 100644 --- a/spec/lib/slack/block_kit/layout/rich_text_spec.rb +++ b/spec/lib/slack/block_kit/layout/rich_text_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::RichText do subject(:rich_text_json) { instance.as_json } @@ -19,6 +20,8 @@ expect(rich_text_json).to eq(expected_json) end + it_behaves_like 'a block that handles block_id length limits' + describe '#rich_text_section' do let(:expected_json) do { diff --git a/spec/lib/slack/block_kit/layout/section_spec.rb b/spec/lib/slack/block_kit/layout/section_spec.rb index a663a00..cbe1da9 100644 --- a/spec/lib/slack/block_kit/layout/section_spec.rb +++ b/spec/lib/slack/block_kit/layout/section_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::Section do subject(:section_json) { instance.as_json } @@ -26,6 +27,8 @@ expect(section_json).to eq(expected_json) end + it_behaves_like 'a block that handles block_id length limits' + describe '#mrkdwn' do let(:expected_json) do { diff --git a/spec/lib/slack/block_kit/layout/video_spec.rb b/spec/lib/slack/block_kit/layout/video_spec.rb index b008d05..290ae3b 100644 --- a/spec/lib/slack/block_kit/layout/video_spec.rb +++ b/spec/lib/slack/block_kit/layout/video_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../limiters/block_id_helpers' RSpec.describe Slack::BlockKit::Layout::Video do + it_behaves_like 'a block that handles block_id length limits', alt_text: 'test', thumbnail_url: 'test', video_url: 'test', title: 'test', description: 'test' + describe '.as_json' do subject(:video_json) { instance.as_json } diff --git a/spec/lib/slack/block_kit/limiters/block_id_helpers.rb b/spec/lib/slack/block_kit/limiters/block_id_helpers.rb new file mode 100644 index 0000000..ac522b5 --- /dev/null +++ b/spec/lib/slack/block_kit/limiters/block_id_helpers.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'a block that handles block_id length limits' do |**params| + let(:block) { described_class.new(block_id: block_id, **params) } + let(:block_id) { 'a' * (Slack::BlockKit::Limiters::BlockId::BLOCK_ID_LIMIT + 1) } + + subject(:json) { block.as_json } + + context 'when autofix is disabled' do + it 'does not truncate the block_id when converting to JSON' do + expect(json[:block_id]).to eq(block_id) + end + end + + context 'when autofix is enabled' do + let(:block) { described_class.new(block_id: block_id, **params) } + + around do |example| + Slack::BlockKit.configuration.autofix_invalid_blocks = true + example.run + Slack::BlockKit.configuration.autofix_invalid_blocks = false + end + + it 'truncates the block_id when converting to JSON' do + expect(json[:block_id]).to eq('a' * Slack::BlockKit::Limiters::BlockId::BLOCK_ID_LIMIT) + end + + context 'when the block_id is already within the limit' do + let(:block_id) { 'a' * (Slack::BlockKit::Limiters::BlockId::BLOCK_ID_LIMIT - 1) } + + it 'does not truncate the block_id' do + expect(json[:block_id]).to eq(block_id) + end + end + + context 'when no block_id is provided' do + let(:block) { described_class.new(**params) } + let(:block_id) { nil } + + it 'does not include block_id in the JSON' do + expect(json).not_to have_key(:block_id) + end + end + end +end diff --git a/spec/lib/slack/block_kit/utils_spec.rb b/spec/lib/slack/block_kit/utils_spec.rb new file mode 100644 index 0000000..0c42cba --- /dev/null +++ b/spec/lib/slack/block_kit/utils_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Slack::BlockKit::Utils do + describe '.truncate' do + it 'truncates a string to the specified length' do + expect(described_class.truncate('Hello, world!', length: 13)).to eq('Hello, world!') + expect(described_class.truncate('Hello, world!', length: 12)).to eq('Hello, wo...') + end + + it 'truncates a string with a custom omission' do + expect(described_class.truncate('Hello, world!', length: 13, omission: '[...]')).to eq('Hello, world!') + expect(described_class.truncate('Hello, world!', length: 12, omission: '[...]')).to eq('Hello, [...]') + end + + it 'truncates a string based on a separator' do + expect(described_class.truncate('Hello, world!', length: 13, separator: ' ')).to eq('Hello, world!') + expect(described_class.truncate('Hello, world!', length: 13, separator: /\s/)).to eq('Hello, world!') + + expect(described_class.truncate('Hello, big world!', length: 13, separator: ' ')).to eq('Hello, big...') + expect(described_class.truncate('Hello, big world!', length: 12, separator: /\s/)).to eq('Hello,...') + expect(described_class.truncate('Hello, big world!', length: 13, separator: /,/)).to eq('Hello...') + end + + it 'truncates a string with a custom omission and separator' do + expect(described_class.truncate('Hello, big world!', length: 16, omission: '[...]', separator: ' ')).to eq('Hello, big[...]') + expect(described_class.truncate('Hello, big world!', length: 14, omission: '[...]', separator: /\s/)).to eq('Hello,[...]') + expect(described_class.truncate('Hello, big world!', length: 16, omission: '[...]', separator: /,/)).to eq('Hello[...]') + end + end +end