line-message-builder 0.8.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 (45) 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 +41 -0
  5. data/CLAUDE.md +124 -0
  6. data/CONVENTIONS.md +36 -0
  7. data/README.md +61 -43
  8. data/claudekit.json +11 -0
  9. data/docs/rubrics/rdoc.md +169 -0
  10. data/lib/line/message/builder/actions/message.rb +52 -31
  11. data/lib/line/message/builder/actions/postback.rb +63 -33
  12. data/lib/line/message/builder/actions/uri.rb +139 -0
  13. data/lib/line/message/builder/actions.rb +6 -3
  14. data/lib/line/message/builder/base.rb +92 -67
  15. data/lib/line/message/builder/container.rb +64 -69
  16. data/lib/line/message/builder/context.rb +55 -80
  17. data/lib/line/message/builder/flex/actionable.rb +53 -32
  18. data/lib/line/message/builder/flex/box.rb +366 -79
  19. data/lib/line/message/builder/flex/bubble.rb +78 -51
  20. data/lib/line/message/builder/flex/builder.rb +47 -36
  21. data/lib/line/message/builder/flex/button.rb +96 -48
  22. data/lib/line/message/builder/flex/carousel.rb +57 -29
  23. data/lib/line/message/builder/flex/icon.rb +152 -0
  24. data/lib/line/message/builder/flex/image.rb +103 -42
  25. data/lib/line/message/builder/flex/partial.rb +58 -55
  26. data/lib/line/message/builder/flex/position.rb +187 -100
  27. data/lib/line/message/builder/flex/separator.rb +84 -0
  28. data/lib/line/message/builder/flex/size.rb +67 -47
  29. data/lib/line/message/builder/flex/span.rb +184 -0
  30. data/lib/line/message/builder/flex/text.rb +210 -54
  31. data/lib/line/message/builder/flex.rb +31 -26
  32. data/lib/line/message/builder/quick_reply.rb +174 -4
  33. data/lib/line/message/builder/text.rb +70 -3
  34. data/lib/line/message/builder/version.rb +1 -1
  35. data/lib/line/message/builder.rb +16 -13
  36. data/lib/line/message/rspec/matchers/have_flex_bubble.rb +1 -1
  37. data/lib/line/message/rspec/matchers/have_flex_component.rb +26 -5
  38. data/lib/line/message/rspec/matchers/have_flex_message.rb +1 -1
  39. data/lib/line/message/rspec/matchers/have_flex_separator.rb +20 -0
  40. data/lib/line/message/rspec/matchers/have_quick_reply.rb +1 -1
  41. data/lib/line/message/rspec/matchers/have_text_message.rb +1 -1
  42. data/lib/line/message/rspec/matchers.rb +1 -0
  43. data/llm.txt +367 -45
  44. data/release-please-config.json +3 -1
  45. metadata +12 -3
@@ -3,53 +3,49 @@
3
3
  module Line
4
4
  module Message
5
5
  module Builder
6
- # The `Base` class serves as the foundation for all message builder classes
7
- # within the `Line::Message::Builder` DSL. It provides core functionality
6
+ # The Base class serves as the foundation for all message builder classes
7
+ # within the Line::Message::Builder DSL. It provides core functionality
8
8
  # for defining options, handling initialization, and delegating method calls
9
9
  # to a context object.
10
10
  #
11
11
  # This class is not typically used directly but is inherited by specific
12
- # message type builders (e.g., `Text`, `Flex`).
12
+ # message type builders (e.g., Text, Flex::Builder).
13
13
  class Base
14
14
  class << self
15
- # @!visibility private
16
- # @!parse extend ClassMethods
17
- def inherited(subclass)
15
+ def inherited(subclass) # :nodoc:
18
16
  super
19
17
  subclass.extend ClassMethods
20
18
  end
21
19
  end
22
20
 
