line-message-builder 0.7.0 → 0.9.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.document +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/.rubocop.yml +2 -0
  5. data/CHANGELOG.md +35 -0
  6. data/CONVENTIONS.md +36 -0
  7. data/README.md +49 -31
  8. data/lib/line/message/builder/actions/message.rb +67 -1
  9. data/lib/line/message/builder/actions/postback.rb +71 -1
  10. data/lib/line/message/builder/actions.rb +18 -1
  11. data/lib/line/message/builder/base.rb +113 -3
  12. data/lib/line/message/builder/container.rb +125 -5
  13. data/lib/line/message/builder/context.rb +111 -3
  14. data/lib/line/message/builder/flex/actionable.rb +46 -1
  15. data/lib/line/message/builder/flex/box.rb +191 -15
  16. data/lib/line/message/builder/flex/bubble.rb +96 -8
  17. data/lib/line/message/builder/flex/builder.rb +74 -8
  18. data/lib/line/message/builder/flex/button.rb +104 -17
  19. data/lib/line/message/builder/flex/carousel.rb +71 -8
  20. data/lib/line/message/builder/flex/image.rb +105 -20
  21. data/lib/line/message/builder/flex/partial.rb +94 -9
  22. data/lib/line/message/builder/flex/position.rb +122 -12
  23. data/lib/line/message/builder/flex/separator.rb +41 -0
  24. data/lib/line/message/builder/flex/size.rb +65 -7
  25. data/lib/line/message/builder/flex/span.rb +110 -0
  26. data/lib/line/message/builder/flex/text.rb +176 -28
  27. data/lib/line/message/builder/flex.rb +56 -12
  28. data/lib/line/message/builder/quick_reply.rb +16 -4
  29. data/lib/line/message/builder/text.rb +12 -1
  30. data/lib/line/message/builder/version.rb +1 -1
  31. data/lib/line/message/builder.rb +33 -3
  32. data/lib/line/message/rspec/matchers/have_flex_component.rb +11 -0
  33. data/lib/line/message/rspec/matchers/have_flex_separator.rb +20 -0
  34. data/lib/line/message/rspec/matchers.rb +1 -0
  35. data/lib/line/message/rspec.rb +14 -0
  36. data/llm.txt +437 -0
  37. metadata +6 -1
@@ -4,43 +4,164 @@ module Line
4
4
  module Message
5
5
  module Builder
6
6
  module Flex
7
- # The text is a component of the Flex message.
7
+ # Represents a "text" component in a LINE Flex Message.
8
+ #
9
+ # Text components are used to display strings of text. They offer various
10
+ # styling options, including font `size`, `weight` (via styles in a {Box}
11
+ # or {Bubble}), `color`, `align`ment, `gravity`, text `wrap`ping,
12
+ # `line_spacing`, and more. A text component can also have an
13
+ # {Actionable#action action} to make it tappable.
14
+ #
15
+ # Text components also support embedded {Span} components, which allow parts of
16
+ # the text to have different styling. Spans can be added to a text component
17
+ # using the {#span} method within the Text component block.
18
+ #
19
+ # @example Creating a text component within a box
20
+ # Line::Message::Builder.with do
21
+ # flex alt_text: "Text Example" do
22
+ # bubble do
23
+ # body do
24
+ # text "Hello, Flex World!",
25
+ # size: :xl,
26
+ # color: "#FF0000",
27
+ # wrap: true do
28
+ # message "More info", text: "Tell me more about text"
29
+ # end
30
+ # end
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ # @example Creating a text component with spans
36
+ # Line::Message::Builder.with do
37
+ # flex alt_text: "Span Example" do
38
+ # bubble do
39
+ # body do
40
+ # text "This message has styled spans:" do
41
+ # span "Red", color: "#FF0000"
42
+ # span " and ", size: :sm
43
+ # span "Bold", weight: :bold
44
+ # end
45
+ # end
46
+ # end
47
+ # end
48
+ # end
49
+ #
50
+ # @see https://developers.line.biz/en/reference/messaging-api/#text
51
+ # @see Actionable For making the text tappable.
52
+ # @see Position::Horizontal For `align` property.
53
+ # @see Position::Vertical For `gravity` property.
54
+ # @see Position::Margin For `margin` property.
55
+ # @see Position::Offset For offset properties.
56
+ # @see Size::Flex For `flex` sizing property.
57
+ # @see Size::Shared For common `size` keywords (e.g., `:xl`, `:sm`).
58
+ # @see Size::AdjustMode For `adjust_mode` property.
8
59
  class Text < Line::Message::Builder::Base
