line-message-builder 0.9.0 → 0.10.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.rubocop.yml +5 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CLAUDE.md +124 -0
  6. data/README.md +24 -16
  7. data/claudekit.json +11 -0
  8. data/docs/rubrics/rdoc.md +169 -0
  9. data/lib/line/message/builder/actions/message.rb +47 -26
  10. data/lib/line/message/builder/actions/postback.rb +56 -26
  11. data/lib/line/message/builder/actions/uri.rb +139 -0
  12. data/lib/line/message/builder/actions.rb +6 -3
  13. data/lib/line/message/builder/base.rb +92 -67
  14. data/lib/line/message/builder/container.rb +55 -60
  15. data/lib/line/message/builder/context.rb +51 -72
  16. data/lib/line/message/builder/flex/actionable.rb +53 -32
  17. data/lib/line/message/builder/flex/box.rb +342 -79
  18. data/lib/line/message/builder/flex/bubble.rb +73 -46
  19. data/lib/line/message/builder/flex/builder.rb +39 -28
  20. data/lib/line/message/builder/flex/button.rb +90 -42
  21. data/lib/line/message/builder/flex/carousel.rb +48 -20
  22. data/lib/line/message/builder/flex/icon.rb +152 -0
  23. data/lib/line/message/builder/flex/image.rb +95 -33
  24. data/lib/line/message/builder/flex/partial.rb +47 -49
  25. data/lib/line/message/builder/flex/position.rb +187 -100
  26. data/lib/line/message/builder/flex/separator.rb +49 -6
  27. data/lib/line/message/builder/flex/size.rb +67 -47
  28. data/lib/line/message/builder/flex/span.rb +106 -32
  29. data/lib/line/message/builder/flex/text.rb +166 -54
  30. data/lib/line/message/builder/flex.rb +26 -23
  31. data/lib/line/message/builder/quick_reply.rb +174 -4
  32. data/lib/line/message/builder/text.rb +70 -3
  33. data/lib/line/message/builder/version.rb +1 -1
  34. data/lib/line/message/builder.rb +14 -11
  35. data/lib/line/message/rspec/matchers/have_flex_bubble.rb +1 -1
  36. data/lib/line/message/rspec/matchers/have_flex_component.rb +16 -6
  37. data/lib/line/message/rspec/matchers/have_flex_message.rb +1 -1
  38. data/lib/line/message/rspec/matchers/have_flex_separator.rb +1 -1
  39. data/lib/line/message/rspec/matchers/have_quick_reply.rb +1 -1
  40. data/lib/line/message/rspec/matchers/have_text_message.rb +1 -1
  41. data/llm.txt +276 -35
  42. data/release-please-config.json +3 -1
  43. metadata +9 -3
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Line
4
+ module Message
5
+ module Builder
6
+ module Flex
7
+ # Represents an "icon" component in a LINE Flex Message.
8
+ #
9
+ # Icons render a small image that decorates the text next to them. They are
10
+ # the standard way to build rating stars, labelled metadata rows, and other
11
+ # icon-and-text pairs.
12
+ #
13
+ # An icon can only be placed in a box whose +layout+ is +:baseline+. Unlike
14
+ # Image, an icon cannot have an action attached to it.
15
+ #
16
+ # == Example: Rating stars beside a score
17
+ #
18
+ # Line::Message::Builder.with do
19
+ # flex alt_text: "Review" do
20
+ # bubble do
21
+ # body do
22
+ # box layout: :baseline do
23
+ # icon "https://example.com/star_on.png", size: :sm
24
+ # icon "https://example.com/star_on.png", size: :sm
25
+ # icon "https://example.com/star_off.png", size: :sm
26
+ # text "3.0", size: :sm, margin: :md
27
+ # end
28
+ # end
29
+ # end
30
+ # end
31
+ # end
32
+ #
33
+ # === Example: Following the reader's font size
34
+ #
35
+ # box layout: :baseline do
36
+ # icon "https://example.com/pin.png", scaling: true
37
+ # text "Taipei", scaling: true
38
+ # end
39
+ #
40
+ # See also:
41
+ # - https://developers.line.biz/en/reference/messaging-api/#icon
42
+ # - Position::Margin for +margin+ property
43
+ # - Position::Offset for offset properties
44
+ # - Size::Shared for common +size+ keywords (e.g., +:sm+, +:xl+)
45
+ class Icon < Line::Message::Builder::Base
46
+ include Position::Margin # Adds `margin` option.
47
+ include Position::Offset # Adds offset options.
48
+ include Size::Shared # Adds `size` option (e.g., :sm, :md, :xl).
49
+
50
+ # The URL of the icon image (must be HTTPS). This is a required attribute.
51
+ attr_reader :url
52
+
53
+ # :method: aspect_ratio
54
+ # :call-seq:
55
+ # aspect_ratio() -> String or nil
56
+ # aspect_ratio(value) -> String
57
+ #
58
+ # Sets or gets the aspect ratio of the icon (width:height).
59
+ #
60
+ # [value]
61
+ # The aspect ratio string (e.g., <code>"1:1"</code>, <code>"2:1"</code>).
62
+ # Default is <code>"1:1"</code>
63
+ option :aspect_ratio, default: nil # API key: aspectRatio
64
+
65
+ # :method: scaling
66
+ # :call-seq:
67
+ # scaling() -> Boolean or nil
68
+ # scaling(value) -> Boolean
69
+ #
70
+ # Sets or gets whether the icon size follows the font size setting of the
71
+ # LINE app.
72
+ #
73
+ # [value]
74
+ # +true+ to scale the icon, +false+ (default) to keep it fixed
75
+ option :scaling, default: nil
76
+
77
+ # Initializes a new Flex Message Icon component.
78
+ #
79
+ # [url]
80
+ # The HTTPS URL of the icon image (required)
81
+ # [context]
82
+ # An optional context for the builder (default: +nil+)
83
+ # [options]
84
+ # A hash of options to set instance variables (e.g., +:size+,
85
+ # +:aspect_ratio+, and options from included modules)
86
+ # [block]
87
+ # An optional block for further configuration
88
+ #
89
+ # Raises RequiredError if +url+ is +nil+ when building the message.
90
+ #
91
+ # == Example
92
+ #
93
+ # Line::Message::Builder::Flex::Icon.new(
94
+ # "https://example.com/star.png",
95
+ # size: :sm
96
+ # )
97
+ def initialize(url, context: nil, **options, &)
98
+ @url = url # The icon URL is mandatory.
99
+
100
+ super(context: context, **options, &)
101
+ end
102
+
103
+ private
104
+
105
+ # :nodoc:
106
+ def to_api # rubocop:disable Metrics/MethodLength
107
+ raise RequiredError, "url is required for an icon component" if url.nil?
108
+
109
+ {
110
+ type: "icon",
111
+ url: url,
112
+ # Position::Margin
113
+ margin: margin,
114
+ # Position::Offset
115
+ position: position,
116
+ offsetTop: offset_top,
117
+ offsetBottom: offset_bottom,
118
+ offsetStart: offset_start,
119
+ offsetEnd: offset_end,
120
+ # Size::Shared
121
+ size: size,
122
+ aspectRatio: aspect_ratio, # From option (maps to API key)
123
+ scaling: scaling # From option
124
+ }.compact
125
+ end
126
+
127
+ # :nodoc:
128
+ def to_sdkv2 # rubocop:disable Metrics/MethodLength
129
+ raise RequiredError, "url is required for an icon component" if url.nil?
130
+
131
+ {
132
+ type: "icon",
133
+ url: url,
134
+ # Position::Margin
135
+ margin: margin,
136
+ # Position::Offset
137
+ position: position,
138
+ offset_top: offset_top,
139
+ offset_bottom: offset_bottom,
140
+ offset_start: offset_start,
141
+ offset_end: offset_end,
142
+ # Size::Shared
143
+ size: size,
144
+ aspect_ratio: aspect_ratio, # From option (maps to API key)
145
+ scaling: scaling # From option
146
+ }.compact
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -8,11 +8,12 @@ module Line
8
8
  #