23
- # The `ClassMethods` module is automatically extended by any class that
24
- # inherits from {Base}. It provides class-level methods for defining
21
+ # The ClassMethods module is automatically extended by any class that
22
+ # inherits from Base. It provides class-level methods for defining
25
23
  # DSL options and configurations.
26
24
  module ClassMethods
27
- # Returns an array of option names that have been defined for this class
28
- # using the {option} method.
29
- #
30
- # @return [Array<Symbol>] A list of defined option names.
31
- # @!visibility private
32
- def options
25
+ def options # :nodoc:
33
26
  @options ||= []
34
27
  end
35
28
 
36
29
  # Defines a new option for the builder class.
30
+ #
37
31
  # This method dynamically creates an instance method on the builder class
38
- # with the given `name`. When this instance method is called:
32
+ # with the given name. When this instance method is called:
39
33
  # - Without arguments, it returns the current value of the option, or
40
- # the `default` value if not set.
41
- # - With an argument, it sets the value of the option. If a `validator`
34
+ # the default value if not set.
35
+ # - With an argument, it sets the value of the option. If a validator
42
36
  # is provided, the value is validated before being set.
43
37
  #
44
- # @param name [Symbol] The name of the option to define. This will also
45
- # be the name of the generated instance method.
46
- # @param default [Object, nil] The default value for the option if not
47
- # explicitly set.
48
- # @param validator [#valid!?, nil] An optional validator object that must
49
- # respond to `valid!(value)`. If the value is invalid, the validator
50
- # should raise an error (typically a {ValidationError}).
51
- # @return [void]
52
- # @example
38
+ # [name]
39
+ # The name of the option to define. This will also be the name of the
40
+ # generated instance method.
41
+ # [default]
42
+ # The default value for the option if not explicitly set.
43
+ # [validator]
44
+ # An optional validator object that must respond to +valid!+ method.
45
+ # If the value is invalid, the validator should raise an error.
46
+ #
47
+ # == Example
48
+ #
53
49
  # class MyBuilder < Base
54
50
  # option :color, default: "red"
55
51
  # option :size, validator: SizeValidator.new
@@ -74,24 +70,39 @@ module Line
74
70
  end
75
71
  end
76
72
 
73
+ # The context object used for method delegation.
74
+ #
75
+ # When methods are called on the builder that it doesn't respond to,
76
+ # they are delegated to this context object if it responds to them.
77
+ # This allows accessing helper methods and variables from the surrounding
78
+ # environment within the builder DSL.
77
79
  attr_reader :context
78
80
 
79
81
  # Initializes a new instance of a builder class.
80
82
  #
81
- # @param context [Object, nil] An optional external context object.
82
- # Methods not defined in the builder will be delegated to this context
83
- # if it responds to them. This allows for using helper methods or
84
- # accessing data from the surrounding environment within the builder DSL.
85
- # @param options [Hash] A hash of options to set on the builder instance.
86
- # These options are typically defined using the {ClassMethods.option .option}
87
- # method in the builder class.
88
- # @param block [Proc, nil] An optional block that is instance-eval'd
89
- # within the new builder instance. This is the primary way the DSL
90
- # is used to define message content.
83
+ # [context]
84
+ # An optional external context object. Methods not defined in the builder
85
+ # will be delegated to this context if it responds to them. This allows
86
+ # for using helper methods or accessing data from the surrounding environment
87
+ # within the builder DSL.
88
+ # [options]
89
+ # A hash of options to set on the builder instance. These options are
90
+ # typically defined using the +option+ method in the builder class.
91
+ # [block]
92
+ # An optional block that is instance-eval'd within the new builder instance.
93
+ # This is the primary way the DSL is used to define message content.
94
+ #
95
+ # == Example
96
+ #
97
+ # builder = MyBuilder.new(context: view_context) do
98
+ # text "Hello from context"
99
+ # end
91
100
  def initialize(context: nil, **options, &block)
92
101
  @context = context
