servus 0.4.0 → 0.5.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/lib/generators/servus/event/event_generator.rb +54 -0
- data/lib/generators/servus/event/templates/event.rb.erb +44 -0
- data/lib/generators/servus/event/templates/event_spec.rb.erb +20 -0
- data/lib/servus/config.rb +18 -13
- data/lib/servus/event.rb +235 -0
- data/lib/servus/events/bus.rb +82 -72
- data/lib/servus/events/class_router.rb +40 -0
- data/lib/servus/events/emitter.rb +11 -11
- data/lib/servus/events/invocation.rb +94 -0
- data/lib/servus/events/router.rb +44 -0
- data/lib/servus/railtie.rb +10 -8
- data/lib/servus/support/errors.rb +1 -1
- data/lib/servus/support/logger.rb +5 -3
- data/lib/servus/support/validator.rb +8 -9
- data/lib/servus/testing/matchers.rb +5 -5
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -2
- metadata +9 -7
- data/lib/generators/servus/event_handler/event_handler_generator.rb +0 -59
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +0 -86
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +0 -48
- data/lib/servus/event_handler.rb +0 -290
- data/lib/servus/events/errors.rb +0 -10
data/lib/servus/event_handler.rb
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Servus
|
|
4
|
-
# Base class for event handlers that map events to service invocations.
|
|
5
|
-
#
|
|
6
|
-
# EventHandler classes live in app/events/ and use a declarative DSL to
|
|
7
|
-
# subscribe to events and invoke services in response. Each handler
|
|
8
|
-
# subscribes to a single event via the `handles` method.
|
|
9
|
-
#
|
|
10
|
-
# @example Basic event handler
|
|
11
|
-
# class UserCreatedHandler < Servus::EventHandler
|
|
12
|
-
# handles :user_created
|
|
13
|
-
#
|
|
14
|
-
# invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
15
|
-
# { user_id: payload[:user_id] }
|
|
16
|
-
# end
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# @see Servus::Events::Bus
|
|
20
|
-
# @see Servus::Base
|
|
21
|
-
class EventHandler
|
|
22
|
-
class << self
|
|
23
|
-
# Declares which event this handler subscribes to.
|
|
24
|
-
#
|
|
25
|
-
# This method registers the handler with the event bus and stores
|
|
26
|
-
# the event name for later reference. Each handler can only subscribe
|
|
27
|
-
# to one event.
|
|
28
|
-
#
|
|
29
|
-
# @param event_name [Symbol] the name of the event to handle
|
|
30
|
-
# @return [void]
|
|
31
|
-
# @raise [RuntimeError] if handles is called multiple times
|
|
32
|
-
#
|
|
33
|
-
# @example
|
|
34
|
-
# class UserCreatedHandler < Servus::EventHandler
|
|
35
|
-
# handles :user_created
|
|
36
|
-
# end
|
|
37
|
-
def handles(event_name)
|
|
38
|
-
raise "Handler already subscribed to :#{@event_name}. Cannot subscribe to :#{event_name}" if @event_name
|
|
39
|
-
|
|
40
|
-
@event_name = event_name
|
|
41
|
-
Servus::Events::Bus.register_handler(event_name, self)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Returns the event name this handler is subscribed to.
|
|
45
|
-
#
|
|
46
|
-
# @return [Symbol, nil] the event name or nil if not yet configured
|
|
47
|
-
attr_reader :event_name
|
|
48
|
-
|
|
49
|
-
# Declares a service invocation in response to the event.
|
|
50
|
-
#
|
|
51
|
-
# Multiple invocations can be declared for a single event. Each invocation
|
|
52
|
-
# requires a block that maps the event payload to the service's arguments.
|
|
53
|
-
#
|
|
54
|
-
# @param service_class [Class] the service class to invoke (must inherit from Servus::Base)
|
|
55
|
-
# @param options [Hash] invocation options
|
|
56
|
-
# @option options [Boolean] :async invoke the service asynchronously via call_async
|
|
57
|
-
# @option options [Symbol] :queue the queue name for async jobs
|
|
58
|
-
# @option options [Proc] :if condition that must return true for invocation
|
|
59
|
-
# @option options [Proc] :unless condition that must return false for invocation
|
|
60
|
-
# @yield [payload] block that maps event payload to service arguments
|
|
61
|
-
# @yieldparam payload [Hash] the event payload
|
|
62
|
-
# @yieldreturn [Hash] keyword arguments for the service's initialize method
|
|
63
|
-
# @return [void]
|
|
64
|
-
#
|
|
65
|
-
# @example Basic invocation
|
|
66
|
-
# invoke SendEmail::Service do |payload|
|
|
67
|
-
# { user_id: payload[:user_id], email: payload[:email] }
|
|
68
|
-
# end
|
|
69
|
-
#
|
|
70
|
-
# @example Async invocation with queue
|
|
71
|
-
# invoke SendEmail::Service, async: true, queue: :mailers do |payload|
|
|
72
|
-
# { user_id: payload[:user_id] }
|
|
73
|
-
# end
|
|
74
|
-
#
|
|
75
|
-
# @example Conditional invocation
|
|
76
|
-
# invoke GrantRewards::Service, if: ->(p) { p[:premium] } do |payload|
|
|
77
|
-
# { user_id: payload[:user_id] }
|
|
78
|
-
# end
|
|
79
|
-
def invoke(service_class, options = {}, &block)
|
|
80
|
-
raise ArgumentError, 'Block required for payload mapping' unless block
|
|
81
|
-
|
|
82
|
-
@invocations ||= []
|
|
83
|
-
@invocations << {
|
|
84
|
-
service_class: service_class,
|
|
85
|
-
options: options,
|
|
86
|
-
mapper: block
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Returns all service invocations declared for this handler.
|
|
91
|
-
#
|
|
92
|
-
# @return [Array<Hash>] array of invocation configurations
|
|
93
|
-
def invocations
|
|
94
|
-
@invocations || []
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Defines the JSON schema for validating event payloads.
|
|
98
|
-
#
|
|
99
|
-
# @param payload [Hash, nil] JSON schema for validating event payloads
|
|
100
|
-
# @return [void]
|
|
101
|
-
#
|
|
102
|
-
# @example
|
|
103
|
-
# class UserCreatedHandler < Servus::EventHandler
|
|
104
|
-
# handles :user_created
|
|
105
|
-
#
|
|
106
|
-
# schema payload: {
|
|
107
|
-
# type: 'object',
|
|
108
|
-
# required: ['user_id', 'email'],
|
|
109
|
-
# properties: {
|
|
110
|
-
# user_id: { type: 'integer' },
|
|
111
|
-
# email: { type: 'string', format: 'email' }
|
|
112
|
-
# }
|
|
113
|
-
# }
|
|
114
|
-
# end
|
|
115
|
-
def schema(payload: nil)
|
|
116
|
-
@payload_schema = payload.with_indifferent_access if payload
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Returns the payload schema.
|
|
120
|
-
#
|
|
121
|
-
# @return [Hash, nil] the payload schema or nil if not defined
|
|
122
|
-
# @api private
|
|
123
|
-
attr_reader :payload_schema
|
|
124
|
-
|
|
125
|
-
# Emits the event this handler is subscribed to.
|
|
126
|
-
#
|
|
127
|
-
# Provides a type-safe, discoverable way to emit events from anywhere in
|
|
128
|
-
# the application (controllers, jobs, rake tasks) without creating a service.
|
|
129
|
-
#
|
|
130
|
-
# @param payload [Hash] the event payload
|
|
131
|
-
# @return [void]
|
|
132
|
-
# @raise [RuntimeError] if no event configured via `handles`
|
|
133
|
-
#
|
|
134
|
-
# @example Emit from controller
|
|
135
|
-
# class UsersController
|
|
136
|
-
# def create
|
|
137
|
-
# user = User.create!(params)
|
|
138
|
-
# UserCreatedHandler.emit({ user_id: user.id, email: user.email })
|
|
139
|
-
# redirect_to user
|
|
140
|
-
# end
|
|
141
|
-
# end
|
|
142
|
-
#
|
|
143
|
-
# @example Emit from background job
|
|
144
|
-
# class ProcessDataJob
|
|
145
|
-
# def perform(data_id)
|
|
146
|
-
# result = process_data(data_id)
|
|
147
|
-
# DataProcessedHandler.emit({ data_id: data_id, status: result })
|
|
148
|
-
# end
|
|
149
|
-
# end
|
|
150
|
-
def emit(payload)
|
|
151
|
-
raise 'No event configured. Call handles :event_name first.' unless @event_name
|
|
152
|
-
|
|
153
|
-
Servus::Support::Validator.validate_event_payload!(self, payload)
|
|
154
|
-
|
|
155
|
-
Servus::Events::Bus.emit(@event_name, payload)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Handles an event by invoking all configured services.
|
|
159
|
-
#
|
|
160
|
-
# Iterates through all declared invocations, evaluates conditions,
|
|
161
|
-
# maps the payload to service arguments, and invokes each service.
|
|
162
|
-
#
|
|
163
|
-
# @param payload [Hash] the event payload
|
|
164
|
-
# @return [Array<Servus::Support::Response>] results from all invoked services
|
|
165
|
-
#
|
|
166
|
-
# @example
|
|
167
|
-
# UserCreatedHandler.handle({ user_id: 123, email: 'user@example.com' })
|
|
168
|
-
def handle(payload)
|
|
169
|
-
invocations.map do |invocation|
|
|
170
|
-
next unless should_invoke?(payload, invocation[:options])
|
|
171
|
-
|
|
172
|
-
invoke_service(invocation, payload)
|
|
173
|
-
end.compact
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Validates that all registered handlers subscribe to events that are actually emitted by services.
|
|
177
|
-
#
|
|
178
|
-
# Checks all handlers against all service emissions and raises an error if any
|
|
179
|
-
# handler subscribes to a non-existent event. Helps catch typos and orphaned handlers.
|
|
180
|
-
#
|
|
181
|
-
# Respects the `Servus.config.strict_event_validation` setting - skips validation if false.
|
|
182
|
-
#
|
|
183
|
-
# @return [void]
|
|
184
|
-
# @raise [Servus::Events::OrphanedHandlerError] if a handler subscribes to a non-existent event
|
|
185
|
-
#
|
|
186
|
-
# @example
|
|
187
|
-
# Servus::EventHandler.validate_all_handlers!
|
|
188
|
-
def validate_all_handlers!
|
|
189
|
-
return unless Servus.config.strict_event_validation
|
|
190
|
-
|
|
191
|
-
emitted_events = collect_emitted_events
|
|
192
|
-
orphaned = find_orphaned_handlers(emitted_events)
|
|
193
|
-
|
|
194
|
-
return if orphaned.empty?
|
|
195
|
-
|
|
196
|
-
raise Servus::Events::OrphanedHandlerError,
|
|
197
|
-
"Handler(s) subscribe to non-existent events:\n" \
|
|
198
|
-
"#{orphaned.map { |h| " - #{h[:handler]} subscribes to :#{h[:event]}" }.join("\n")}"
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
private
|
|
202
|
-
|
|
203
|
-
# Collects all event names that are emitted by services.
|
|
204
|
-
#
|
|
205
|
-
# @return [Set<Symbol>] set of all emitted event names
|
|
206
|
-
# @api private
|
|
207
|
-
def collect_emitted_events
|
|
208
|
-
events = Set.new
|
|
209
|
-
|
|
210
|
-
services = ObjectSpace.each_object(Class).select { |klass| klass < Servus::Base }
|
|
211
|
-
|
|
212
|
-
services.each do |service_class|
|
|
213
|
-
service_class.event_emissions.each_value do |emissions|
|
|
214
|
-
emissions.each { |emission| events << emission[:event_name] }
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
events
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Finds handlers that subscribe to events not emitted by any service.
|
|
222
|
-
#
|
|
223
|
-
# @param emitted_events [Set<Symbol>] set of all emitted event names
|
|
224
|
-
# @return [Array<Hash>] array of orphaned handler info
|
|
225
|
-
# @api private
|
|
226
|
-
def find_orphaned_handlers(emitted_events)
|
|
227
|
-
orphaned = []
|
|
228
|
-
|
|
229
|
-
handlers = ObjectSpace.each_object(Class).select { _1 < Servus::EventHandler && _1 != Servus::EventHandler }
|
|
230
|
-
|
|
231
|
-
handlers.each do |handler_class|
|
|
232
|
-
next unless handler_class.event_name
|
|
233
|
-
next if emitted_events.include?(handler_class.event_name)
|
|
234
|
-
|
|
235
|
-
orphaned << { handler: handler_class.name, event: handler_class.event_name }
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
orphaned
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# Invokes a single service with the mapped payload.
|
|
242
|
-
#
|
|
243
|
-
# @param invocation [Hash] the invocation configuration
|
|
244
|
-
# @param payload [Hash] the event payload
|
|
245
|
-
# @return [Servus::Support::Response] the service result
|
|
246
|
-
# @api private
|
|
247
|
-
def invoke_service(invocation, payload)
|
|
248
|
-
use_async = invocation.dig(:options, :async) || false
|
|
249
|
-
|
|
250
|
-
if use_async
|
|
251
|
-
service_kwargs = prepare_call_sync_args(invocation, payload)
|
|
252
|
-
invocation[:service_class].call_async(**service_kwargs)
|
|
253
|
-
else
|
|
254
|
-
service_kwargs = invocation[:mapper].call(payload)
|
|
255
|
-
invocation[:service_class].call(**service_kwargs)
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# Checks if a service should be invoked based on conditions.
|
|
260
|
-
#
|
|
261
|
-
# @param payload [Hash] the event payload
|
|
262
|
-
# @param options [Hash] the invocation options
|
|
263
|
-
# @return [Boolean] true if the service should be invoked
|
|
264
|
-
# @api private
|
|
265
|
-
def should_invoke?(payload, options)
|
|
266
|
-
return false if options[:if] && !options[:if].call(payload)
|
|
267
|
-
return false if options[:unless]&.call(payload)
|
|
268
|
-
|
|
269
|
-
true
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Prepares the service arguments by merging event payload with job options.
|
|
273
|
-
#
|
|
274
|
-
# @param invocation [Hash] the invocation configuration
|
|
275
|
-
# @param payload [Hash] the event payload
|
|
276
|
-
# @return [Hash] combined service arguments
|
|
277
|
-
def prepare_call_sync_args(invocation, payload)
|
|
278
|
-
mapper = invocation[:mapper]
|
|
279
|
-
options = invocation[:options]
|
|
280
|
-
|
|
281
|
-
# Extract service arguments and merge with job options
|
|
282
|
-
job_opts = options.slice(:queue, :wait, :wait_until, :priority, :job_options).compact
|
|
283
|
-
|
|
284
|
-
mapper
|
|
285
|
-
.call(payload)
|
|
286
|
-
.merge(job_opts)
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
end
|
data/lib/servus/events/errors.rb
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Servus
|
|
4
|
-
module Events
|
|
5
|
-
# Raised when an event handler subscribes to an event that no service emits.
|
|
6
|
-
#
|
|
7
|
-
# This helps catch typos in event names and orphaned handlers.
|
|
8
|
-
class OrphanedHandlerError < StandardError; end
|
|
9
|
-
end
|
|
10
|
-
end
|