9
9
  # Images are specified by a URL and can be included in various parts of a
10
10
  # Flex Message, such as a box, a bubble's hero section, etc. They offer
11
- # several properties to control their appearance, including `size`,
12
- # `aspect_ratio`, and `aspect_mode`. An image can also have an
13
- # {Actionable#action action} to make it tappable.
11
+ # several properties to control their appearance, including +size+,
12
+ # +aspect_ratio+, +aspect_mode+ and +background_color+. An image can also
13
+ # have an action to make it tappable.
14
+ #
15
+ # == Example
14
16
  #
15
- # @example Creating an image component within a box
16
17
  # Line::Message::Builder.with do
17
18
  # flex alt_text: "Image Example" do
18
19
  # bubble do
@@ -27,14 +28,27 @@ module Line
27
28
  # end
28
29
  # end
29
30
  #
30
- # @see https://developers.line.biz/en/reference/messaging-api/#image
31
- # @see Actionable For making the image tappable.
32
- # @see Position::Horizontal For `align` property.
33
- # @see Position::Vertical For `gravity` property.
34
- # @see Position::Margin For `margin` property.
35
- # @see Position::Offset For offset properties.
36
- # @see Size::Flex For `flex` sizing property.
37
- # @see Size::Image For `size` (image specific keywords), `aspect_ratio`, `aspect_mode`.
31
+ # === Example: Playing an animated image on a tinted backdrop
32
+ #
33
+ # Line::Message::Builder.with do
34
+ # flex alt_text: "Image Example" do
35
+ # bubble do
36
+ # hero_image "https://example.com/loading.png",
37
+ # animated: true,
38
+ # background_color: "#FFFFFF"
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # See also:
44
+ # - https://developers.line.biz/en/reference/messaging-api/#image
45
+ # - Actionable for making the image tappable
46
+ # - Position::Horizontal for +align+ property
47
+ # - Position::Vertical for +gravity+ property
48
+ # - Position::Margin for +margin+ property
49
+ # - Position::Offset for offset properties
50
+ # - Size::Flex for +flex+ sizing property
51
+ # - Size::Image for +size+, +aspect_ratio+, and +aspect_mode+ properties
38
52
  class Image < Line::Message::Builder::Base