93
102
  @quick_reply = nil
94
103
 
104
+ validate_options!(options)
105
+
95
106
  self.class.options.each do |option|
96
107
  send(option, options[option]) if options.key?(option)
97
108
  end
@@ -100,15 +111,17 @@ module Line
100
111
  end
101
112
 
102
113
  # Defines a quick reply for the message.
114
+ #
103
115
  # A quick reply consists of a set of buttons that are displayed along
104
116
  # with the message, allowing users to make quick responses.
105
117
  #
106
- # The provided block is executed in the context of a new {QuickReply}
118
+ # The provided block is executed in the context of a new QuickReply
107
119
  # instance, where you can define the quick reply buttons.
108
120
  #
109
- # @yield [quick_reply] The block is yielded with a {QuickReply} instance.
110
- # @return [QuickReply] The created {QuickReply} object.
111
- # @example
121
+ # :yields: quick_reply
122
+ #
123
+ # == Example
124
+ #
112
125
  # text_message do
113
126
  # text "Please choose an option:"
114
127
  # quick_reply do
@@ -120,34 +133,11 @@ module Line
120
133
  @quick_reply = QuickReply.new(context: context, &)
121
134
  end
122
135
 
123
- # Determines if the builder can respond to a given method, including
124
- # checking if the context object can respond to it.
125
- # This is part of Ruby's mechanism for `method_missing` and is used
126
- # here to enable delegation to the `context` object.
127
- #
128
- # @param method_name [Symbol] The name of the method.
129
- # @param include_private [Boolean] Whether to include private methods
130
- # in the search.
131
- # @return [Boolean] `true` if the builder or its context can respond to
132
- # the method, `false` otherwise.
133
- # @!visibility private
134
- def respond_to_missing?(method_name, include_private = false)
136
+ def respond_to_missing?(method_name, include_private = false) # :nodoc:
135
137
  context.respond_to?(method_name, include_private) || super
136
138
  end
137
139
 
138
- # Handles calls to undefined methods by attempting to delegate them to the
139
- # `context` object. If the `context` object responds to the method,
140
- # it is called. Otherwise, it behaves like the standard `method_missing`.
141
- # This allows the DSL to feel more integrated with the surrounding code
142
- # by making methods from the `context` directly available within the
143
- # builder block.
144
- #
145
- # @param method_name [Symbol] The name of the missing method.
146
- # @param ... [Object] Arguments passed to the method.
147
- # @raise [NoMethodError] If neither the builder nor the context can
148
- # handle the method call.
149
- # @!visibility private
150
- def method_missing(method_name, ...)
140
+ def method_missing(method_name, ...) # :nodoc:
151
141
  if context.respond_to?(method_name)
152
142
  context.public_send(method_name, ...)
153
143
  else
@@ -155,6 +145,20 @@ module Line
155
145
  end
156
146
  end
157
147
 
148
+ # Converts the builder to a hash representation.
149
+ #
150
+ # The format of the hash depends on the mode (API or SDK v2) determined
151
+ # by the builder's configuration. Subclasses must implement +to_api+ and
152
+ # +to_sdkv2+ methods to provide the actual conversion logic.
153
+ #
154
+ # [return]
155
+ # A hash representation of the message in the appropriate format.
156
+ #
157
+ # == Example
158
+ #
159
+ # builder = Text.new { text "Hello" }
160
+ # builder.to_h
161
+ # # => { type: "text", text: "Hello" }
158
162
  def to_h
159
163
  return to_sdkv2 if sdkv2?
160
164
 
@@ -163,11 +167,32 @@ module Line
163
167
 
164
168
  private
165
169
 
