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.
@@ -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
@@ -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