22
33module Hooks
44 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 )
66 #
77 # This module provides a consistent interface for accessing global components
88 # across all plugin types, eliminating code duplication and ensuring consistent
99 # behavior throughout the application.
1010 #
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+ #
1114 # @example Usage in a class that needs instance methods
1215 # class MyHandler
1316 # include Hooks::Core::ComponentAccess
@@ -28,6 +31,33 @@ module Core
2831 # stats.increment("requests.validated")
2932 # end
3033 # 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
3161 module ComponentAccess
3262 # Short logger accessor
3363 # @return [Hooks::Log] Logger instance for logging messages
@@ -64,6 +94,130 @@ def stats
6494 def failbot
6595 Hooks ::Core ::GlobalComponents . failbot
6696 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
67221 end
68222 end
69223end
0 commit comments