166
- def to_api
170
+ # Rejects options the builder does not declare.
171
+ #
172
+ # An undeclared option is almost always a typo or a property the LINE API
173
+ # spells differently. Ignoring it produces a message that is silently
174
+ # missing the styling that was asked for, which nothing downstream can
175
+ # detect, so it is refused here instead.
176
+ #
177
+ # [options]
178
+ # The hash of options passed to the constructor
179
+ #
180
+ # Raises ValidationError listing the unknown option and the ones this
181
+ # builder accepts.
182
+ def validate_options!(options) # :nodoc:
183
+ unknown = options.keys - self.class.options
184
+ return if unknown.empty?
185
+
186
+ raise ValidationError,
187
+ "Unknown option: #{unknown.join(", ")} for #{self.class}. " \
188
+ "Allowed options are: #{self.class.options.join(", ")}"
189
+ end
190
+
191
+ def to_api # :nodoc:
167
192
  raise NotImplementedError, "#{self.class} must implement #to_api"
168
193
  end
169
194
 
170
- def to_sdkv2
195
+ def to_sdkv2 # :nodoc:
171
196
  raise NotImplementedError, "#{self.class} must implement #to_sdkv2"
172
197
  end
173
198
  end
@@ -3,53 +3,58 @@
3
3
  module Line
4
4
  module Message
5
5
  module Builder
6
- # The `Container` class is the top-level entry point for constructing a batch
6
+ # The +Container+ class is the top-level entry point for constructing a batch
7
7
  # of LINE messages using the builder DSL. It acts as a holder for one or
8
- # more individual message objects (such as {Text} or {Flex} messages).
8
+ # more individual message objects (such as Text or Flex messages).
9
9
  #
10
- # When you use `Line::Message::Builder.with {}`, you are operating within
11
- # the context of a `Container` instance. This container allows you to define
10
+ # When you use <code>Line::Message::Builder.with {}</code>, you are operating within
11
+ # the context of a +Container+ instance. This container allows you to define
12
12
  # multiple messages that can be sent together in a single API call to LINE,
13
13
  # although the LINE API typically expects an array of message objects,
14
14
  # which this container helps to build.
15
15
  #
16
16
  # Each message added to the container can also have its own quick reply.
17
17
  #
18
- # @example Building multiple messages
19
- # message_payload = Line::Message::Builder.with do |root|
20
- # root.text "Hello, this is the first message!"
21
- # root.flex alt_text: "This is a Flex Message" do |flex_builder|
22
- # flex_builder.bubble do |bubble|
23
- # bubble.body do |body|
18
+ # == Example
19
+ #
20
+ # message_payload = Line::Message::Builder.with do
21
+ # text "Hello, this is the first message!"
22
+ # flex alt_text: "This is a Flex Message" do
23
+ # bubble do
24
+ # body do
24
25
  # body.text "This is a Flex Message body."
25
26
  # end
26
27
  # end
27
28
  # end
28
29
  # end.build # => Returns an array of message hashes
29
30
  #
30
- # @see Base
31
- # @see Text
32
- # @see Flex::Builder
33
- # @see QuickReply
31
+ # See also:
32
+ # - Base
33
+ # - Text
34
+ # - Flex::Builder
35
+ # - QuickReply
34
36
  class Container
35
- # @!attribute [r] context
36
- # @return [Context] The context object, which can hold external data or
37
- # helper methods accessible within the builder blocks.
37
+ # [return]
38
+ # The context object, which can hold external data or
39
+ # helper methods accessible within the builder blocks.
38
40
  attr_reader :context
39
41
 
40
42
  # Initializes a new message container.
41
- # This is typically not called directly but through `Line::Message::Builder.with`.
43
+ # This is typically not called directly but through <code>Line::Message::Builder.with</code>.
42
44
  # The provided block is instance-eval'd, allowing DSL methods like
43
- # {#text} and {#flex} to be called directly on the container instance.
45
+ # #text and #flex to be called directly on the container instance.
44
46
  #
45
- # @param context [Object, nil] An optional context object that can be used
47
+ # [context]
48
+ # An optional context object that can be used
46
49
  # to share data or helper methods within the builder block. It's wrapped