39
53
  include Actionable # Enables defining an action for the image.
40
54
  include Position::Horizontal # Adds `align` option for horizontal alignment.
@@ -44,35 +58,77 @@ module Line
44
58
  include Size::Flex # Adds `flex` option for sizing within a parent box.
45
59
  include Size::Image # Adds image-specific sizing options like `size`, `aspect_ratio`, `aspect_mode`.
46
60
 
47
- # @!attribute [r] url
48
- # @return [String] The URL of the image. Must be HTTPS.
49
- # This is a required attribute.
61
+ # The URL of the image (must be HTTPS). This is a required attribute.
50
62
  attr_reader :url
51
63
 
52
- # Specifies the aspect ratio of the image (width:height).
53
- # E.g., "1:1", "16:9", "20:13". Default is "1:1".
54
- # @!method aspect_ratio(value)
55
- # @param value [String, nil] The aspect ratio string.
56
- # @return [String, nil] The current aspect ratio.
64
+ # :method: aspect_ratio
65
+ # :call-seq:
66
+ # aspect_ratio() -> String or nil
67
+ # aspect_ratio(value) -> String
68
+ #
69
+ # Sets or gets the aspect ratio of the image (width:height).
70
+ #
71
+ # [value]
72
+ # The aspect ratio string (e.g., <code>"1:1"</code>, <code>"16:9"</code>,
73
+ # <code>"20:13"</code>). Default is <code>"1:1"</code>.
57
74
  option :aspect_ratio, default: nil
58
75
 
