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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f382ed3d92dd577c93965ae207c00d28cac6dfddf452cf22fb5dafcd69d56eb
|
|
4
|
+
data.tar.gz: f5a4394a33f5a3061d3c5746fdd058eb457cb29baa2f0adb9cc97c0679f5c158
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ccb1adc1b0bd4b7ff9f8b754571be541bbf7e3b9246746fe73c9e1259ad4b6f6e3a5a4d6c07d3b9f30cb8ddda94dfc2cad7c78fb6dc31fb17440c1d3a19b6be
|
|
7
|
+
data.tar.gz: 0107e7e4d770913a5e86de5c496791e6311c913dc6b2bdad03417b78a2e66be6d50c2fc5d213f87b51162ac4a15d3c6c9dcb9b323267a9db9d0b222099f60245
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servus
|
|
4
|
+
module Generators
|
|
5
|
+
# Rails generator for creating Servus event classes.
|
|
6
|
+
#
|
|
7
|
+
# Generates an event class and spec file. The event name is inferred
|
|
8
|
+
# from the class name — no explicit +event_name+ call needed.
|
|
9
|
+
#
|
|
10
|
+
# @example Generate an event
|
|
11
|
+
# rails g servus:event referral_verified
|
|
12
|
+
#
|
|
13
|
+
# @example Generated files
|
|
14
|
+
# app/events/referral_verified.rb
|
|
15
|
+
# spec/app/events/referral_verified_spec.rb
|
|
16
|
+
#
|
|
17
|
+
# @see https://guides.rubyonrails.org/generators.html
|
|
18
|
+
class EventGenerator < Rails::Generators::NamedBase
|
|
19
|
+
source_root File.expand_path('templates', __dir__)
|
|
20
|
+
|
|
21
|
+
class_option :no_docs, type: :boolean,
|
|
22
|
+
default: false,
|
|
23
|
+
desc: 'Skip documentation comments in generated files'
|
|
24
|
+
|
|
25
|
+
# Creates the event class and spec files.
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def create_event_file
|
|
29
|
+
template 'event.rb.erb', event_path
|
|
30
|
+
template 'event_spec.rb.erb', event_spec_path
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# @return [String] event file path
|
|
36
|
+
# @api private
|
|
37
|
+
def event_path
|
|
38
|
+
File.join(Servus.config.events_dir, "#{file_name}_event.rb")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String] spec file path
|
|
42
|
+
# @api private
|
|
43
|
+
def event_spec_path
|
|
44
|
+
File.join(Servus.config.tests_dir, Servus.config.events_dir, "#{file_name}_event_spec.rb")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String] event class name (e.g. "ReferralVerifiedEvent")
|
|
48
|
+
# @api private
|
|
49
|
+
def event_class_name
|
|
50
|
+
"#{class_name}Event"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<%- unless options[:no_docs] -%>
|
|
4
|
+
# Defines the :<%= file_name %> event.
|
|
5
|
+
#
|
|
6
|
+
# Event classes declare the contract (schema) for an event and optionally
|
|
7
|
+
# wire up service invocations that run when the event fires. The event
|
|
8
|
+
# name is inferred from the class name.
|
|
9
|
+
#
|
|
10
|
+
# @example Emit this event from anywhere
|
|
11
|
+
# <%= event_class_name %>.emit({ user_id: 123 })
|
|
12
|
+
#
|
|
13
|
+
# @example Invoke a service when this event fires
|
|
14
|
+
# invoke SendEmail::Service, async: true do |payload|
|
|
15
|
+
# { user_id: payload[:user_id] }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Pass full payload through (no mapper block)
|
|
19
|
+
# invoke AuditLogger::Service, async: true
|
|
20
|
+
#
|
|
21
|
+
# @example Conditional invocation
|
|
22
|
+
# invoke GrantRewards::Service, if: ->(payload) { payload[:premium] } do |payload|
|
|
23
|
+
# { user_id: payload[:user_id] }
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Available options for `invoke`:
|
|
27
|
+
# - async: true - Invoke service asynchronously via ActiveJob
|
|
28
|
+
# - queue: :queue_name - Specify ActiveJob queue (requires async: true)
|
|
29
|
+
# - if: ->(payload) {} - Condition that must be true to invoke
|
|
30
|
+
# - unless: ->(payload) {} - Condition that must be false to invoke
|
|
31
|
+
#
|
|
32
|
+
# @see Servus::Event
|
|
33
|
+
# @see Servus::Events::Bus
|
|
34
|
+
<%- end -%>
|
|
35
|
+
class <%= event_class_name %> < Servus::Event
|
|
36
|
+
schema payload: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
description: '<%= event_class_name %> event payload',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# invoke YourService, async: true do |payload|
|
|
42
|
+
# { example_arg: payload[:example_field] }
|
|
43
|
+
# end
|
|
44
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe <%= event_class_name %> do
|
|
6
|
+
let(:payload) do
|
|
7
|
+
{
|
|
8
|
+
# TODO: Add sample payload fields
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
<%- unless options[:no_docs] -%>
|
|
13
|
+
# TODO: Add tests for service invocations
|
|
14
|
+
# it 'invokes YourService with mapped arguments' do
|
|
15
|
+
# expect { described_class.emit(payload) }
|
|
16
|
+
# .to emit_event(:<%= file_name %>)
|
|
17
|
+
# .with(hash_including(expected_field: 'value'))
|
|
18
|
+
# end
|
|
19
|
+
<%- end -%>
|
|
20
|
+
end
|
data/lib/servus/config.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Servus
|
|
|
22
22
|
# @return [String] the schemas directory path
|
|
23
23
|
attr_accessor :schemas_dir
|
|
24
24
|
|
|
25
|
-
# The directory where
|
|
25
|
+
# The directory where Event classes are located.
|
|
26
26
|
#
|
|
27
27
|
# Defaults to `Rails.root/app/events` in Rails applications.
|
|
28
28
|
#
|
|
@@ -36,14 +36,6 @@ module Servus
|
|
|
36
36
|
# @return [String] the services directory path
|
|
37
37
|
attr_accessor :services_dir
|
|
38
38
|
|
|
39
|
-
# Whether to validate that all event handlers subscribe to events that are actually emitted by services.
|
|
40
|
-
#
|
|
41
|
-
# When enabled, raises an error on boot if handlers subscribe to non-existent events.
|
|
42
|
-
# Helps catch typos and orphaned handlers.
|
|
43
|
-
#
|
|
44
|
-
# @return [Boolean] true to validate, false to skip validation
|
|
45
|
-
attr_accessor :strict_event_validation
|
|
46
|
-
|
|
47
39
|
# The directory where guard classes are located.
|
|
48
40
|
#
|
|
49
41
|
# Defaults to `Rails.root/app/guards` in Rails applications.
|
|
@@ -82,14 +74,28 @@ module Servus
|
|
|
82
74
|
# @return [Boolean] true to require result schemas, false to allow schema-less services
|
|
83
75
|
attr_accessor :require_service_result_schema
|
|
84
76
|
|
|
85
|
-
# Whether to require all event
|
|
77
|
+
# Whether to require all event classes to define a payload schema.
|
|
86
78
|
#
|
|
87
79
|
# When enabled, raises {Servus::Support::Errors::SchemaRequiredError} when
|
|
88
|
-
# an event
|
|
80
|
+
# an event validates a payload without a payload schema defined.
|
|
89
81
|
#
|
|
90
|
-
# @return [Boolean] true to require payload schemas, false to allow schema-less
|
|
82
|
+
# @return [Boolean] true to require payload schemas, false to allow schema-less events
|
|
91
83
|
attr_accessor :require_event_payload_schema
|
|
92
84
|
|
|
85
|
+
# The ordered list of routers that resolve invocations for events.
|
|
86
|
+
#
|
|
87
|
+
# The Bus iterates routers in order, collects invocations, deduplicates
|
|
88
|
+
# by key (first wins), and executes. Defaults to +[ClassRouter.new]+
|
|
89
|
+
# which reads +invoke+ declarations from Event classes.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Servus::Events::Router>]
|
|
92
|
+
attr_writer :routers
|
|
93
|
+
|
|
94
|
+
# @return [Array<Servus::Events::Router>]
|
|
95
|
+
def routers
|
|
96
|
+
@routers || [Servus::Events::ClassRouter.new]
|
|
97
|
+
end
|
|
98
|
+
|
|
93
99
|
# Whether external instantiation of services is blocked and instance
|
|
94
100
|
# `#call` methods are automatically privatized.
|
|
95
101
|
#
|
|
@@ -121,7 +127,6 @@ module Servus
|
|
|
121
127
|
# @api private
|
|
122
128
|
def initialize
|
|
123
129
|
set_default_directories
|
|
124
|
-
@strict_event_validation = true
|
|
125
130
|
@include_default_guards = true
|
|
126
131
|
@lockdown_enabled = true
|
|
127
132
|
@require_service_arguments_schema = false
|
data/lib/servus/event.rb
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servus
|
|
4
|
+
# Base class for event definitions.
|
|
5
|
+
#
|
|
6
|
+
# Event classes live in app/events/ and serve three purposes:
|
|
7
|
+
#
|
|
8
|
+
# 1. *Contract* — declares the event exists and defines its name
|
|
9
|
+
# 2. *Validator* — schema enforcement on any emission
|
|
10
|
+
# 3. *Declarative routing* — optional +invoke+ declarations
|
|
11
|
+
#
|
|
12
|
+
# The event name can be set explicitly via +event_name+ or inferred
|
|
13
|
+
# from the class name (e.g. +OrderPlaced+ becomes +:order_placed+).
|
|
14
|
+
# Call +ensure_registered!+ to trigger inference for classes that
|
|
15
|
+
# don't declare an explicit name.
|
|
16
|
+
#
|
|
17
|
+
# @example Event with explicit name and invoke declarations
|
|
18
|
+
# class UserCreated < Servus::Event
|
|
19
|
+
# event_name :user_created
|
|
20
|
+
#
|
|
21
|
+
# schema payload: { type: 'object', required: ['user_id'] }
|
|
22
|
+
#
|
|
23
|
+
# invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
24
|
+
# { user_id: payload[:user_id] }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Event with inferred name (no invoke — schema-only contract)
|
|
29
|
+
# class OrderPlaced < Servus::Event
|
|
30
|
+
# schema payload: { type: 'object', required: ['order_id'] }
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @example Event that passes full payload through (no mapper block)
|
|
34
|
+
# class AuditLogCreated < Servus::Event
|
|
35
|
+
# event_name :audit_log_created
|
|
36
|
+
#
|
|
37
|
+
# invoke AuditLogger::Service, async: true
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# @see Servus::Events::Bus
|
|
41
|
+
# @see Servus::Events::Router
|
|
42
|
+
# @see Servus::Base
|
|
43
|
+
class Event
|
|
44
|
+
class << self
|
|
45
|
+
# Declares or returns the event name.
|
|
46
|
+
#
|
|
47
|
+
# When called with an argument, sets the event name and registers
|
|
48
|
+
# with the Bus. When called without arguments, returns the current
|
|
49
|
+
# event name.
|
|
50
|
+
#
|
|
51
|
+
# If never called explicitly, use +ensure_registered!+ to infer
|
|
52
|
+
# the name from the class name.
|
|
53
|
+
#
|
|
54
|
+
# @overload event_name(name)
|
|
55
|
+
# @param name [Symbol] the event name to register
|
|
56
|
+
# @return [void]
|
|
57
|
+
# @raise [RuntimeError] if called twice with different names
|
|
58
|
+
#
|
|
59
|
+
# @overload event_name
|
|
60
|
+
# @return [Symbol, nil] the event name or nil if not configured
|
|
61
|
+
#
|
|
62
|
+
# @example Explicit name
|
|
63
|
+
# class UserCreated < Servus::Event
|
|
64
|
+
# event_name :user_created
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# @example Inferred name (via ensure_registered!)
|
|
68
|
+
# class OrderPlaced < Servus::Event; end
|
|
69
|
+
# OrderPlaced.ensure_registered!
|
|
70
|
+
# OrderPlaced.event_name # => :order_placed
|
|
71
|
+
def event_name(name = nil)
|
|
72
|
+
return @event_name if name.nil?
|
|
73
|
+
|
|
74
|
+
raise "Event already subscribed to :#{@event_name}. Cannot subscribe to :#{name}" if @event_name
|
|
75
|
+
|
|
76
|
+
@event_name = name
|
|
77
|
+
Servus::Events::Bus.register_event(name, self)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Infers and registers the event name from the class name if not
|
|
81
|
+
# already set explicitly. Safe to call multiple times — does
|
|
82
|
+
# nothing if already registered. Skips anonymous classes.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def ensure_registered!
|
|
86
|
+
return if @event_name
|
|
87
|
+
return if name.nil?
|
|
88
|
+
|
|
89
|
+
event_name(name.demodulize.underscore.to_sym)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Declares a service invocation in response to the event.
|
|
93
|
+
#
|
|
94
|
+
# Multiple invocations can be declared for a single event. Each invocation
|
|
95
|
+
# requires a block that maps the event payload to the service's arguments.
|
|
96
|
+
#
|
|
97
|
+
# @param service_class [Class] the service class to invoke (must inherit from Servus::Base)
|
|
98
|
+
# @param options [Hash] invocation options
|
|
99
|
+
# @option options [Boolean] :async invoke the service asynchronously via call_async
|
|
100
|
+
# @option options [Symbol] :queue the queue name for async jobs
|
|
101
|
+
# @option options [Proc] :if condition that must return true for invocation
|
|
102
|
+
# @option options [Proc] :unless condition that must return false for invocation
|
|
103
|
+
# @yield [payload] block that maps event payload to service arguments
|
|
104
|
+
# @yieldparam payload [Hash] the event payload
|
|
105
|
+
# @yieldreturn [Hash] keyword arguments for the service's initialize method
|
|
106
|
+
# @return [void]
|
|
107
|
+
#
|
|
108
|
+
# @example Basic invocation
|
|
109
|
+
# invoke SendEmail::Service do |payload|
|
|
110
|
+
# { user_id: payload[:user_id], email: payload[:email] }
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# @example Async invocation with queue
|
|
114
|
+
# invoke SendEmail::Service, async: true, queue: :mailers do |payload|
|
|
115
|
+
# { user_id: payload[:user_id] }
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
118
|
+
# @example Conditional invocation
|
|
119
|
+
# invoke GrantRewards::Service, if: ->(p) { p[:premium] } do |payload|
|
|
120
|
+
# { user_id: payload[:user_id] }
|
|
121
|
+
# end
|
|
122
|
+
def invoke(service_class, options = {}, &block)
|
|
123
|
+
@invocations ||= []
|
|
124
|
+
@invocations << {
|
|
125
|
+
service_class: service_class,
|
|
126
|
+
options: options,
|
|
127
|
+
mapper: block || ->(payload) { payload }
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns all service invocations declared for this event.
|
|
132
|
+
#
|
|
133
|
+
# @return [Array<Hash>] array of invocation configurations
|
|
134
|
+
def invocations
|
|
135
|
+
@invocations || []
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Defines the JSON schema for validating event payloads.
|
|
139
|
+
#
|
|
140
|
+
# @param payload [Hash, nil] JSON schema for validating event payloads
|
|
141
|
+
# @return [void]
|
|
142
|
+
#
|
|
143
|
+
# @example
|
|
144
|
+
# class UserCreated < Servus::Event
|
|
145
|
+
# event_name :user_created
|
|
146
|
+
#
|
|
147
|
+
# schema payload: {
|
|
148
|
+
# type: 'object',
|
|
149
|
+
# required: ['user_id', 'email'],
|
|
150
|
+
# properties: {
|
|
151
|
+
# user_id: { type: 'integer' },
|
|
152
|
+
# email: { type: 'string', format: 'email' }
|
|
153
|
+
# }
|
|
154
|
+
# }
|
|
155
|
+
# end
|
|
156
|
+
def schema(payload: nil)
|
|
157
|
+
@payload_schema = payload.with_indifferent_access if payload
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns the payload schema.
|
|
161
|
+
#
|
|
162
|
+
# @return [Hash, nil] the payload schema or nil if not defined
|
|
163
|
+
# @api private
|
|
164
|
+
attr_reader :payload_schema
|
|
165
|
+
|
|
166
|
+
# Emits this event via the Bus.
|
|
167
|
+
#
|
|
168
|
+
# Provides a type-safe, discoverable way to emit events from anywhere in
|
|
169
|
+
# the application (controllers, jobs, rake tasks) without creating a service.
|
|
170
|
+
#
|
|
171
|
+
# @param payload [Hash] the event payload
|
|
172
|
+
# @return [void]
|
|
173
|
+
# @raise [RuntimeError] if no event name configured
|
|
174
|
+
#
|
|
175
|
+
# @example Emit from controller
|
|
176
|
+
# class UsersController
|
|
177
|
+
# def create
|
|
178
|
+
# user = User.create!(params)
|
|
179
|
+
# UserCreated.emit({ user_id: user.id, email: user.email })
|
|
180
|
+
# redirect_to user
|
|
181
|
+
# end
|
|
182
|
+
# end
|
|
183
|
+
#
|
|
184
|
+
# @example Emit from background job
|
|
185
|
+
# class ProcessDataJob
|
|
186
|
+
# def perform(data_id)
|
|
187
|
+
# result = process_data(data_id)
|
|
188
|
+
# DataProcessed.emit({ data_id: data_id, status: result })
|
|
189
|
+
# end
|
|
190
|
+
# end
|
|
191
|
+
def emit(payload)
|
|
192
|
+
raise 'No event configured. Call event_name :name first.' unless @event_name
|
|
193
|
+
|
|
194
|
+
Servus::Support::Validator.validate_event_payload!(self, payload)
|
|
195
|
+
|
|
196
|
+
Servus::Events::Bus.emit(@event_name, payload)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns Invocation objects for the given payload, with conditions
|
|
200
|
+
# already evaluated. This is what routers call to resolve actions.
|
|
201
|
+
#
|
|
202
|
+
# @param payload [Hash] the event payload
|
|
203
|
+
# @return [Array<Servus::Events::Invocation>] invocations that passed conditions
|
|
204
|
+
def invocations_for(payload)
|
|
205
|
+
invocations.filter_map do |inv|
|
|
206
|
+
next unless should_invoke?(payload, inv[:options])
|
|
207
|
+
|
|
208
|
+
Servus::Events::Invocation.new(
|
|
209
|
+
service: inv[:service_class],
|
|
210
|
+
params: inv[:mapper].call(payload),
|
|
211
|
+
options: inv[:options].except(:if, :unless)
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Handles an event by resolving and executing all invocations.
|
|
217
|
+
#
|
|
218
|
+
# @param payload [Hash] the event payload
|
|
219
|
+
# @return [Array] results from all invoked services
|
|
220
|
+
def handle(payload)
|
|
221
|
+
invocations_for(payload).map(&:execute)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
# @api private
|
|
227
|
+
def should_invoke?(payload, options)
|
|
228
|
+
return false if options[:if] && !options[:if].call(payload)
|
|
229
|
+
return false if options[:unless]&.call(payload)
|
|
230
|
+
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
data/lib/servus/events/bus.rb
CHANGED
|
@@ -2,93 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Events
|
|
5
|
-
#
|
|
5
|
+
# Central event bus for registering Event classes and dispatching
|
|
6
|
+
# events through configured routers.
|
|
6
7
|
#
|
|
7
|
-
# The Bus
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# The Bus maintains a registry mapping event names to their Event
|
|
9
|
+
# class definitions (one-to-one). On +emit+, it delegates to the
|
|
10
|
+
# configured routers to resolve invocations, deduplicates by key,
|
|
11
|
+
# and executes. ActiveSupport::Notifications wraps the dispatch
|
|
12
|
+
# cycle for the +subscribe_all+ hook (e.g. forwarding to Eventus).
|
|
10
13
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @example Registering a handler
|
|
15
|
-
# class UserCreatedHandler < Servus::EventHandler
|
|
16
|
-
# handles :user_created
|
|
14
|
+
# @example Registering an event class
|
|
15
|
+
# class UserCreated < Servus::Event
|
|
16
|
+
# event_name :user_created
|
|
17
17
|
# end
|
|
18
|
+
# # Registration happens automatically via the event_name DSL
|
|
18
19
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# @example Retrieving handlers for an event
|
|
22
|
-
# handlers = Servus::Events::Bus.handlers_for(:user_created)
|
|
23
|
-
# handlers.each { |handler| handler.handle(payload) }
|
|
20
|
+
# @example Emitting an event
|
|
21
|
+
# Bus.emit(:user_created, { user_id: 123 })
|
|
24
22
|
#
|
|
25
|
-
# @example
|
|
26
|
-
# Bus.
|
|
27
|
-
#
|
|
23
|
+
# @example Forwarding all events to an external system
|
|
24
|
+
# Bus.subscribe_all do |event_name, payload, started_at:, **|
|
|
25
|
+
# ExternalForwarder.perform_later(event: event_name, payload:)
|
|
26
|
+
# end
|
|
28
27
|
#
|
|
29
|
-
# @see Servus::
|
|
28
|
+
# @see Servus::Event
|
|
29
|
+
# @see Servus::Events::Router
|
|
30
|
+
# @see Servus::Events::ClassRouter
|
|
30
31
|
class Bus
|
|
31
32
|
class << self
|
|
32
|
-
# Registers
|
|
33
|
+
# Registers an Event class for a specific event name.
|
|
33
34
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# automatically subscribed to ActiveSupport::Notifications.
|
|
35
|
+
# Each event name maps to exactly one Event class. Attempting to
|
|
36
|
+
# register a second class for the same name raises an error.
|
|
37
37
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
38
|
+
# Event classes are typically registered automatically at boot time
|
|
39
|
+
# via the +event_name+ DSL method or +ensure_registered!+.
|
|
40
40
|
#
|
|
41
|
-
# @param
|
|
42
|
-
# @param
|
|
43
|
-
# @return [
|
|
41
|
+
# @param name [Symbol] the event name
|
|
42
|
+
# @param event_class [Class] the Event subclass to register
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @raise [RuntimeError] if the event name is already registered
|
|
44
45
|
#
|
|
45
46
|
# @example
|
|
46
|
-
# Bus.
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Subscribe to ActiveSupport::Notifications
|
|
52
|
-
subscription = ActiveSupport::Notifications.subscribe(notification_name(event_name)) do |*args|
|
|
53
|
-
event = ActiveSupport::Notifications::Event.new(*args)
|
|
54
|
-
handler_class.handle(event.payload)
|
|
47
|
+
# Bus.register_event(:user_created, UserCreated)
|
|
48
|
+
def register_event(name, event_class)
|
|
49
|
+
if events.key?(name)
|
|
50
|
+
raise "Event :#{name} is already registered to #{events[name]}. Cannot register #{event_class}"
|
|
55
51
|
end
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
subscriptions[event_name] ||= []
|
|
59
|
-
subscriptions[event_name] << subscription
|
|
53
|
+
events[name] = event_class
|
|
60
54
|
end
|
|
61
55
|
|
|
62
|
-
#
|
|
56
|
+
# Returns the Event class registered for the given name.
|
|
63
57
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# @param event_name [Symbol] the name of the event
|
|
68
|
-
# @return [Array<Class>] array of handler classes registered for this event
|
|
58
|
+
# @param name [Symbol] the event name
|
|
59
|
+
# @return [Class, nil] the Event class or nil if not registered
|
|
69
60
|
#
|
|
70
61
|
# @example
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
def
|
|
74
|
-
|
|
62
|
+
# event_class = Bus.event_for(:user_created)
|
|
63
|
+
# event_class.invocations_for(payload)
|
|
64
|
+
def event_for(name)
|
|
65
|
+
events[name]
|
|
75
66
|
end
|
|
76
67
|
|
|
77
|
-
# Emits an event
|
|
68
|
+
# Emits an event through the configured routers.
|
|
78
69
|
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
70
|
+
# Collects invocations from all routers (in config array order),
|
|
71
|
+
# deduplicates by key (first wins), and executes each. The entire
|
|
72
|
+
# dispatch is wrapped in ActiveSupport::Notifications so that
|
|
73
|
+
# +subscribe_all+ listeners receive timing information.
|
|
82
74
|
#
|
|
83
75
|
# @param event_name [Symbol] the name of the event to emit
|
|
84
|
-
# @param payload [Hash] the event payload
|
|
76
|
+
# @param payload [Hash] the event payload
|
|
85
77
|
# @return [void]
|
|
86
78
|
#
|
|
87
79
|
# @example
|
|
88
80
|
# Bus.emit(:user_created, { user_id: 123, email: 'user@example.com' })
|
|
89
81
|
# # Rails log: servus.events.user_created (1.2ms) {:user_id=>123, :email=>"user@example.com"}
|
|
90
82
|
def emit(event_name, payload)
|
|
91
|
-
ActiveSupport::Notifications.instrument(notification_name(event_name), payload)
|
|
83
|
+
ActiveSupport::Notifications.instrument(notification_name(event_name), payload) do
|
|
84
|
+
resolve_invocations(event_name, payload)
|
|
85
|
+
.uniq(&:key)
|
|
86
|
+
.each(&:execute)
|
|
87
|
+
end
|
|
92
88
|
end
|
|
93
89
|
|
|
94
90
|
# Subscribes to all Servus event emissions.
|
|
@@ -120,7 +116,26 @@ module Servus
|
|
|
120
116
|
end
|
|
121
117
|
end
|
|
122
118
|
|
|
123
|
-
#
|
|
119
|
+
# Installs the internal event logger. Called once at boot via
|
|
120
|
+
# the Railtie. Logs every event emission with its AS::Notifications
|
|
121
|
+
# correlation ID and dispatch duration.
|
|
122
|
+
#
|
|
123
|
+
# Multiple +subscribe_all+ subscriptions coexist — the app can
|
|
124
|
+
# add its own (e.g. for Eventus forwarding) independently.
|
|
125
|
+
#
|
|
126
|
+
# @return [void]
|
|
127
|
+
def enable_logging!
|
|
128
|
+
return if @logging_enabled
|
|
129
|
+
|
|
130
|
+
subscribe_all do |event_name, payload, **meta|
|
|
131
|
+
duration_ms = (meta[:finished_at] - meta[:started_at]) * 1000
|
|
132
|
+
Servus::Support::Logger.log_event(event_name, payload, event_id: meta[:id], duration_ms:)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
@logging_enabled = true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Clears all registered events.
|
|
124
139
|
#
|
|
125
140
|
# Useful for testing and development mode reloading.
|
|
126
141
|
#
|
|
@@ -129,28 +144,23 @@ module Servus
|
|
|
129
144
|
# @example
|
|
130
145
|
# Bus.clear
|
|
131
146
|
def clear
|
|
132
|
-
|
|
133
|
-
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
@handlers = nil
|
|
137
|
-
@subscriptions = nil
|
|
147
|
+
@events = nil
|
|
138
148
|
end
|
|
139
149
|
|
|
140
150
|
private
|
|
141
151
|
|
|
142
|
-
#
|
|
152
|
+
# Collects invocations from all configured routers.
|
|
143
153
|
#
|
|
144
|
-
# @
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
# @param event_name [Symbol] the event name
|
|
155
|
+
# @param payload [Hash] the event payload
|
|
156
|
+
# @return [Array<Servus::Events::Invocation>]
|
|
157
|
+
def resolve_invocations(event_name, payload)
|
|
158
|
+
Servus.config.routers.flat_map { |router| router.resolve(event_name, payload) }
|
|
147
159
|
end
|
|
148
160
|
|
|
149
|
-
# Hash
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def subscriptions
|
|
153
|
-
@subscriptions ||= {}
|
|
161
|
+
# @return [Hash{Symbol => Class}] event name to Event class mapping
|
|
162
|
+
def events
|
|
163
|
+
@events ||= {}
|
|
154
164
|
end
|
|
155
165
|
|
|
156
166
|
# Converts an event name to a namespaced notification name.
|