47
- # in a {Context} object.
48
- # @param mode [Symbol] The mode to use for building messages. Can be either
49
- # `:api` (default) for direct LINE Messaging API format or `:sdkv2` for
50
+ # in a Context object.
51
+ # [mode]
52
+ # The mode to use for building messages. Can be either
53
+ # <code>:api</code> (default) for direct LINE Messaging API format or <code>:sdkv2</code> for
50
54
  # LINE Bot SDK v2 compatible format.
51
- # @param block [Proc] A block containing DSL calls to define messages
52
- # (e.g., `text "Hello"`, `flex { ... }`).
55
+ # [block]
56
+ # A block containing DSL calls to define messages
57
+ # (e.g., <code>text "Hello"</code>, <code>flex { ... }</code>).
53
58
  def initialize(context: nil, mode: :api, &block)
54
59
  @messages = [] # Initializes an empty array to store message objects
55
60
  @context = Context.new(context, mode:)
@@ -57,17 +62,20 @@ module Line
57
62
  instance_eval(&block) if ::Kernel.block_given?
58
63
  end
59
64
 
60
- # Creates a new {Text} message and adds it to this container.
65
+ # Creates a new Text message and adds it to this container.
61
66
  #
62
- # @param text [String] The text content of the message.
63
- # @param options [Hash] Additional options for the text message,
64
- # such as `:quick_reply`. See {Text#initialize}.
65
- # @param block [Proc, nil] An optional block that will be instance-eval'd
66
- # in the context of the new {Text} message instance. This can be used
67
+ # [text]
68
+ # The text content of the message.
69
+ # [option]
70
+ # Additional options for the text message,
71
+ # such as <code>:quick_reply</code>. See Text#initialize.
72
+ # [block]
73
+ # An optional block that will be instance-eval'd
74
+ # in the context of the new Text message instance. This can be used
67
75
  # to add a quick reply to the text message.
68
- # @return [Text] The newly created {Text} message object.
69
76
  #
70
- # @example
77
+ # == Example
78
+ #
71
79
  # root.text "Hello, world!" do
72
80
  # quick_reply do
73
81
  # button action: :message, label: "Hi!", text: "Hi!"
@@ -77,24 +85,27 @@ module Line
77
85
  @messages << Text.new(text, context: context, **options, &)
78
86
  end
79
87
 
80
- # Creates a new {Flex::Builder} for constructing a Flex Message and adds it
88
+ # Creates a new Flex::Builder for constructing a Flex Message and adds it
81
89
  # to this container. The block is mandatory and is used to define the
82
90
  # content of the Flex Message using the Flex Message DSL.
83
91
  #
84
- # @param options [Hash] Options for the Flex Message, primarily `:alt_text`.
85
- # See {Flex::Builder#initialize}. It's important to provide `alt_text`.
86
- # @param block [Proc] A block that will be instance-eval'd in the context
87
- # of the new {Flex::Builder} instance. This block is used to define the
92
+ # [option]
93
+ # Options for the Flex Message, primarily <code>:alt_text</code>.
94
+ # See Flex::Builder#initialize. It's important to provide +alt_text+.
95
+ # [block]
96
+ # A block that will be instance-eval'd in the context
97
+ # of the new Flex::Builder instance. This block is used to define the
88
98
  # structure and content of the Flex Message (e.g., bubbles, carousels).
89
- # @return [Flex::Builder] The newly created {Flex::Builder} object.
90
- # @raise [ArgumentError] if `alt_text` is not provided in options (validation
91
- # is typically within Flex::Builder).
92
99
  #
93
- # @example
94
- # root.flex alt_text: "Important information" do |fb|
95
- # fb.bubble do |bubble|
96
- # bubble.header { |h| h.text "Header" }
97
- # bubble.body { |b| b.text "Body" }
100
+ # Raises ArgumentError if +alt_text+ is not provided in options (validation
101
+ # is typically within Flex::Builder).
102
+ #
103
+ # == Example
104
+ #
105
+ # flex alt_text: "Important information" do
106
+ # bubble do
107
+ # header { text "Header" }
108
+ # body { text "Body" }
98
109
  # end