59
- # Specifies how the image should be displayed within the area defined by `aspect_ratio`.
60
- # @!method aspect_mode(value)
61
- # @param value [Symbol, String, nil] Aspect mode. Can be `:cover` (default)
62
- # or `:fit`.
63
- # @return [Symbol, String, nil] The current aspect mode.
76
+ # :method: aspect_mode
77
+ # :call-seq:
78
+ # aspect_mode() -> Symbol or nil
79
+ # aspect_mode(value) -> Symbol
80
+ #
81
+ # Sets or gets how the image should be displayed within the area defined by +aspect_ratio+.
82
+ #
83
+ # [value]
84
+ # The aspect mode (can be +:cover+ (default) or +:fit+)
64
85
  option :aspect_mode, default: nil # :cover, :fit
65
86
 
87
+ # :method: background_color
88
+ # :call-seq:
89
+ # background_color() -> String or nil
90
+ # background_color(value) -> String
91
+ #
92
+ # Sets or gets the background color shown behind the image.
93
+ #
94
+ # [value]
95
+ # Hexadecimal color code (e.g., <code>"#RRGGBB"</code>)
96
+ option :background_color, default: nil # API key: backgroundColor
97
+
98
+ # :method: animated
99
+ # :call-seq:
100
+ # animated() -> Boolean or nil
101
+ # animated(value) -> Boolean
102
+ #
103
+ # Sets or gets whether an animated image (APNG) is played back. A single
104
+ # message may mark at most 10 images as animated, and frames larger than
105
+ # 300 KB are not played.
106
+ #
107
+ # [value]
108
+ # +true+ to play the animation, +false+ (default) to show a still frame
109
+ option :animated, default: nil
110
+
66
111
  # Initializes a new Flex Message Image component.
67
112
  #
68
- # @param url [String] The HTTPS URL of the image. This is required.
69
- # @param context [Object, nil] An optional context for the builder.
70
- # @param options [Hash] A hash of options to set instance variables
71
- # (e.g., `:aspect_ratio`, `:aspect_mode`, `:size`, and options from included modules).
72
- # @param block [Proc, nil] An optional block, typically used to define an
73
- # {Actionable#action action} for the image.
74
- # @raise [ArgumentError] if `url` is nil (though the more specific `RequiredError`
75
- # is raised in `to_h`).
113
+ # [url]
114
+ # The HTTPS URL of the image (required)
115
+ # [context]
116
+ # An optional context for the builder (default: +nil+)
117
+ # [options]
118
+ # A hash of options to set instance variables (e.g., +:aspect_ratio+,
119
+ # +:aspect_mode+, +:size+, and options from included modules)
120
+ # [block]
121
+ # An optional block, typically used to define an action for the image
122
+ #
123
+ # Raises RequiredError if +url+ is +nil+ when building the message.
124
+ #
125
+ # == Example
126
+ #
127
+ # image = Line::Message::Builder::Flex::Image.new(
128
+ # "https://example.com/image.png",
129
+ # aspect_ratio: "16:9",
130
+ # aspect_mode: :cover
131
+ # )
76
132
  def initialize(url, context: nil, **options, &)
77
133
  @url = url # The image URL is mandatory.
78
134
 
@@ -81,6 +137,7 @@ module Line
81
137
 
82
138
  private
83
139
 
140
+ # :nodoc:
84
141
  def to_api # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
85
142
  raise RequiredError, "url is required for an image component" if url.nil?
86
143
 
@@ -104,11 +161,14 @@ module Line
104
161
  size: size,
105
162
  aspectRatio: aspect_ratio, # From option
106
163
  aspectMode: aspect_mode, # From option
164
+ backgroundColor: background_color, # From option (maps to API key)
165
+ animated: animated, # From option
107
166
  # Actionable
108
167
  action: action&.to_h # From Actionable module
109
168
  }.compact
110
169
  end
111
170
 
171
+ # :nodoc:
112
172
  def to_sdkv2 # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
113
173
  raise RequiredError, "url is required for an image component" if url.nil?
