line-message-builder 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f66fccfc1d5cd52c32662d8e912aac28f79e82d0b86ee837125ebe292fdd5f13
4
- data.tar.gz: b819400dc6c6afbcb85b99335dbbf60c0a8c2cfe71c4f4f2aa3653e56fd5edf5
3
+ metadata.gz: 3bbeb0eeec944a0886b6e6dec3b171c88182c0097ad31c03cc16d7270f1eb471
4
+ data.tar.gz: 8f08afeab2e40b152e8e4218b00f03f725be5f3cfa70b687ebaf20e7d1dd2d7e
5
5
  SHA512:
6
- metadata.gz: 2e7a7f40dede7c209dd9d4f581020a2d9cf9b9403675b055382446a440852861ab1585832b9bae31b31a12aaea68f5bd7c32e2ce9cde8950715db38864b726dd
7
- data.tar.gz: 87d0a63d6fdab6a7338745eac2e3b391605c8631ac7cef4226ac17e098b93ac152f39ecfe5bbfbe9f98576ab52fd92e9a1377819c3bfc10238cd95e342ff09fb
6
+ metadata.gz: 0afd2ab03e4bbe7df364ebc27764380e48b9fd00a0a78ba345655f9227b7ff1cac00831024fd4ca923d2df1fb195c7e3c4947e6290f530a3466d002b325af4f3
7
+ data.tar.gz: 91455d81ccc9d6aa646e5a5851ad55baa19e89d4c427b4c9e4fa951676f6cf5d6e8005983d5a76610c05ced666a353d06f54ef7020516ad67250000cb4379ba7
@@ -1 +1 @@
1
- {".":"0.2.0"}
1
+ {".":"0.4.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/elct9620/line-message-builder/compare/v0.3.0...v0.4.0) (2025-04-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * Add flex and margin options to Line Flex Button with improved error handling ([c8cd142](https://github.com/elct9620/line-message-builder/commit/c8cd1427528d5495d13bf93898da4638588065ae))
9
+ * Add flex box options and validation for LINE message builder ([133f87d](https://github.com/elct9620/line-message-builder/commit/133f87ddb3fd2a093947db0aef6363cd4c150701))
10
+ * Add flex bubble options and RSpec matcher for Line message builder ([605ad89](https://github.com/elct9620/line-message-builder/commit/605ad89870a53c55182f96db5f2738b5c347f0dc))
11
+ * Add margin option to flex text builder ([1d44522](https://github.com/elct9620/line-message-builder/commit/1d44522a44218bedab3d2009ef483ed112784248))
12
+ * Add quote token support for Line text messages ([7871999](https://github.com/elct9620/line-message-builder/commit/7871999dfc7032e75e5407c8b16bed7ef7dd28b8))
13
+ * Add support for flex, margin, and align options in Flex image builder ([693d0ab](https://github.com/elct9620/line-message-builder/commit/693d0abf5ef524b2e6d075f3c7e65eca49cf7f27))
14
+
15
+ ## [0.3.0](https://github.com/elct9620/line-message-builder/compare/v0.2.0...v0.3.0) (2025-04-05)
16
+
17
+
18
+ ### Miscellaneous Chores
19
+
20
+ * release 0.3.0 ([2f0b00c](https://github.com/elct9620/line-message-builder/commit/2f0b00c875a2d547c576e383aaeeae3bf777e898))
21
+
3
22
  ## [0.2.0](https://github.com/elct9620/line-message-builder/compare/v0.1.0...v0.2.0) (2025-04-05)
4
23
 
5
24
 
data/README.md CHANGED
@@ -62,8 +62,8 @@ For Rails, you can use `view_context` to make `Builder` to access Rails helpers.
62
62
  builder = Line::MessageBuilder::Builder.with(view_context) do
63
63
  text "Anything you want?" do
64
64
  quick_reply do
65
- action "Yes", label: "Yes", image_url: image_url("yes.png")
66
- action "No", label: "No", image_url: image_url("no.png")
65
+ message "Yes", label: "Yes", image_url: image_url("yes.png")
66
+ message "No", label: "No", image_url: image_url("no.png")
67
67
  end
68
68
  end
69
69
  end
@@ -82,8 +82,8 @@ context = ActionView::Base.new(
82
82
  builder = Line::MessageBuilder::Builder.with(context) do
83
83
  text "Anything you want?" do
84
84
  quick_reply do
85
- action "Yes", label: "Yes", image_url: image_url("yes.png")
86
- action "No", label: "No", image_url: image_url("no.png")
85
+ message "Yes", label: "Yes", image_url: image_url("yes.png")
86
+ message "No", label: "No", image_url: image_url("no.png")
87
87
  end
88
88
  end
89
89
  end
@@ -91,6 +91,17 @@ end
91
91
 
92
92
  ### RSpec Matcher
93
93
 
94
+ | Matcher | Description |
95
+ | --------------------------- | ------------------------------------ |
96
+ | `have_line_text_message` | Match a text message |
97
+ | `have_line_flex_message` | Match a flex message |
98
+ | `have_line_flex_bubble` | Match a flex message with bubble |
99
+ | `have_line_flex_text` | Match a flex message with text |
100
+ | `have_line_flex_image` | Match a flex message with image |
101
+ | `have_line_flex_button` | Match a flex message with button |
102
+ | `have_line_flex_box` | Match a flex message with box |
103
+
104
+
94
105
  Add `line/message/rspec` to your `spec_helper.rb` or `rails_helper.rb`:
95
106
 
96
107
  ```ruby
@@ -121,6 +132,19 @@ it { is_expected.to have_line_text_message("Hello, world!") }
121
132
  it { is_expected.to have_line_text_message(/Nice to meet you!/) }
122
133
  ```
123
134
 
135
+ The matchers can work with webmock `a_request`:
136
+
137
+ ```ruby
138
+ it "reply with message" do
139
+ expect(a_request(:post, "https://api.line.me/v2/bot/message/reply")
140
+ .with(
141
+ body: hash_including({
142
+ messages: have_line_text_message(/Hello, world!/),
143
+ },
144
+ ))).to have_been_made.once
145
+ end
146
+ ```
147
+
124
148
  ## Capabilities
125
149
 
126
150
  - ✅ Supported
@@ -131,7 +155,7 @@ it { is_expected.to have_line_text_message(/Nice to meet you!/) }
131
155
 
132
156
  | Type | Supported |
133
157
  | ---- | --------- |
134
- | Text | |
158
+ | Text | 🚧 |
135
159
  | Text v2 | ❌ |
136
160
  | Sticker | ❌ |
137
161
  | Sticker | ❌ |
@@ -166,19 +190,19 @@ it { is_expected.to have_line_text_message(/Nice to meet you!/) }
166
190
 
167
191
  ### Flex Components
168
192
 
169
- | Component | Supported |
170
- | --------- | --------- |
171
- | Bubble | 🚧 |
172
- | Carousel | ❌ |
173
- | Box | 🚧 |
174
- | Button | 🚧 |
175
- | Image | 🚧 |
176
- | Video | ❌ |
177
- | Icon | ❌ |
178
- | Text | 🚧 |
179
- | Span | ❌ |
180
- | Separator | ❌ |
181
- | Filler | ❌ |
193
+ | Component | Supported |
194
+ | --------- | --------- |
195
+ | Bubble | 🚧 |
196
+ | Carousel | ❌ |
197
+ | Box | 🚧 |
198
+ | Button | 🚧 |
199
+ | Image | 🚧 |
200
+ | Video | ❌ |
201
+ | Icon | ❌ |
202
+ | Text | 🚧 |
203
+ | Span | ❌ |
204
+ | Separator | ❌ |
205
+ | Filler | ❌ Deprecated |
182
206
 
183
207
  ## Development
184
208
 
@@ -6,7 +6,7 @@ module Line
6
6
  module Actions
7
7
  # The Message class is used to build message actions for quick replies.
8
8
  class Message < Line::Message::Builder::Base
9
- def initialize(text: nil, label: nil, context: nil, &)
9
+ def initialize(text, label: nil, context: nil, &)
10
10
  @text = text
11
11
  @label = label
12
12
 
@@ -6,7 +6,7 @@ module Line
6
6
  module Actions
7
7
  # The Postback class is used to build postback actions for quick replies.
8
8
  class Postback < Line::Message::Builder::Base
9
- def initialize(data: nil, label: nil, display_text: nil, context: nil, &)
9
+ def initialize(data, label: nil, display_text: nil, context: nil, &)
10
10
  @data = data
11
11
  @label = label
12
12
  @display_text = display_text
@@ -13,12 +13,12 @@ module Line
13
13
  super
14
14
  end
15
15
 
16
- def text(text, &)
17
- @messages << Text.new(text, context: context, &)
16
+ def text(text, **options, &)
17
+ @messages << Text.new(text, context: context, **options, &)
18
18
  end
19
19
 
20
- def flex(**args, &)
21
- @messages << Flex::Builder.new(**args, context: context, &)
20
+ def flex(**options, &)
21
+ @messages << Flex::Builder.new(context: context, **options, &)
22
22
  end
23
23
 
24
24
  def build
@@ -6,7 +6,11 @@ module Line
6
6
  module Flex
7
7
  # The box is a component for the Flex message.
8
8
  class Box < Line::Message::Builder::Base
9
+ attr_reader :contents
10
+
9
11
  option :layout, default: :horizontal
12
+ option :spacing, default: nil
13
+ option :flex, default: nil
10
14
 
11
15
  def initialize(context: nil, **options, &)
12
16
  @contents = []
@@ -31,10 +35,15 @@ module Line
31
35
  end
32
36
 
33
37
  def to_h
38
+ raise RequiredError, "layout is required" if layout.nil?
39
+ raise RequiredError, "contents is required" if contents.empty?
40
+
34
41
  {
35
42
  type: "box",
36
- layout: @layout,
37
- contents: @contents.map(&:to_h)
43
+ layout: layout,
44
+ spacing: spacing,
45
+ flex: flex,
46
+ contents: contents.map(&:to_h)
38
47
  }.compact
39
48
  end
40
49
  end
@@ -6,7 +6,10 @@ module Line
6
6
  module Flex
7
7
  # The bubble is container for the Flex message.
8
8
  class Bubble < Line::Message::Builder::Base
9
- def initialize(context: nil, &)
9
+ option :size, default: nil
10
+ option :styles, default: nil
11
+
12
+ def initialize(context: nil, **options, &)
10
13
  @header = nil
11
14
  @hero = nil
12
15
  @body = nil
@@ -41,7 +44,9 @@ module Line
41
44
  header: @header&.to_h,
42
45
  hero: @hero&.to_h,
43
46
  body: @body&.to_h,
44
- footer: @footer&.to_h
47
+ footer: @footer&.to_h,
48
+ size: size,
49
+ styles: styles
45
50
  }.compact
46
51
  end
47
52
  end
@@ -9,20 +9,22 @@ module Line
9
9
  option :alt_text, default: nil
10
10
 
11
11
  def initialize(context: nil, **options, &)
12
- @contents = []
12
+ @contents = nil
13
13
 
14
14
  super
15
15
  end
16
16
 
17
- def bubble(&)
18
- @contents << Line::Message::Builder::Flex::Bubble.new(context: context, &)
17
+ def bubble(**options, &)
18
+ @contents = Line::Message::Builder::Flex::Bubble.new(context: context, **options, &)
19
19
  end
20
20
 
21
21
  def to_h
22
+ raise Error, "contents should be bubble or carousel" if @contents.nil?
23
+
22
24
  {
23
25
  type: "flex",
24
26
  altText: @alt_text,
25
- contents: @contents.map(&:to_h),
27
+ contents: @contents.to_h,
26
28
  quickReply: @quick_reply&.to_h
27
29
  }.compact
28
30
  end
@@ -6,6 +6,10 @@ module Line
6
6
  module Flex
7
7
  # The button is a component of the Flex message.
8
8
  class Button < Line::Message::Builder::Base
9
+ attr_reader :action
10
+
11
+ option :flex, default: nil
12
+ option :margin, default: nil
9
13
  option :style, default: :link
10
14
  option :height, default: :md
11
15
 
@@ -16,21 +20,23 @@ module Line
16
20
  end
17
21
 
18
22
  def message(text, label:, display_text: nil, &)
19
- @action = Actions::Message.new(text: text, label: label, display_text: display_text, &)
23
+ @action = Actions::Message.new(text, label: label, display_text: display_text, &)
20
24
  end
21
25
 
22
26
  def postback(data, label: nil, display_text: nil, &)
23
- @action = Actions::Postback.new(data: data, label: label, display_text: display_text, &)
27
+ @action = Actions::Postback.new(data, label: label, display_text: display_text, &)
24
28
  end
25
29
 
26
30
  def to_h
27
- raise Error, "Action is required" unless @action
31
+ raise RequiredError, "action is required" if action.nil?
28
32
 
29
33
  {
30
34
  type: "button",
31
- action: @action.to_h,
32
- style: @style,
33
- height: @height
35
+ action: action.to_h,
36
+ flex: flex,
37
+ margin: margin,
38
+ style: style,
39
+ height: height
34
40
  }.compact
35
41
  end
36
42
  end
@@ -6,9 +6,14 @@ module Line
6
6
  module Flex
7
7
  # The image is a component for the Flex message.
8
8
  class Image < Line::Message::Builder::Base
9
+ attr_reader :url
10
+
9
11
  option :size, default: nil
10
12
  option :aspect_ratio, default: nil
11
13
  option :aspect_mode, default: nil
14
+ option :flex, default: nil
15
+ option :margin, default: nil
16
+ option :align, default: nil
12
17
 
13
18
  def initialize(url, context: nil, **options, &)
14
19
  @url = url
@@ -16,25 +21,18 @@ module Line
16
21
  super(context: context, **options, &)
17
22
  end
18
23
 
19
- def size(size)
20
- @size = size
21
- end
22
-
23
- def aspect_ratio(aspect_ratio)
24
- @aspect_ratio = aspect_ratio
25
- end
26
-
27
- def aspect_mode(aspect_mode)
28
- @aspect_mode = aspect_mode
29
- end
24
+ def to_h # rubocop:disable Metrics/MethodLength
25
+ raise RequiredError, "url is required" if url.nil?
30
26
 
31
- def to_h
32
27
  {
33
28
  type: "image",
34
- url: @url,
35
- size: @size,
36
- aspectRatio: @aspect_ratio,
37
- aspectMode: @aspect_mode
29
+ url: url,
30
+ size: size,
31
+ flex: flex,
32
+ margin: margin,
33
+ align: align,
34
+ aspectRatio: aspect_ratio,
35
+ aspectMode: aspect_mode
38
36
  }.compact
39
37
  end
40
38
  end
@@ -6,12 +6,15 @@ module Line
6
6
  module Flex
7
7
  # The text is a component of the Flex message.
8
8
  class Text < Line::Message::Builder::Base
9
+ attr_reader :text
10
+
11
+ option :size, default: nil
9
12
  option :wrap, default: false
10
13
  option :line_spacing, default: nil
11
14
  option :color, default: nil
12
- option :size, default: nil
13
15
  option :align, default: nil
14
16
  option :flex, default: nil
17
+ option :margin, default: nil
15
18
 
16
19
  def initialize(text, context: nil, **options, &)
17
20
  @text = text
@@ -23,16 +26,19 @@ module Line
23
26
  @wrap = true
24
27
  end
25
28
 
26
- def to_h
29
+ def to_h # rubocop:disable Metrics/MethodLength
30
+ raise RequiredError, "text is required" if text.nil?
31
+
27
32
  {
28
33
  type: "text",
29
- text: @text,
30
- wrap: @wrap,
31
- lineSpacing: @line_spacing,
32
- color: @color,
33
- size: @size,
34
- align: @align,
35
- flex: @flex
34
+ text: text,
35
+ wrap: wrap,
36
+ lineSpacing: line_spacing,
37
+ color: color,
38
+ size: size,
39
+ align: align,
40
+ margin: margin,
41
+ flex: flex
36
42
  }.compact
37
43
  end
38
44
  end
@@ -13,14 +13,14 @@ module Line
13
13
 
14
14
  def message(text, label:, image_url: nil, &)
15
15
  action(
16
- Actions::Message.new(text: text, label: label, &),
16
+ Actions::Message.new(text, label: label, &),
17
17
  image_url
18
18
  )
19
19
  end
20
20
 
21
21
  def postback(data, label: nil, display_text: nil, image_url: nil, &)
22
22
  action(
23
- Actions::Postback.new(data: data, label: label, display_text: display_text, &),
23
+ Actions::Postback.new(data, label: label, display_text: display_text, &),
24
24
  image_url
25
25
  )
26
26
  end
@@ -5,16 +5,19 @@ module Line
5
5
  module Builder
6
6
  # Text message builder.
7
7
  class Text < Base
8
- def initialize(text, context: nil, &block)
8
+ option :quote_token, default: nil
9
+
10
+ def initialize(text, context: nil, **options, &block)
9
11
  @text = text
10
12
 
11
- super(context: context, &block)
13
+ super(context: context, **options, &block)
12
14
  end
13
15
 
14
16
  def to_h
15
17
  {
16
18
  type: "text",
17
19
  text: @text,
20
+ quoteToken: quote_token,
18
21
  quickReply: @quick_reply&.to_h
19
22
  }.compact
20
23
  end
@@ -3,7 +3,7 @@
3
3
  module Line
4
4
  module Message
5
5
  module Builder
6
- VERSION = "0.2.0"
6
+ VERSION = "0.4.0"
7
7
  end
8
8
  end
9
9
  end
@@ -7,6 +7,7 @@ module Line
7
7
  # The Builder module provides a DSL for building LINE messages.
8
8
  module Builder
9
9
  class Error < StandardError; end
10
+ class RequiredError < Error; end
10
11
 
11
12
  require_relative "builder/base"
12
13
  require_relative "builder/actions"
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Line
4
+ module Message
5
+ module RSpec
6
+ # :nodoc:
7
+ module Matchers
8
+ # The flex component matcher for RSpec to search nested flex components in the message array.
9
+ class HaveFlexBubble
10
+ def initialize(expected)
11
+ @expected = Utils.stringify_keys!(expected || {}, deep: true)
12
+ end
13
+
14
+ def description
15
+ return "have flex bubble" if @expected.empty?
16
+
17
+ "have flex bubble matching #{@expected.inspect}"
18
+ end
19
+
20
+ def matches?(actual)
21
+ @actual = Utils.stringify_keys!(actual, deep: true)
22
+ @actual.any? { |message| match_flex_component?(message) }
23
+ end
24
+ alias == matches?
25
+
26
+ def failure_message
27
+ return "expected to find a flex bubble" if @expected.empty?
28
+
29
+ "expected to find a flex bubble matching #{@expected.inspect}"
30
+ end
31
+
32
+ private
33
+
34
+ def match_flex_component?(message)
35
+ return false unless message["type"] == "flex"
36
+
37
+ match_content?(message["contents"])
38
+ end
39
+
40
+ def match_content?(content)
41
+ return match_options?(content) if content["type"] == "bubble"
42
+ return false unless content["contents"]
43
+
44
+ content["contents"].any? { |nested_content| match_content?(nested_content) }
45
+ end
46
+
47
+ def match_options?(content)
48
+ return false unless content["type"] == "bubble"
49
+
50
+ ::RSpec::Matchers::BuiltIn::Include.new(@expected).matches?(content)
51
+ end
52
+ end
53
+
54
+ def have_line_flex_bubble(expected = nil) # rubocop:disable Naming/PredicateName
55
+ HaveFlexBubble.new(expected)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -20,9 +20,10 @@ module Line
20
20
  end
21
21
 
22
22
  def matches?(actual)
23
- @actual = actual
23
+ @actual = Utils.stringify_keys!(actual, deep: true)
24
24
  @actual.any? { |message| match_flex_component?(message) }
25
25
  end
26
+ alias == matches?
26
27
 
27
28
  def failure_message
28
29
  return "expected to find a flex component" if @expected.nil?
@@ -34,16 +35,16 @@ module Line
34
35
  private
35
36
 
36
37
  def match_flex_component?(message)
37
- return false unless message[:type] == "flex"
38
+ return false unless message["type"] == "flex"
38
39
 
39
- message[:contents].any? { |content| match_content?(content) }
40
+ match_content?(message["contents"])
40
41
  end
41
42
 
42
43
  def match_content?(content)
43
- return match_bubble?(content) if content[:type] == "bubble"
44
+ return match_bubble?(content) if content["type"] == "bubble"
44
45
 
45
- if content[:contents]
46
- return content[:contents].any? do |nested_content|
46
+ if content["contents"]
47
+ return content["contents"].any? do |nested_content|
47
48
  match_content?(nested_content)
48
49
  end || @expected.call(content)
49
50
  end
@@ -52,7 +53,7 @@ module Line
52
53
  end
53
54
 
54
55
  def match_bubble?(content)
55
- %i[header hero body footer].any? do |key|
56
+ %w[header hero body footer].any? do |key|
56
57
  block = content[key]
57
58
  next unless block
58
59
 
@@ -61,7 +62,7 @@ module Line
61
62
  end
62
63
 
63
64
  def match_bubble_block?(block)
64
- return block[:contents].any? { |nested_content| match_content?(nested_content) } if block[:contents]
65
+ return block["contents"].any? { |nested_content| match_content?(nested_content) } if block["contents"]
65
66
 
66
67
  match_content?(block)
67
68
  end
@@ -72,37 +73,45 @@ module Line
72
73
  end
73
74
 
74
75
  def have_line_flex_box(**options) # rubocop:disable Naming/PredicateName
76
+ options = Utils.stringify_keys!(options, deep: true)
77
+
75
78
  HaveFlexComponent.new(expected_desc: "box(#{options.inspect})") do |content|
76
- next false unless content[:type] == "box"
79
+ next false unless content["type"] == "box"
77
80
 
78
81
  ::RSpec::Matchers::BuiltIn::Include.new(options).matches?(content)
79
82
  end
80
83
  end
81
84
 
82
85
  def have_line_flex_text(text, **options) # rubocop:disable Naming/PredicateName
86
+ options = Utils.stringify_keys!(options, deep: true)
87
+
83
88
  HaveFlexComponent.new(expected_desc: "text(#{text.inspect})") do |content|
84
- next false unless content[:type] == "text"
89
+ next false unless content["type"] == "text"
85
90
 
86
- content[:text].match?(text) && ::RSpec::Matchers::BuiltIn::Include.new(options).matches?(content)
91
+ content["text"].match?(text) && ::RSpec::Matchers::BuiltIn::Include.new(options).matches?(content)
87
92
  end
88
93
  end
89
94
 
90
95
  def have_line_flex_button(type, **options) # rubocop:disable Naming/PredicateName
96
+ options = Utils.stringify_keys!(options, deep: true)
97
+
91
98
  HaveFlexComponent.new(expected_desc: "#{type} button(#{options.inspect})") do |content|
92
- next false unless content[:type] == "button"
99
+ next false unless content["type"] == "button"
93
100
 
94
- ::RSpec::Matchers::BuiltIn::Include.new({ type: type, **options }).matches?(content[:action]) ||
95
- ::RSpec::Matchers::BuiltIn::Include.new({ **options, action: ::RSpec::Matchers::BuiltIn::Include.new(
96
- type: type
101
+ ::RSpec::Matchers::BuiltIn::Include.new({ "type" => type, **options }).matches?(content["action"]) ||
102
+ ::RSpec::Matchers::BuiltIn::Include.new({ **options, "action" => ::RSpec::Matchers::BuiltIn::Include.new(
103
+ "type" => type
97
104
  ) }).matches?(content)
98
105
  end
99
106
  end
100
107
 
101
108
  def have_line_flex_image(url, **options) # rubocop:disable Naming/PredicateName
109
+ options = Utils.stringify_keys!(options, deep: true)
110
+
102
111
  HaveFlexComponent.new(expected_desc: "image(#{url.inspect})") do |content|
103
- next false unless content[:type] == "image"
112
+ next false unless content["type"] == "image"
104
113
 
105
- ::RSpec::Matchers::BuiltIn::Include.new({ url: url, **options }).matches?(content)
114
+ ::RSpec::Matchers::BuiltIn::Include.new({ "url" => url, **options }).matches?(content)
106
115
  end
107
116
  end
108
117
  end
@@ -18,9 +18,10 @@ module Line
18
18
  end
19
19
 
20
20
  def matches?(actual)
21
- @actual = actual
21
+ @actual = Utils.stringify_keys!(actual, deep: true)
22
22
  @actual.any? { |message| match_alt_text?(message) }
23
23
  end
24
+ alias == matches?
24
25
 
25
26
  def failure_message
26
27
  "expected to find a flex message alt text matching #{@expected}"
@@ -29,10 +30,10 @@ module Line
29
30
  private
30
31
 
31
32
  def match_alt_text?(message)
32
- return false unless message[:type] == "flex"
33
+ return false unless message["type"] == "flex"
33
34
  return true if @expected.nil?
34
35
 
35
- message[:altText].match?(@expected)
36
+ message["altText"].match?(@expected)
36
37
  end
37
38
  end
38
39
 
@@ -8,7 +8,7 @@ module Line
8
8
  # The quick reply matcher for RSpec to search for quick reply action in the message array.
9
9
  class HaveQuickReply
10
10
  def initialize(expected)
11
- @expected = expected
11
+ @expected = Utils.stringify_keys!(expected, deep: true)
12
12
  end
13
13
 
14
14
  def description
@@ -18,9 +18,10 @@ module Line
18
18
  end
19
19
 
20
20
  def matches?(actual)
21
- @actual = actual
21
+ @actual = Utils.stringify_keys!(actual, deep: true)
22
22
  @actual.any? { |message| match_message(message) }
23
23
  end
24
+ alias == matches?
24
25
 
25
26
  def failure_message
26
27
  "expected to find a quick reply message matching #{@expected}"
@@ -29,10 +30,10 @@ module Line
29
30
  private
30
31
 
31
32
  def match_message(message)
32
- reply = message[:quickReply]
33
+ reply = message["quickReply"]
33
34
  return false unless reply
34
35
 
35
- reply[:items].any? { |item| match_action(item[:action]) }
36
+ reply["items"].any? { |item| match_action(item["action"]) }
36
37
  end
37
38
 
38
39
  def match_action(action)
@@ -8,32 +8,37 @@ module Line
8
8
  # The text message matcher for RSpec to search for text messages in the message array.
9
9
  class HaveTextMessage
10
10
  def initialize(expected)
11
- @expected = expected
11
+ @text, @options = expected
12
+ @options = Utils.stringify_keys!(@options || {})
12
13
  end
13
14
 
14
15
  def description
15
- return "have text message" if @expected.nil?
16
+ return "have text message" if @text.nil?
17
+ return "have text message matching #{@text.inspect}" if @options.empty?
16
18
 
17
- "have text message matching #{@expected.inspect}"
19
+ "have text message matching #{@text.inspect} with options #{@options.inspect}"
18
20
  end
19
21
 
20
22
  def matches?(actual)
21
- @actual = actual
22
- @actual.each do |message|
23
- next unless message[:type] == "text"
23
+ @actual = Utils.stringify_keys!(actual, deep: true)
24
+ @actual.any? do |message|
25
+ next unless message["type"] == "text"
26
+ next true if @text.nil?
24
27
 
25
- return true if message[:text].match?(@expected)
28
+ message["text"].match?(@text) && ::RSpec::Matchers::BuiltIn::Include.new(@options).matches?(message)
26
29
  end
27
-
28
- false
29
30
  end
31
+ alias == matches?
30
32
 
31
33
  def failure_message
32
- "expected to find a text message matching #{@expected}"
34
+ return "expected to find a text message" if @text.nil?
35
+ return "expected to find a text message matching #{@text.inspect}" if @options.empty?
36
+
37
+ "expected to find a text message matching #{@text.inspect} with options #{@options.inspect}"
33
38
  end
34
39
  end
35
40
 
36
- def have_line_text_message(expected = nil) # rubocop:disable Naming/PredicateName
41
+ def have_line_text_message(*expected) # rubocop:disable Naming/PredicateName
37
42
  HaveTextMessage.new(expected)
38
43
  end
39
44
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Line
4
+ module Message
5
+ module RSpec
6
+ module Matchers
7
+ # :nodoc:
8
+ module Utils
9
+ def self.stringify_keys!(arg, deep: false) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
10
+ case arg
11
+ when Hash
12
+ Hash[
13
+ *arg.map { |key, value| [key.to_s, deep ? stringify_keys!(value, deep: deep) : value] }
14
+ .inject([]) { |memo, pair| memo + pair }
15
+ ]
16
+ when Array
17
+ arg.map { |item| deep ? stringify_keys!(item, deep: deep) : item }
18
+ else
19
+ arg
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "matchers/utils"
3
4
  require_relative "matchers/have_text_message"
4
5
  require_relative "matchers/have_quick_reply"
5
-
6
6
  require_relative "matchers/have_flex_message"
7
+ require_relative "matchers/have_flex_bubble"
7
8
  require_relative "matchers/have_flex_component"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: line-message-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aotokitsuruya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-05 00:00:00.000000000 Z
11
+ date: 2025-04-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: The LINE Messaging API message builder.
14
14
  email:
@@ -43,10 +43,12 @@ files:
43
43
  - lib/line/message/builder/version.rb
44
44
  - lib/line/message/rspec.rb
45
45
  - lib/line/message/rspec/matchers.rb
46
+ - lib/line/message/rspec/matchers/have_flex_bubble.rb
46
47
  - lib/line/message/rspec/matchers/have_flex_component.rb
47
48
  - lib/line/message/rspec/matchers/have_flex_message.rb
48
49
  - lib/line/message/rspec/matchers/have_quick_reply.rb
49
50
  - lib/line/message/rspec/matchers/have_text_message.rb
51
+ - lib/line/message/rspec/matchers/utils.rb
50
52
  - release-please-config.json
51
53
  - sig/line/message/builder.rbs
52
54
  homepage: https://github.com/elct9620/line-message-builder