99
110
  # end
100
111
  def flex(**options, &)
@@ -102,47 +113,31 @@ module Line
102
113
  end
103
114
 
104
115
  # Converts all messages held by this container into their hash representations.
105
- # This method iterates over each message object (e.g., {Text}, {Flex::Builder})
106
- # stored in the container and calls its `to_h` method. The result is an
116
+ # This method iterates over each message object (e.g., Text, Flex::Builder)
117
+ # stored in the container and calls its +to_h+ method. The result is an
107
118
  # array of hashes, where each hash represents a single LINE message object
108
119
  # ready for JSON serialization and sending to the LINE Messaging API.
109
- #
110
- # @return [Array<Hash>] An array of message objects, each represented as a hash.
111
- # This is the format expected by the LINE API for the `messages` field
112
- # in a request body.
113
120
  def build
114
121
  @messages.map(&:to_h)
115
122
  end
116
123
 
117
- # Converts the array of message hashes (obtained from {#build}) into a
124
+ # Converts the array of message hashes (obtained from #build) into a
118
125
  # JSON string. This is a convenience method for serializing the messages
119
126
  # payload.
120
127
  #
121
- # @param args [Object] Optional arguments that are passed along to `to_json`
128
+ # [arg]
129
+ # Optional arguments that are passed along to +to_json+
122
130
  # method of the underlying array.
123
- # @return [String] A JSON string representing the array of message objects.
124
131
  def to_json(*args)
125
132
  build.to_json(*args)
126
133
  end
127
134
 
128
- # Checks if a method is defined in the context object.
129
- # This is part of Ruby's method_missing mechanism.
130
- #
131
- # @param method_name [Symbol] The name of the method being checked
132
- # @param include_private [Boolean] Whether to include private methods
133
- # @return [Boolean] True if the method exists in the context, false otherwise
135
+ # :nodoc:
134
136
  def respond_to_missing?(method_name, include_private = false)
135
137
  context.respond_to?(method_name, include_private) || super
136
138
  end
137
139
 
138
- # Delegates method calls to the context object if they exist there.
139
- # This allows helper methods defined in the context to be called directly
140
- # from within the builder DSL.
141
- #
142
- # @param method_name [Symbol] The name of the method being called
143
- # @param args [Array] The arguments passed to the method
144
- # @return [Object] The result of calling the method on the context
145
- # @raise [NoMethodError] If the method doesn't exist in the context
140
+ # :nodoc:
146
141
  def method_missing(method_name, ...)
147
142
  return context.send(method_name, ...) if context.respond_to?(method_name)
148
143
 
@@ -3,120 +3,93 @@
3
3
  module Line
4
4
  module Message
5
5
  module Builder
6
- # The `Context` class is a crucial component of the `Line::Message::Builder`
6
+ # The Context class is a crucial component of the Line::Message::Builder
7
7
  # DSL, enabling a flexible and dynamic environment for message construction.
8
8
  # It acts as a wrapper around an optional user-provided context object and
9
- # manages a separate hash of "assigns" (local variables for the DSL).
9
+ # manages a separate hash of +assigns+ (local variables for the DSL).
10
10
  #
11
- # The primary purposes of the `Context` are:
12
- # 1. **To provide access to helper methods**: If a user passes a context object
11
+ # The primary purposes of the Context are:
12
+ # 1. To provide access to helper methods: If a user passes a context object
13
13
  # (e.g., a Rails view context, a presenter, or any custom object) during