114
174
 
@@ -132,6 +192,8 @@ module Line
132
192
  size: size,
133
193
  aspect_ratio: aspect_ratio, # From option
134
194
  aspect_mode: aspect_mode, # From option
195
+ background_color: background_color, # From option (maps to API key)
196
+ animated: animated, # From option
135
197
  # Actionable
136
198
  action: action&.to_h # From Actionable module
137
199
  }.compact
@@ -4,19 +4,20 @@ module Line
4
4
  module Message
5
5
  module Builder
6
6
  module Flex
7
- # The `HasPartial` module provides functionality for Flex Message components
8
- # (like {Box}, {Bubble}) to render reusable "partials". Partials are defined
9
- # by creating classes that inherit from {Partial}.
7
+ # The HasPartial module provides functionality for Flex Message components
8
+ # (like Box, Bubble) to render reusable "partials". Partials are defined
9
+ # by creating classes that inherit from Partial.
10
10
  #
11
- # Including this module into a component class gives it the {#partial!} method.
11
+ # Including this module into a component class gives it the +partial!+ method.
12
+ #
13
+ # == Example
12
14
  #
13
- # @example Defining and using a partial
14
15
  # # Define a reusable partial
15
16
  # class MyButtonPartial < Line::Message::Builder::Flex::Partial
16
17
  # def call
17
- # # `button` method is available from the context (e.g., a Box)
18
+ # # +button+ method is available from the context (e.g., a Box)
18
19
  # button style: :primary do
19
- # # `data` and `label` are available from `context.assigns`
20
+ # # +data+ and +label+ are available from <code>context.assigns</code>
20
21
  # postback data, label: label
21
22
  # end
22
23
  # end
@@ -33,20 +34,22 @@ module Line
33
34
  # end
34
35
  # end
35
36
  #
36
- # @see Partial
37
+ # See also:
38
+ # - Partial
37
39
  module HasPartial
38
- # Renders a given {Partial} class within the current component's context.
40
+ # Renders a given Partial class within the current component's context.
41
+ #
42
+ # This method temporarily makes the provided +assigns+ available to the
43
+ # partial through the <code>context.assigns</code> mechanism. After the partial's
44
+ # +call+ method completes, the original <code>context.assigns</code> are restored.
39
45
  #
40
- # This method temporarily makes the provided `assigns` available to the
41
- # partial through the `context.assigns` mechanism. After the partial's
42
- # `call` method completes, the original `context.assigns` are restored.
46
+ # [partial_class]
47
+ # The class of the partial to render. Must be a subclass of Partial.
48
+ # [assigns]
49
+ # A hash of key-value pairs that will be made available as +assigns+
50
+ # within the partial's +call+ method.
43
51
  #
44
- # @param partial_class [Class] The class of the partial to render. Must be a
45
- # subclass of {Partial}.
46
- # @param assigns [Hash] A hash of key-value pairs that will be made
47
- # available as `assigns` within the partial's `call` method.
48
- # @return [void]
49
- # @raise [ArgumentError] if `partial_class` is not a subclass of {Partial}.
52
+ # Raises ArgumentError if +partial_class+ is not a subclass of Partial.
50
53
  def partial!(partial_class, **assigns)
51
54
  unless partial_class < Partial
52
55
  raise ArgumentError,
@@ -62,60 +65,55 @@ module Line
62
65
  end
63
66
  end
64
67
 
65
- # The `Partial` class is an abstract base for creating reusable snippets
68
+ # The Partial class is an abstract base for creating reusable snippets
66
69
  # of Flex Message content. To define a partial, create a new class that
67
- # inherits from `Partial` and implements the {#call} instance method.
70
+ # inherits from Partial and implements the +call+ instance method.
68
71
  #
69
- # Within the `call` method, you can use the standard Flex Message builder
70
- # DSL methods (e.g., `text`, `box`, `button`) as if you were directly inside
72
+ # Within the +call+ method, you can use the standard Flex Message builder
73
+ # DSL methods (e.g., +text+, +box+, +button+) as if you were directly inside
71
74
  # the component where the partial is being rendered. This is possible because