9
- include Actionable
10
- include Position::Horizontal
11
- include Position::Vertical
12
- include Position::Margin
13
- include Position::Offset
14
- include Size::Flex
15
- include Size::Shared
16
- include Size::AdjustMode
60
+ include Actionable # Enables defining an action for the text.
61
+ include Position::Horizontal # Adds `align` option.
62
+ include Position::Vertical # Adds `gravity` option.
63
+ include Position::Margin # Adds `margin` option.
64
+ include Position::Offset # Adds offset options.
65
+ include Size::Flex # Adds `flex` option for sizing within a parent box.
66
+ include Size::Shared # Adds `size` option (e.g., :sm, :md, :xl).
67
+ include Size::AdjustMode # Adds `adjust_mode` option.
17
68
 
18
- attr_reader :text
69
+ # @!attribute [r] text
70
+ # @return [String] The actual text content to be displayed.
71
+ # This is a required attribute.
72
+ attr_reader :text, :contents
19
73
 
74
+ # Specifies whether the text should wrap or be truncated if it exceeds
75
+ # the component's width.
76
+ # @!method wrap(value)
77
+ # @param value [Boolean] `true` to enable text wrapping, `false` (default) to disable.
78
+ # @return [Boolean] The current wrap setting.
20
79
  option :wrap, default: false
21
- option :line_spacing, default: nil
80
+
81
+ # Specifies the spacing between lines of text.
82
+ # Can be a pixel value (e.g., "10px") or a keyword.
83
+ # @!method line_spacing(value)
84
+ # @param value [String, nil] The line spacing value (e.g., `"5px"`).
85
+ # @return [String, nil] The current line spacing.
86
+ option :line_spacing, default: nil # API key: lineSpacing
87
+
88
+ # Specifies the color of the text.
89
+ # @!method color(value)
90
+ # @param value [String, nil] Hexadecimal color code (e.g., `"#RRGGBB"`, `"#RRGGBBAA"`).
91
+ # @return [String, nil] The current text color.
22
92
  option :color, default: nil
23
93
 
24
- def initialize(text, context: nil, **options, &)
25
- @text = text
94
+ # Initializes a new Flex Message Text component.
95
+ #
96
+ # @param text_content [String] The text to display. This is required.
97
+ # @param context [Object, nil] An optional context for the builder.
98
+ # @param options [Hash] A hash of options to set instance variables
99
+ # (e.g., `:wrap`, `:color`, `:size`, and options from included modules).
100
+ # @param block [Proc, nil] An optional block, typically used to define an
101
+ # {Actionable#action action} for the text.
102
+ # @raise [ArgumentError] if `text_content` is nil (though the more specific
103
+ # `RequiredError` is raised in `to_h`).
104
+ def initialize(text_content = nil, context: nil, **options, &)
105
+ @text = text_content # The text content is mandatory.
106
+ @contents = [] # Initialize contents for spans, if any.
26
107
 
27
- super(context: context, **options, &)
108
+ super(context: context, **options, &) # Sets options and evals block (for action).
28
109
  end
29
110
 
111
+ # A convenience DSL method to set the `wrap` property to `true`.
112
+ #
113
+ # @example
114
+ # text_component.text "Long text..."
115
+ # text_component.wrap! # Enables text wrapping
116
+ #
117
+ # @return [true]
30
118
  def wrap!
31
- @wrap = true
119
+ wrap(true) # Use the setter generated by `option`
120
+ end
121
+
122
+ # Adds a {Span} component to this text component's contents.
123
+ #
124
+ # Spans allow different styling for parts of the text, such as color,
125
+ # weight, size, and decoration.
126
+ #
127
+ # @param text_content [String] The span text content.
128
+ # @param options [Hash] Options for the span. See {Span#initialize}.
129
+ # @param block [Proc, nil] An optional block for advanced span configuration.
130
+ # @return [Flex::Span] The newly created Span object.
131
+ # @example
132
+ # text "Message with spans:" do
133
+ # span "Important", color: "#FF0000", weight: :bold
134
+ # span " normal text"
135
+ # end
136
+ def span(text_content, **options, &)
137
+ @contents << Span.new(text_content, context: @context, **options, &)
138
+ end
139
+
140
+ def any_content?
141
+ !contents.empty? || !text.nil?
32
142
  end
33
143
 