14
- # builder initialization (`Line::Message::Builder.with(my_helper_object)`),
15
- # methods defined on `my_helper_object` become directly callable within
16
- # the DSL blocks.
17
- # 2. **To allow local data storage (`assigns`)**: The `assigns` hash allows
18
- # for setting and retrieving temporary data that can be shared across
19
- # different parts of a complex message construction block, without needing
20
- # to pass it explicitly or pollute the user-provided context.
14
+ # builder initialization, methods defined on that object become directly
15
+ # callable within the DSL blocks.
16
+ # 2. To allow local data storage: The +assigns+ hash allows for setting and
17
+ # retrieving temporary data that can be shared across different parts of
18
+ # a complex message construction block, without needing to pass it
19
+ # explicitly or pollute the user-provided context.
21
20
  #
22
21
  # Method calls within the DSL that are not defined on the builder objects
23
- # themselves are resolved by `Context` in the following order:
24
- # - First, it checks if the method name corresponds to a key in the `assigns` hash.
25
- # - If not found in `assigns`, it checks if the wrapped user-context object
22
+ # themselves are resolved by Context in the following order:
23
+ # - First, it checks if the method name corresponds to a key in the +assigns+ hash.
24
+ # - If not found in +assigns+, it checks if the wrapped user-context object
26
25
  # responds to the method.
27
26
  # - If neither, the call proceeds up the normal Ruby method lookup chain.
28
27
  #
29
28
  # This mechanism allows for a clean and powerful way to integrate external logic
30
29
  # and data into the message building process.
31
30
  #
32
- # @example Using a custom context
33
- # class MyHelpers
31
+ # == Example: Using a custom context
32
+ #
33
+ # class MyContext
34
34
  # def current_user_name
35
35
  # "Alice"
36
36
  # end
37
37
  # end
38
38
  #
39
- # helpers = MyHelpers.new
40
- # Line::Message::Builder.with(helpers) do |root|
41
- # # `current_user_name` is resolved from `helpers` by Context
42
- # root.text "Hello, #{current_user_name}!"
43
- #
44
- # # Using assigns
45
- # assigns[:item_count] = 5
46
- # root.text "You have #{assigns[:item_count]} items."
39
+ # context = MyContext.new
40
+ # Line::Message::Builder.with(context) do
41
+ # # current_user_name is resolved from context by Context
42
+ # text "Hello, #{current_user_name}!"
47
43
  # end
48
44
  class Context
49
- # @!attribute assigns
50
- # A hash for storing arbitrary data that can be accessed within the
51
- # builder DSL. This is useful for temporary variables or shared state
52
- # during message construction.
53
- # @return [Hash] The hash of assigned values.
54
- # @example
55
- # context.assigns[:user_id] = 123
56
- # puts context.assigns[:user_id] # => 123
45
+ # A hash for storing arbitrary data that can be accessed within the
46
+ # builder DSL. This is useful for temporary variables or shared state
47
+ # during message construction.
48
+ #
49
+ # == Example
50
+ #
51
+ # context.assigns[:user_id] = 123
52
+ # puts context.assigns[:user_id] # => 123
57
53
  attr_accessor :assigns
58
54
 
59
- # @!attribute [r] mode
60
- # The mode in which the builder is operating. This affects how messages
61
- # are formatted in the final output.
62
- # @return [Symbol] Either `:api` for direct LINE Messaging API format
63
- # or `:sdkv2` for LINE Bot SDK v2 compatible format.
55
+ # The mode in which the builder is operating. This affects how messages
56
+ # are formatted in the final output. Either +:api+ for direct LINE
57
+ # Messaging API format or +:sdkv2+ for LINE Bot SDK v2 compatible format.
64
58
  attr_reader :mode
65
59
 
66
60
  # Initializes a new Context object.
67
61
  #
