hooks-ruby 0.5.1 → 0.6.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.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/config.ru +31 -1
- data/lib/hooks/core/builder.rb +6 -1
- data/lib/hooks/core/component_access.rb +155 -1
- data/lib/hooks/core/global_components.rb +56 -12
- data/lib/hooks/version.rb +1 -1
- data/lib/hooks.rb +3 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 001f3c92a2c07552f4293bc9b5d078e720a6d3ee601ffee80ca83ef1cf9425a5
|
4
|
+
data.tar.gz: 51e7307ad34ea50049dcc2f148e7650626f332854f9f4e409fdf461f0c699547
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d36cbea00db3a9752a4acd20732bcb3c61072fb64593345a3b947110fbe29fbeb2b21afe47fb8821546679f003276561ba63caf79eba15838b9a185ea062afc6
|
7
|
+
data.tar.gz: 2c7252c823b0e004a9877fb4c30c57e51ac2322fcf8e503485da526ad504f3345c3aaf08db37736ba9b04df653bf5fe901b6070e9bb92e0aa14d76f9f7a00f22
|
data/README.md
CHANGED
@@ -140,6 +140,8 @@ Congratulations! You have successfully set up a basic Hooks server which will li
|
|
140
140
|
|
141
141
|
Keep reading to learn how to customize your Hooks server with different plugins for handlers, authentication, and more.
|
142
142
|
|
143
|
+
For an in-depth flow diagram of how the Hooks server processes incoming requests, see the [Architecture Flow](docs/architecture_flow.md) documentation.
|
144
|
+
|
143
145
|
### Advanced
|
144
146
|
|
145
147
|
This section will go into a more advanced and detailed example of how to setup a Hooks server with custom plugins, authentication, and more. This section also assumes you already have the `hooks-ruby` gem installed via a bundler Gemfile as shown in the [Installation](#installation-) section above.
|
data/config.ru
CHANGED
@@ -1,6 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# An example file that is a part of the acceptance tests for the Hooks framework.
|
4
|
+
# This can be used as a reference point as it is a working implementation of a Hooks application.
|
5
|
+
|
3
6
|
require_relative "lib/hooks"
|
4
7
|
|
5
|
-
|
8
|
+
# Example publisher class that simulates publishing messages
|
9
|
+
# This class could be literally anything and it is used here to demonstrate how to pass in custom kwargs...
|
10
|
+
# ... to the Hooks application which later become available in Handlers throughout the application.
|
11
|
+
class ExamplePublisher
|
12
|
+
def initialize
|
13
|
+
@published_messages = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(data)
|
17
|
+
@published_messages << data
|
18
|
+
puts "Published: #{data.inspect}"
|
19
|
+
"Message published successfully"
|
20
|
+
end
|
21
|
+
|
22
|
+
def publish(data)
|
23
|
+
call(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def messages
|
27
|
+
@published_messages
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create publisher instance
|
32
|
+
publisher = ExamplePublisher.new
|
33
|
+
|
34
|
+
# Create and run the hooks application with custom publisher
|
35
|
+
app = Hooks.build(config: "./spec/acceptance/config/hooks.yaml", publisher:)
|
6
36
|
run app
|
data/lib/hooks/core/builder.rb
CHANGED
@@ -15,9 +15,11 @@ module Hooks
|
|
15
15
|
#
|
16
16
|
# @param config [String, Hash] Path to config file or config hash
|
17
17
|
# @param log [Logger] Custom logger instance
|
18
|
-
|
18
|
+
# @param **extra_components [Hash] Arbitrary user-defined components to make available to handlers
|
19
|
+
def initialize(config: nil, log: nil, **extra_components)
|
19
20
|
@log = log
|
20
21
|
@config_input = config
|
22
|
+
@extra_components = extra_components
|
21
23
|
end
|
22
24
|
|
23
25
|
# Build and return Rack-compatible application
|
@@ -37,6 +39,9 @@ module Hooks
|
|
37
39
|
|
38
40
|
Hooks::Log.instance = @log
|
39
41
|
|
42
|
+
# Register user-defined components globally
|
43
|
+
Hooks::Core::GlobalComponents.register_extra_components(@extra_components)
|
44
|
+
|
40
45
|
# Hydrate our Retryable instance
|
41
46
|
Retry.setup!(log: @log)
|
42
47
|
|
@@ -2,12 +2,15 @@
|
|
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 Hooks
|
|
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 @@ module Hooks
|
|
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
|
@@ -1,11 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "monitor"
|
4
|
+
|
3
5
|
module Hooks
|
4
6
|
module Core
|
5
7
|
# Global registry for shared components accessible throughout the application
|
6
8
|
class GlobalComponents
|
7
9
|
@test_stats = nil
|
8
10
|
@test_failbot = nil
|
11
|
+
@extra_components = {}
|
12
|
+
@mutex = Monitor.new
|
13
|
+
|
14
|
+
# Register arbitrary user-defined components. This method is called on application startup
|
15
|
+
#
|
16
|
+
# @param components [Hash] Hash of component name => component instance
|
17
|
+
# @return [void]
|
18
|
+
def self.register_extra_components(components)
|
19
|
+
@mutex.synchronize do
|
20
|
+
@extra_components = components.dup.freeze
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get a user-defined component by name
|
25
|
+
#
|
26
|
+
# @param name [Symbol, String] Component name
|
27
|
+
# @return [Object, nil] Component instance or nil if not found
|
28
|
+
def self.get_extra_component(name)
|
29
|
+
@extra_components[name.to_sym] || @extra_components[name.to_s]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get all registered user component names
|
33
|
+
#
|
34
|
+
# @return [Array<Symbol>] Array of component names
|
35
|
+
def self.extra_component_names
|
36
|
+
@extra_components.keys.map(&:to_sym)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if a user component exists
|
40
|
+
#
|
41
|
+
# @param name [Symbol, String] Component name
|
42
|
+
# @return [Boolean] True if component exists
|
43
|
+
def self.extra_component_exists?(name)
|
44
|
+
@extra_components.key?(name.to_sym) || @extra_components.key?(name.to_s)
|
45
|
+
end
|
9
46
|
|
10
47
|
# Get the global stats instance
|
11
48
|
# @return [Hooks::Plugins::Instruments::StatsBase] Stats instance for metrics reporting
|
@@ -22,29 +59,36 @@ module Hooks
|
|
22
59
|
# Set a custom stats instance (for testing)
|
23
60
|
# @param stats_instance [Object] Custom stats instance
|
24
61
|
def self.stats=(stats_instance)
|
25
|
-
@
|
62
|
+
@mutex.synchronize do
|
63
|
+
@test_stats = stats_instance
|
64
|
+
end
|
26
65
|
end
|
27
66
|
|
28
67
|
# Set a custom failbot instance (for testing)
|
29
68
|
# @param failbot_instance [Object] Custom failbot instance
|
30
69
|
def self.failbot=(failbot_instance)
|
31
|
-
@
|
70
|
+
@mutex.synchronize do
|
71
|
+
@test_failbot = failbot_instance
|
72
|
+
end
|
32
73
|
end
|
33
74
|
|
34
75
|
# Reset components to default instances (for testing)
|
35
76
|
#
|
36
77
|
# @return [void]
|
37
78
|
def self.reset
|
38
|
-
@
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
79
|
+
@mutex.synchronize do
|
80
|
+
@test_stats = nil
|
81
|
+
@test_failbot = nil
|
82
|
+
@extra_components = {}.freeze
|
83
|
+
# Clear and reload default instruments
|
84
|
+
PluginLoader.clear_plugins
|
85
|
+
require_relative "../plugins/instruments/stats"
|
86
|
+
require_relative "../plugins/instruments/failbot"
|
87
|
+
PluginLoader.instance_variable_set(:@instrument_plugins, {
|
88
|
+
stats: Hooks::Plugins::Instruments::Stats.new,
|
89
|
+
failbot: Hooks::Plugins::Instruments::Failbot.new
|
90
|
+
})
|
91
|
+
end
|
48
92
|
end
|
49
93
|
end
|
50
94
|
end
|
data/lib/hooks/version.rb
CHANGED
data/lib/hooks.rb
CHANGED
@@ -32,11 +32,13 @@ module Hooks
|
|
32
32
|
#
|
33
33
|
# @param config [String, Hash] Path to config file or config hash
|
34
34
|
# @param log [Logger] Custom logger instance (optional)
|
35
|
+
# @param **extra_components [Hash] Arbitrary user-defined components to make available to handlers
|
35
36
|
# @return [Object] Rack-compatible application
|
36
|
-
def self.build(config: nil, log: nil)
|
37
|
+
def self.build(config: nil, log: nil, **extra_components)
|
37
38
|
Core::Builder.new(
|
38
39
|
config:,
|
39
40
|
log:,
|
41
|
+
**extra_components
|
40
42
|
).build
|
41
43
|
end
|
42
44
|
end
|