34
- def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
35
- raise RequiredError, "text is required" if text.nil?
144
+ def to_h
145
+ raise RequiredError, "text content is required for a text component" unless any_content?
36
146
 
147
+ return to_sdkv2 if context.sdkv2?
148
+
149
+ to_api
150
+ end
151
+
152
+ private
153
+
154
+ def to_api # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
37
155
  {
38
156
  type: "text",
39
157
  text: text,
40
- wrap: wrap,
41
- # Position
42
- align: align,
43
- gravity: gravity,
158
+ contents: contents.map(&:to_h), # Convert spans to hash
159
+ wrap: wrap, # From option
160
+ color: color, # From option
161
+ lineSpacing: line_spacing, # From option (maps to API key)
162
+ # Position::Horizontal & Position::Vertical
163
+ align: align, # From Position::Horizontal
164
+ gravity: gravity, # From Position::Vertical
44
165
  # Position::Margin
45
166
  margin: margin,
46
167
  # Position::Offset
@@ -51,13 +172,40 @@ module Line
51
172
  offsetEnd: offset_end,
52
173
  # Size::Flex
53
174
  flex: flex,
54
- # Size::Shared
55
- size: size,
56
- # Size::AdjustMode
57
- adjustMode: adjust_mode,
58
- lineSpacing: line_spacing,
59
- color: color,
60
- action: action&.to_h
175
+ # Size::Shared & Size::AdjustMode
176
+ size: size, # From Size::Shared
177
+ adjustMode: adjust_mode, # From Size::AdjustMode
178
+ # Actionable
179
+ action: action&.to_h # From Actionable module
180
+ }.compact
181
+ end
182
+
183
+ def to_sdkv2 # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
184
+ {
185
+ type: "text",
186
+ text: text,
187
+ contents: contents.map(&:to_h), # Convert spans to hash
188
+ wrap: wrap, # From option
189
+ color: color, # From option
190
+ line_spacing: line_spacing, # From option (maps to API key)
191
+ # Position::Horizontal & Position::Vertical
192
+ align: align, # From Position::Horizontal
193
+ gravity: gravity, # From Position::Vertical
194
+ # Position::Margin
195
+ margin: margin,
196
+ # Position::Offset
197
+ position: position,
198
+ offset_top: offset_top,
199
+ offset_bottom: offset_bottom,
200
+ offset_start: offset_start,
201
+ offset_end: offset_end,
202
+ # Size::Flex
203
+ flex: flex,
204
+ # Size::Shared & Size::AdjustMode
205
+ size: size, # From Size::Shared
206
+ adjust_mode: adjust_mode, # From Size::AdjustMode
207
+ # Actionable
208
+ action: action&.to_h # From Actionable module
61
209
  }.compact
62
210
  end
63
211
  end
@@ -3,25 +3,69 @@
3
3
  module Line
4
4
  module Message
5
5
  module Builder
6
- # The Flex module allows to build Flex messages.
6
+ # The `Line::Message::Builder::Flex` module serves as the primary namespace
7
+ # for all classes, modules, and components related to the construction of
8
+ # LINE Flex Messages within the `line-message-builder` gem.
9
+ #
10
+ # Flex Messages are highly customizable messages that can display rich content
11
+ # with various layouts, components, and interactive elements. This module
12
+ # organizes the DSL for building these messages.
13
+ #
14
+ # This file (`lib/line/message/builder/flex.rb`) is responsible for loading
15
+ # all necessary sub-components and builder logic for Flex Messages, such as:
16
+ # - {Flex::Builder}: The main class used to construct a complete Flex Message object.
17
+ # - Container components: {Flex::Bubble}, {Flex::Carousel}.
18
+ # - Basic components: {Flex::Box}, {Flex::Text}, {Flex::Button}, {Flex::Image}.
19
+ # - Mixin modules for shared functionality: {Flex::Actionable},
20
+ # modules within {Flex::Position} and {Flex::Size}.
21
+ # - The partial system: {Flex::HasPartial} and {Flex::Partial}.
22
+ #
23
+ # While this module itself is a namespace, the actual process of building a
24
+ # Flex Message typically starts within a {Line::Message::Builder::Container}
25
+ # block, by calling the `flex` method on the container. This method then
26
+ # instantiates and uses {Flex::Builder} to define the Flex Message structure.
27
+ #
28
+ # @example How a Flex Message is typically initiated (conceptual)
29
+ # Line::Message::Builder.with do
30
+ # # This `flex` call on root_container would utilize Flex::Builder
31
+ # flex alt_text: "My Flex Message" do
32
+ # bubble do
33
+ # # ... define bubble content ...
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # @see Flex::Builder For the main Flex Message construction entry point.
39
+ # @see Flex::Bubble
40
+ # @see Flex::Carousel
41
+ # @see Flex::Box
42
+ # @see Flex::Text
43
+ # @see Flex::Button
44
+ # @see Flex::Image
45
+ # @see https://developers.line.biz/en/docs/messaging-api/using-flex-messages/
7
46
  module Flex