72
- # the `Partial` instance delegates unknown method calls to the component
73
- # instance that is rendering it (passed as `context` during initialization).
75
+ # the Partial instance delegates unknown method calls to the component
76
+ # instance that is rendering it (passed as +context+ during initialization).
74
77
  #
75
- # @abstract Subclass and implement {#call} to create a concrete partial.
76
- # @see HasPartial For how to render partials.
78
+ # Subclass and implement +call+ to create a concrete partial.
79
+ #
80
+ # See also:
81
+ # - HasPartial for how to render partials
77
82
  class Partial
78
83
  # Initializes a new Partial instance.
79
- # This is typically called by the {HasPartial#partial!} method.
84
+ # This is typically called by the HasPartial#partial! method.
80
85
  #
81
- # @param context [Object] The component instance (e.g., {Box}, {Bubble})
82
- # within which this partial is being rendered. This context provides
83
- # the DSL methods (like `text`, `box`) used in the partial's `call` method.
86
+ # [context]
87
+ # The component instance (e.g., Box, Bubble) within which this partial
88
+ # is being rendered. This context provides the DSL methods (like +text+,
89
+ # +box+) used in the partial's +call+ method.
84
90
  def initialize(context:)
85
91
  @context = context # The component (e.g., Box, Bubble) rendering this partial
86
92
  end
87
93
 
88
94
  # This method must be implemented by subclasses to define the content of
89
95
  # the partial. Inside this method, use the Flex Message builder DSL methods
90
- # (e.g., `text "Hello"`, `button { ... }`) which will be delegated to the
91
- # rendering context.
96
+ # (e.g., <code>text "Hello"</code>, <code>button { ... }</code>) which will be
97
+ # delegated to the rendering context.
98
+ #
99
+ # Any arguments passed to +partial!+ as +assigns+ are available via
100
+ # <code>context.assigns</code> or directly if the rendering context's +method_missing+
101
+ # handles +assigns+ lookup (as Line::Message::Builder::Context does).
92
102
  #
93
- # Any arguments passed to `partial!` as `assigns` are available via
94
- # `context.assigns` or directly if the rendering context's `method_missing`
95
- # handles `assigns` lookup (as {Line::Message::Builder::Context} does).
103
+ # Arguments passed from the +partial!+ call's +assigns+ can be explicitly defined
104
+ # as parameters here, or accessed via <code>context.assigns</code>.
96
105
  #
97
- # @param ... [Object] Arguments passed from the `partial!` call's `assigns`
98
- # can be explicitly defined as parameters here, or accessed via `context.assigns`.
99
- # @raise [NotImplementedError] if a subclass does not implement this method.
106
+ # Raises NotImplementedError if a subclass does not implement this method.
100
107
  def call(*)
101
108
  raise NotImplementedError,
102
109
  "The #{self.class.name} class must implement the #call method to define its content."
103
110
  end
104
111
 
105
- # @!visibility private
106
- # Part of Ruby's dynamic method dispatch. It's overridden here to declare
107
- # that instances of `Partial` can respond to methods to which the
108
- # wrapped `@context` object responds.
109
- def respond_to_missing?(method_name, include_private = false)
112
+ def respond_to_missing?(method_name, include_private = false) # :nodoc:
110
113
  @context.respond_to?(method_name, include_private) || super
111
114
  end
112
115
 
113
- # @!visibility private
114
- # Handles calls to methods not explicitly defined on the `Partial` class
115
- # by delegating them to the wrapped `@context` object (the rendering component).
116
- # This allows the `call` method of a partial to use DSL methods like `text`,
117
- # `box`, `button`, etc., as if they were defined directly in the partial.
118
- def method_missing(method_name, ...)
116
+ def method_missing(method_name, ...) # :nodoc:
119
117
  if @context.respond_to?(method_name)
120
118
  @context.public_send(method_name, ...)
121
119
  else