68
- # @param context [Object, nil] An optional object whose methods will be made
69
- # available within the DSL. If `nil`, only `assigns` and standard
70
- # builder methods will be available.
71
- # @param mode [Symbol] The mode of the context, which can be `:api` (default)
72
- # for direct LINE Messaging API format or `:sdkv2` for LINE Bot SDK v2
73
- # compatible format.
62
+ # [context]
63
+ # An optional object whose methods will be made available within the DSL.
64
+ # If +nil+, only +assigns+ and standard builder methods will be available.
65
+ # [mode]
66
+ # The mode of the context, which can be +:api+ (default) for direct LINE
67
+ # Messaging API format or +:sdkv2+ for LINE Bot SDK v2 compatible format.
68
+ #
69
+ # == Example
70
+ #
71
+ # # With a custom context object
72
+ # my_context = MyHelpers.new
73
+ # context = Line::Message::Builder::Context.new(my_context, mode: :api)
74
+ #
75
+ # === Example: Without context
76
+ #
77
+ # # Without context, using only assigns
78
+ # context = Line::Message::Builder::Context.new(nil)
79
+ # context.assigns[:user_name] = "Alice"
74
80
  def initialize(context, mode: :api)
75
81
  @context = context
76
82
  @assigns = {}
77
83
  @mode = mode
78
84
  end
79
85
 
80
- # Part of Ruby's dynamic method dispatch. It's overridden here to declare
81
- # that instances of `Context` can respond to methods that are either:
82
- # 1. Keys in the `@assigns` hash.
83
- # 2. Methods to which the wrapped `@context` object responds.
84
- #
85
- # This ensures that `respond_to?` behaves consistently with how
86
- # `method_missing` resolves method calls.
87
- #
88
- # @param method_name [Symbol] The name of the method being queried.
89
- # @param include_private [Boolean] Whether to include private methods in
90
- # the check.
91
- # @return [Boolean] `true` if the context can handle the method,
92
- # `false` otherwise.
93
- # @!visibility private
94
- def respond_to_missing?(method_name, include_private = false)
86
+ def respond_to_missing?(method_name, include_private = false) # :nodoc:
95
87
  @assigns.key?(method_name) ||
96
88
  @context.respond_to?(method_name, include_private) || # Check @context directly
97
89
  super
98
90
  end
99
91
 
100
- # Handles calls to methods not explicitly defined on the `Context` class.
101
- # The resolution order is:
102
- # 1. If `method_name` is a key in the `@assigns` hash, its value is returned.
103
- # 2. If the wrapped `@context` object responds to `method_name`, the call
104
- # is delegated to `@context`.
105
- # 3. Otherwise, `super` is called, allowing the standard Ruby method
106
- # lookup to continue (which will likely result in a `NoMethodError`
107
- # if the method is truly undefined).
108
- #
109
- # This is the core mechanism that allows DSL blocks to seamlessly access
110
- # data from `assigns` or methods from the user-provided context.
111
- #
112
- # @param method_name [Symbol] The name of the invoked method.
113
- # @param ... [Object] Arguments passed to the method.
114
- # @return [Object, nil] The value from `@assigns`, the result of the
115
- # delegated call to `@context`, or raises `NoMethodError` via `super`.
116
- # @raise [NoMethodError] If the method is not found in `assigns` or
117
- # on the wrapped context.
118
- # @!visibility private
119
- def method_missing(method_name, ...)
92
+ def method_missing(method_name, ...) # :nodoc:
120
93
  return @assigns[method_name] if @assigns.key?(method_name)
121
94
  # Check @context directly
122
95
  return @context.public_send(method_name, ...) if @context.respond_to?(method_name)
@@ -126,8 +99,10 @@ module Line
126
99
 
127
100
  # Checks if the current mode is set to SDK v2 compatibility.
128
101
  #
129
- # @return [Boolean] `true` if the mode is `:sdkv2`, `false` otherwise.
130
- # @example
102
+ # Returns +true+ if the mode is +:sdkv2+, +false+ otherwise.
103
+ #
104
+ # == Example
105
+ #
131
106
  # if context.sdkv2?
132
107
  # # Format message for LINE Bot SDK v2
133
108
  # else