47
+ # Main builder for the entire Flex Message object
8
48
  require_relative "flex/builder"
49
+
50
+ # Mixin modules for component capabilities
9
51
  require_relative "flex/actionable"
10
- require_relative "flex/position"
11
- require_relative "flex/size"
52
+ require_relative "flex/position" # For positioning options (margin, padding, etc.)
53
+ require_relative "flex/size" # For sizing options (flex factor, specific sizes)
12
54
 
13
- # Partial
55
+ # Partial system for reusable components
14
56
  require_relative "flex/partial"
15
57
 
16
- # Container
17
- require_relative "flex/bubble"
18
- require_relative "flex/carousel"
58
+ # Container-type components
59
+ require_relative "flex/bubble" # Individual message unit
60
+ require_relative "flex/carousel" # Horizontally scrollable list of bubbles
19
61
 
20
- # Components
21
- require_relative "flex/box"
22
- require_relative "flex/text"
23
- require_relative "flex/button"
24
- require_relative "flex/image"
62
+ # Basic building-block components
63
+ require_relative "flex/box" # Layout container
64
+ require_relative "flex/text" # Text display
65
+ require_relative "flex/button" # Interactive button
66
+ require_relative "flex/image" # Image display
67
+ require_relative "flex/separator" # Visual separator
68
+ require_relative "flex/span" # Text span for styling parts of text
25
69
  end
26
70
  end
27
71
  end
@@ -13,19 +13,21 @@ module Line
13
13
 
14
14
  def message(text, label:, image_url: nil, &)
15
15
  action(
16
- Actions::Message.new(text, label: label, &),
16
+ Actions::Message.new(text, context: context, 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, label: label, display_text: display_text, &),
23
+ Actions::Postback.new(data, context: context, label: label, display_text: display_text, &),
24
24
  image_url
25
25
  )
26
26
  end
27
27
 
