2
2
3
3
module Hooks
4
4
module Core
5
- # Shared module providing access to global components (logger, stats, failbot)
5
+ # Shared module providing access to global components (logger, stats, failbot, and user-defined components )
6
6
#
7
7
# This module provides a consistent interface for accessing global components
8
8
# across all plugin types, eliminating code duplication and ensuring consistent
9
9
# behavior throughout the application.
10
10
#
11
+ # In addition to built-in components (log, stats, failbot), this module provides
12
+ # dynamic access to any user-defined components passed to Hooks.build().
13
+ #
11
14
# @example Usage in a class that needs instance methods
12
15
# class MyHandler
13
16
# include Hooks::Core::ComponentAccess
@@ -28,6 +31,33 @@ module Core
28
31
# stats.increment("requests.validated")
29
32
# end
30
33
# end
34
+ #
35
+ # @example Using user-defined components
36
+ # # Application setup
37
+ # publisher = KafkaPublisher.new
38
+ # email_service = EmailService.new
39
+ # app = Hooks.build(
40
+ # config: "config.yaml",
41
+ # publisher: publisher,
42
+ # email_service: email_service
43
+ # )
44
+ #
45
+ # # Handler implementation
46
+ # class WebhookHandler < Hooks::Plugins::Handlers::Base
47
+ # include Hooks::Core::ComponentAccess
48
+ #
49
+ # def call(payload:, headers:, env:, config:)
50
+ # # Use built-in components
51
+ # log.info("Processing webhook")
52
+ # stats.increment("webhooks.received")
53
+ #
54
+ # # Use user-defined components
55
+ # publisher.send_message(payload, topic: "webhooks")
56
+ # email_service.send_notification(payload['email'], "Webhook processed")
57
+ #
58
+ # { status: "success" }
59
+ # end
60
+ # end
31
61
module ComponentAccess
32
62
# Short logger accessor
33
63
# @return [Hooks::Log] Logger instance for logging messages
@@ -64,6 +94,130 @@ def stats
64
94
def failbot
65
95
Hooks ::Core ::GlobalComponents . failbot
66
96
end
97
+
98
+ # Dynamic method access for user-defined components
99
+ #
100
+ # This method enables handlers to call user-defined components as methods.
101
+ # For example, if a user registers a 'publisher' component, handlers can
102
+ # call `publisher` or `publisher.some_method` directly.
103
+ #
104
+ # The method supports multiple usage patterns:
105
+ # - Direct access: Returns the component instance for further method calls
106
+ # - Callable access: If the component responds to #call, invokes it with provided arguments
107
+ # - Method chaining: Allows fluent interface patterns with registered components
108
+ #
109
+ # @param method_name [Symbol] The method name being called
110
+ # @param args [Array] Arguments passed to the method
111
+ # @param kwargs [Hash] Keyword arguments passed to the method
112
+ # @param block [Proc] Block passed to the method
113
+ # @return [Object] The user component or result of method call
114
+ # @raise [NoMethodError] If component doesn't exist and no super method available
115
+ #
116
+ # @example Accessing a publisher component directly
117
+ # # Given: Hooks.build(publisher: MyKafkaPublisher.new)
118
+ # class MyHandler < Hooks::Plugins::Handlers::Base
119
+ # def call(payload:, headers:, env:, config:)
120
+ # publisher.send_message(payload, topic: "webhooks")
121
+ # { status: "published" }
122
+ # end
123
+ # end
124
+ #
125
+ # @example Using a callable component (Proc/Lambda)
126
+ # # Given: Hooks.build(notifier: ->(msg) { puts "Notification: #{msg}" })
127
+ # class MyHandler < Hooks::Plugins::Handlers::Base
128
+ # def call(payload:, headers:, env:, config:)
129
+ # notifier.call("New webhook received")
130
+ # # Or use the shorthand syntax:
131
+ # notifier("Processing webhook for #{payload['user_id']}")
132
+ # { status: "notified" }
133
+ # end
134
+ # end
135
+ #
136
+ # @example Using a service object
137
+ # # Given: Hooks.build(email_service: EmailService.new(api_key: "..."))
138
+ # class MyHandler < Hooks::Plugins::Handlers::Base
139
+ # def call(payload:, headers:, env:, config:)
140
+ # email_service.send_notification(
141
+ # to: payload['email'],
142
+ # subject: "Webhook Processed",
143
+ # body: "Your webhook has been successfully processed"
144
+ # )
145
+ # { status: "email_sent" }
146
+ # end
147
+ # end
148
+ #
149
+ # @example Passing blocks to components
150
+ # # Given: Hooks.build(batch_processor: BatchProcessor.new)
151
+ # class MyHandler < Hooks::Plugins::Handlers::Base
152
+ # def call(payload:, headers:, env:, config:)
153
+ # batch_processor.process_with_callback(payload) do |result|
154
+ # log.info("Batch processing completed: #{result}")
155
+ # end
156
+ # { status: "batch_queued" }
157
+ # end
158
+ # end
159
+ def method_missing ( method_name , *args , **kwargs , &block )
160
+ component = Hooks ::Core ::GlobalComponents . get_extra_component ( method_name )
161
+
162
+ if component
163
+ # If called with arguments or block, try to call the component as a method
164
+ if args . any? || kwargs . any? || block
165
+ component . call ( *args , **kwargs , &block )
166
+ else
167
+ # Otherwise return the component itself
168
+ component
169
+ end
170
+ else
171
+ # Fall back to normal method_missing behavior
172
+ super
173
+ end
174
+ end
175
+
176
+ # Respond to user-defined component names
177
+ #
178
+ # This method ensures that handlers properly respond to user-defined component
179
+ # names, enabling proper method introspection and duck typing support.
180
+ #
181
+ # @param method_name [Symbol] The method name being checked
182
+ # @param include_private [Boolean] Whether to include private methods
183
+ # @return [Boolean] True if method exists or is a user component
184
+ #
185
+ # @example Checking if a component is available
186
+ # class MyHandler < Hooks::Plugins::Handlers::Base
187
+ # def call(payload:, headers:, env:, config:)
188
+ # if respond_to?(:publisher)
189
+ # publisher.send_message(payload)
190
+ # { status: "published" }
191
+ # else
192
+ # log.warn("Publisher not available, skipping message send")
193
+ # { status: "skipped" }
194
+ # end
195
+ # end
196
+ # end
197
+ #
198
+ # @example Conditional component usage
199
+ # class MyHandler < Hooks::Plugins::Handlers::Base
200
+ # def call(payload:, headers:, env:, config:)
201
+ # results = { status: "processed" }
202
+ #
203
+ # # Only use analytics if available
204
+ # if respond_to?(:analytics)
205
+ # analytics.track_event("webhook_processed", payload)
206
+ # results[:analytics] = "tracked"
207
+ # end
208
+ #
209
+ # # Only send notifications if notifier is available
210
+ # if respond_to?(:notifier)
211
+ # notifier.call("Webhook processed: #{payload['id']}")
212
+ # results[:notification] = "sent"
213
+ # end
214
+ #
215
+ # results
216
+ # end
217
+ # end
218
+ def respond_to_missing? ( method_name , include_private = false )
219
+ Hooks ::Core ::GlobalComponents . extra_component_exists? ( method_name ) || super
220
+ end
67
221
end
68
222
end
69
223
end
0 commit comments