28
- def to_h
28
+ private
29
+
30
+ def to_api
29
31
  {
30
32
  items: @items.map do |item, image_url|
31
33
  {
@@ -37,7 +39,17 @@ module Line
37
39
  }
38
40
  end
39
41
 
40
- private
42
+ def to_sdkv2
43
+ {
44
+ items: @items.map do |item, image_url|
45
+ {
46
+ type: "action",
47
+ image_url: image_url,
48
+ action: item.to_h
49
+ }
50
+ end
51
+ }
52
+ end
41
53
 
42
54
  def action(action, image_url)
43
55
  @items << [action, image_url]
@@ -13,7 +13,9 @@ module Line
13
13
  super(context: context, **options, &block)
14
14
  end
15
15
 
16
- def to_h
16
+ private
17
+
18
+ def to_api
17
19
  {
18
20
  type: "text",
19
21
  text: @text,
@@ -21,6 +23,15 @@ module Line
21
23
  quickReply: @quick_reply&.to_h
22
24
  }.compact
23
25
  end
26
+
27
+ def to_sdkv2
28
+ {
29
+ type: "text",
30
+ text: @text,
31
+ quote_token: quote_token,
32
+ quick_reply: @quick_reply&.to_h
33
+ }.compact
34
+ end
24
35
  end
25
36
  end
26
37
  end
@@ -3,7 +3,7 @@
3
3
  module Line
4
4
  module Message
5
5
  module Builder
6
- VERSION = "0.7.0"
6
+ VERSION = "0.9.0"
7
7
  end
8
8
  end
9
9
  end
@@ -4,10 +4,23 @@ require_relative "builder/version"
4
4
 
5
5
  module Line
6
6
  module Message
7
- # The Builder module provides a DSL for building LINE messages.
7
+ # The Builder module provides a Domain Specific Language (DSL) for constructing
8
+ # and validating LINE messages. It offers a structured and intuitive way to
9
+ # define various message types, such as text, flex messages, and quick replies,
10
+ # ensuring they adhere to the LINE Messaging API specifications.
11
+ #
12
+ # This module simplifies the process of creating complex message structures
13
+ # by providing a set of builder classes and helper methods.
8
14
  module Builder
15
+ # Base error class for all errors raised by the Line::Message::Builder module.
16
+ # This allows for rescuing any specific builder error or all builder errors.
9
17
  class Error < StandardError; end
18
+ # Error raised when a required attribute or element is missing during message construction.
19
+ # For example, if a text message is created without specifying the text content.
10
20
  class RequiredError < Error; end
21
+ # Error raised when an attribute or element fails validation rules.
22
+ # For example, if a URL provided for an action is invalid or if a text
23
+ # message exceeds the maximum allowed length.
11
24
  class ValidationError < Error; end
12
25
 
13
26
  require_relative "builder/context"
@@ -21,8 +34,25 @@ module Line
21
34
 
22
35
  module_function
23
36
 
24
- def with(context = nil, &)
25
- Container.new(context: context, &)
37
+ # Entry point for building a message container.
38
+ # This method initializes a new message {Container} and evaluates the
39
+ # provided block within the context of that container.
40
+ #
41
+ # @param context [Object, nil] An optional context object that can be made
42
+ # available within the builder block. This can be useful for accessing
43
+ # helper methods or data within the block.
44
+ # @param mode [Symbol] The mode to use for building messages. Can be either
45
+ # `:api` (default) for direct LINE Messaging API format or `:sdkv2` for
46
+ # LINE Bot SDK v2 compatible format.
47
+ # @yield [container] The block is yielded with the newly created {Container}
48
+ # instance, allowing you to define the message structure using the DSL.
49
+ # @return [Container] The initialized message container with the defined structure.
50
+ # @example
51
+ # message = Line::Message::Builder.with do
52
+ # text "Hello, world!"
53
+ # end
54
+ def with(context = nil, mode: :api, &)
55
+ Container.new(context: context, mode: mode, &)
26
56
  end
27
57
  end
28
58
  end
@@ -49,6 +49,7 @@ module Line
49
49
  end || @expected.call(content)
50
50
  end
51
51
 
52
+ # Directly check if this is the component we're looking for
52
53
  @expected.call(content)
53
54
  end
54
55
 
@@ -114,6 +115,16 @@ module Line
114
115
  ::RSpec::Matchers::BuiltIn::Include.new({ "url" => url, **options }).matches?(content)
115
116
  end
116
117
  end
118
+
119
+ def have_line_flex_span(text, **options) # rubocop:disable Naming/PredicateName
120
+ options = Utils.stringify_keys!(options, deep: true)
121
+
122
+ HaveFlexComponent.new(expected_desc: "span(#{text.inspect})") do |content|
123
+ next false unless content["type"] == "span"
124
+
125
+ content["text"].match?(text) && ::RSpec::Matchers::BuiltIn::Include.new(options).matches?(content)
126
+ end
127
+ end
117
128
  end
118
129
  end
119
130
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Line
4
+ module Message
5
+ module RSpec
6
+ # :nodoc:
7
+ module Matchers
8
+ def have_line_flex_separator(**options) # rubocop:disable Naming/PredicateName
9
+ options = Utils.stringify_keys!(options, deep: true)
10
+
11
+ HaveFlexComponent.new(expected_desc: "separator(#{options.inspect})") do |content|
12
+ next false unless content["type"] == "separator"
13
+
14
+ options.empty? || ::RSpec::Matchers::BuiltIn::Include.new(options).matches?(content)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -6,3 +6,4 @@ require_relative "matchers/have_quick_reply"
6
6
  require_relative "matchers/have_flex_message"
7
7
  require_relative "matchers/have_flex_bubble"
8
8
  require_relative "matchers/have_flex_component"
9
+ require_relative "matchers/have_flex_separator"
@@ -1,3 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This file serves as the main entry point for RSpec matchers related to the
4
+ # `line-message` gem. When `require "line/message/rspec"` is used in a
5
+ # RSpec test suite, this file is loaded, and it, in turn, loads the
6
+ # necessary files to make the custom matchers available for testing
7
+ # `Line::Message::Builder` objects and their outputs.
8
+ #
9
+ # By requiring this file, developers gain access to specialized matchers
10
+ # designed to simplify the testing of message structures built with
11
+ # `line-message`.
12
+ #
13
+ # @example in `spec_helper.rb` or a specific test file
14
+ # require "line/message/rspec"
15
+ #
16
+ # @see Line::Message::Rspec::Matchers
3
17
  require "line/message/rspec/matchers"