omnes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/lint.yml +24 -0
- data/.github/workflows/test.yml +26 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +44 -0
- data/.travis.yml +6 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +11 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +617 -0
- data/Rakefile +8 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/lib/omnes/bus.rb +274 -0
- data/lib/omnes/errors.rb +101 -0
- data/lib/omnes/event.rb +72 -0
- data/lib/omnes/execution.rb +42 -0
- data/lib/omnes/publication.rb +43 -0
- data/lib/omnes/registry.rb +102 -0
- data/lib/omnes/subscriber/adapter/active_job.rb +74 -0
- data/lib/omnes/subscriber/adapter/method/errors.rb +51 -0
- data/lib/omnes/subscriber/adapter/method.rb +45 -0
- data/lib/omnes/subscriber/adapter/sidekiq.rb +92 -0
- data/lib/omnes/subscriber/adapter.rb +33 -0
- data/lib/omnes/subscriber/errors.rb +24 -0
- data/lib/omnes/subscriber/state.rb +68 -0
- data/lib/omnes/subscriber.rb +229 -0
- data/lib/omnes/subscription.rb +58 -0
- data/lib/omnes/unstructured_event.rb +42 -0
- data/lib/omnes/version.rb +5 -0
- data/lib/omnes.rb +83 -0
- data/omnes.gemspec +45 -0
- metadata +157 -0
data/lib/omnes/bus.rb
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "omnes/publication"
|
4
|
+
require "omnes/registry"
|
5
|
+
require "omnes/subscription"
|
6
|
+
require "omnes/unstructured_event"
|
7
|
+
|
8
|
+
module Omnes
|
9
|
+
# An event bus for the publish/subscribe pattern
|
10
|
+
#
|
11
|
+
# An instance of this class acts as an event bus middleware for publishers of
|
12
|
+
# events and their subscriptions.
|
13
|
+
#
|
14
|
+
# ```
|
15
|
+
# bus = Omnes::Bus.new
|
16
|
+
# ```
|
17
|
+
#
|
18
|
+
# Before being able to work with a given event, its name (a {Symbol}) needs to
|
19
|
+
# be registered:
|
20
|
+
#
|
21
|
+
# ```
|
22
|
+
# bus.register(:foo)
|
23
|
+
# ```
|
24
|
+
#
|
25
|
+
# An event can be anything responding to a method `:name` which, needless to
|
26
|
+
# say, must match with a registered name.
|
27
|
+
#
|
28
|
+
# Typically, there're two main ways to generate events.
|
29
|
+
#
|
30
|
+
# An event can be generated at publication time, where you provide its name
|
31
|
+
# and a payload to be consumed by its subscribers.
|
32
|
+
#
|
33
|
+
# ```
|
34
|
+
# bus.publish(:foo, bar: :baz)
|
35
|
+
# ```
|
36
|
+
#
|
37
|
+
# In that case, an instance of {Omnes::UnstructuredEvent} is generated
|
38
|
+
# under the hood.
|
39
|
+
#
|
40
|
+
# Unstructured events are straightforward to create and use, but they're
|
41
|
+
# harder to debug as they're defined at publication time. On top of that,
|
42
|
+
# other features, such as event persistence, can't be reliably built on top of
|
43
|
+
# them.
|
44
|
+
#
|
45
|
+
# You can also publish an instance of a class including {Omnes::Event}. The
|
46
|
+
# only fancy thing it provides is an OOTB event name generated based on the
|
47
|
+
# class name. See {Omnes::Event} for details.
|
48
|
+
#
|
49
|
+
# ```
|
50
|
+
# class Foo
|
51
|
+
# include Omnes::Event
|
52
|
+
#
|
53
|
+
# attr_reader :bar
|
54
|
+
#
|
55
|
+
# def initialize
|
56
|
+
# @bar = :baz
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# bus.publish(Foo.new)
|
61
|
+
# ```
|
62
|
+
#
|
63
|
+
# Instance-backed events provide a well-defined structure, and other features,
|
64
|
+
# like event persistence, can be added on top of them.
|
65
|
+
#
|
66
|
+
# Regardless of the type of published event, it's yielded to its subscriptions
|
67
|
+
# so that they can do their job:
|
68
|
+
#
|
69
|
+
# ```
|
70
|
+
# bus.subscribe(:foo) do |event|
|
71
|
+
# # event.payload[:bar] or event[:bar] for unstructured events
|
72
|
+
# # event.bar for the event instance example
|
73
|
+
# end
|
74
|
+
# ```
|
75
|
+
#
|
76
|
+
# The subscription code can be given as a block (previous example) or as
|
77
|
+
# anything responding to a method `#call`.
|
78
|
+
#
|
79
|
+
# ```
|
80
|
+
# class MySubscription
|
81
|
+
# def call(event)
|
82
|
+
# # ...
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# bus.subscribe(:foo, MySubscription.new)
|
87
|
+
# ```
|
88
|
+
#
|
89
|
+
# See also {Omnes::Subscriber} for a more powerful way to define standalone
|
90
|
+
# event handlers.
|
91
|
+
#
|
92
|
+
# You can also create a subscription that will run for all events:
|
93
|
+
#
|
94
|
+
# ```
|
95
|
+
# bus.subscribe_to_all(MySubscription.new)
|
96
|
+
# ```
|
97
|
+
#
|
98
|
+
# Custom matchers can be defined. A matcher is something responding to `#call`
|
99
|
+
# and taking the event as an argument. It needs to return `true` or `false` to
|
100
|
+
# decide whether the subscription needs to be run for that event.
|
101
|
+
#
|
102
|
+
# ```
|
103
|
+
# matcher ->(event) { event.name.start_with?(:foo) }
|
104
|
+
#
|
105
|
+
# bus.subscribe_with_matcher(matcher, MySubscription.new)
|
106
|
+
# ```
|
107
|
+
class Bus
|
108
|
+
# @api private
|
109
|
+
def self.EventType(value, **payload)
|
110
|
+
case value
|
111
|
+
when Symbol
|
112
|
+
UnstructuredEvent.new(omnes_event_name: value, payload: payload)
|
113
|
+
else
|
114
|
+
value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# @api private
|
119
|
+
attr_reader :cal_loc_start,
|
120
|
+
:subscriptions
|
121
|
+
|
122
|
+
# @!attribute [r] registry
|
123
|
+
# @return [Omnes::Bus::Registry]
|
124
|
+
attr_reader :registry
|
125
|
+
|
126
|
+
def initialize(cal_loc_start: 1, registry: Registry.new, subscriptions: [])
|
127
|
+
@cal_loc_start = cal_loc_start
|
128
|
+
@registry = registry
|
129
|
+
@subscriptions = subscriptions
|
130
|
+
end
|
131
|
+
|
132
|
+
# Registers an event name
|
133
|
+
#
|
134
|
+
# @param event_name [Symbol]
|
135
|
+
# @param caller_location [Thread::Backtrace::Location] Caller location
|
136
|
+
# associated to the registration. Useful for debugging (shown in error
|
137
|
+
# messages). It defaults to this method's caller.
|
138
|
+
#
|
139
|
+
# @raise [Omnes::AlreadyRegisteredEventError] when the event is already
|
140
|
+
# registered
|
141
|
+
# @raise [Omnes::InvalidEventNameError] when the event is not a {Symbol}
|
142
|
+
#
|
143
|
+
# @return [Omnes::Registry::Registration]
|
144
|
+
def register(event_name, caller_location: caller_locations(cal_loc_start)[0])
|
145
|
+
registry.register(event_name, caller_location: caller_location)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Publishes an event, running all matching subscriptions
|
149
|
+
#
|
150
|
+
# @overload publish(event_name, caller_location:, **payload)
|
151
|
+
# @param event_name [Symbol] Name for the generated
|
152
|
+
# {Omnes::UnstructuredEvent} event.
|
153
|
+
# @param **payload [Hash] Payload for the generated
|
154
|
+
# {Omnes::UnstrUnstructuredEvent}
|
155
|
+
#
|
156
|
+
# @overload publish(event, caller_location:)
|
157
|
+
# @param event [#name] An event instance
|
158
|
+
#
|
159
|
+
# @param caller_location [Thread::Backtrace::Location] Caller location
|
160
|
+
# associated to the publication. Useful for debugging (shown in error
|
161
|
+
# messages). It defaults to this method's caller.
|
162
|
+
#
|
163
|
+
# @return [Omnes::Publication] A publication object encapsulating metadata
|
164
|
+
# for the event and the originated subscription executions
|
165
|
+
#
|
166
|
+
# @raise [Omnes::UnknownEventError] When event name has not been registered
|
167
|
+
def publish(event, caller_location: caller_locations(cal_loc_start)[0], **payload)
|
168
|
+
publication_time = Time.now.utc
|
169
|
+
event = self.class.EventType(event, **payload)
|
170
|
+
registry.check_event_name(event.omnes_event_name)
|
171
|
+
executions = execute_subscriptions_for_event(event)
|
172
|
+
|
173
|
+
Publication.new(
|
174
|
+
event: event,
|
175
|
+
executions: executions,
|
176
|
+
caller_location: caller_location,
|
177
|
+
time: publication_time
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Adds a subscription for a single event
|
182
|
+
#
|
183
|
+
# @param event_name [Symbol] Name of the event
|
184
|
+
# @param callable [#call] Subscription callback taking the event
|
185
|
+
# @yield [event] Subscription callback if callable is not given
|
186
|
+
#
|
187
|
+
# @return [Omnes::Subscription]
|
188
|
+
#
|
189
|
+
# @raise [Omnes::UnknownEventError] When event name has not been registered
|
190
|
+
def subscribe(event_name, callable = nil, &block)
|
191
|
+
registry.check_event_name(event_name)
|
192
|
+
|
193
|
+
subscribe_with_matcher(Subscription::SINGLE_EVENT_MATCHER.curry[event_name], callable, &block)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Adds a subscription for all events
|
197
|
+
#
|
198
|
+
# @param callable [#call] Subscription callback taking the event
|
199
|
+
# @yield [event] Subscription callback if callable is not given
|
200
|
+
#
|
201
|
+
# @return [Omnes::Subscription]
|
202
|
+
def subscribe_to_all(callable = nil, &block)
|
203
|
+
subscribe_with_matcher(Subscription::ALL_EVENTS_MATCHER, callable, &block)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Adds a subscription with given matcher
|
207
|
+
#
|
208
|
+
# @param matcher [#call] Callable taking the event and returning a boolean
|
209
|
+
# @param callable [#call] Subscription callback taking the event
|
210
|
+
# @yield [event] Subscription callback if callable is not given
|
211
|
+
#
|
212
|
+
# @return [Omnes::Subscription]
|
213
|
+
def subscribe_with_matcher(matcher, callable = nil, &block)
|
214
|
+
callback = callable || block
|
215
|
+
Subscription.new(matcher: matcher, callback: callback).tap do |subscription|
|
216
|
+
@subscriptions << subscription
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Removes a subscription
|
221
|
+
#
|
222
|
+
# @param subscription [Omnes::Subscription]
|
223
|
+
def unsubscribe(subscription)
|
224
|
+
@subscriptions.delete(subscription)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Runs given block performing only a selection of subscriptions
|
228
|
+
#
|
229
|
+
# That's something useful for testing purposes, as it allows to silence
|
230
|
+
# subscriptions that are not part of the system under test.
|
231
|
+
#
|
232
|
+
# After the block is over, original subscriptions are restored.
|
233
|
+
#
|
234
|
+
# @param selection [Array<Omnes::Subscription>]
|
235
|
+
# @yield Block to run
|
236
|
+
#
|
237
|
+
# @raise [Omnes::UnknownSubscriptionError] when the subscription is not
|
238
|
+
# known by the bus
|
239
|
+
def performing_only(*selection)
|
240
|
+
selection.each do |subscription|
|
241
|
+
unless subscriptions.include?(subscription)
|
242
|
+
raise UnknownSubscriptionError.new(subscription: subscription,
|
243
|
+
bus: self)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
all_subscriptions = subscriptions
|
247
|
+
@subscriptions = selection
|
248
|
+
yield
|
249
|
+
ensure
|
250
|
+
@subscriptions = all_subscriptions
|
251
|
+
end
|
252
|
+
|
253
|
+
# Specialized version of {#performing_only} with no subscriptions
|
254
|
+
#
|
255
|
+
# @see #performing_only
|
256
|
+
def performing_nothing(&block)
|
257
|
+
performing_only(&block)
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def execute_subscriptions_for_event(event)
|
263
|
+
subscriptions_for_event(event).map do |subscription|
|
264
|
+
subscription.(event)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def subscriptions_for_event(event_name)
|
269
|
+
@subscriptions.select do |subscription|
|
270
|
+
subscription.matches?(event_name)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
data/lib/omnes/errors.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Omnes
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# Raised when an event name is not known
|
7
|
+
class UnknownEventError < Error
|
8
|
+
attr_reader :event_name, :known_events
|
9
|
+
|
10
|
+
def initialize(event_name:, known_events:)
|
11
|
+
@event_name = event_name
|
12
|
+
@known_events = known_events
|
13
|
+
super(default_message)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def default_message
|
19
|
+
<<~MSG
|
20
|
+
'#{event_name}' event is not registered.
|
21
|
+
#{suggestions_message}
|
22
|
+
|
23
|
+
All known events are:
|
24
|
+
|
25
|
+
'#{known_events.join("', '")}'
|
26
|
+
MSG
|
27
|
+
end
|
28
|
+
|
29
|
+
def suggestions_message
|
30
|
+
DidYouMean::PlainFormatter.new.message_for(suggestions)
|
31
|
+
end
|
32
|
+
|
33
|
+
def suggestions
|
34
|
+
dictionary = DidYouMean::SpellChecker.new(dictionary: known_events)
|
35
|
+
|
36
|
+
dictionary.correct(event_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raised when trying to register an invalid event name
|
41
|
+
class InvalidEventNameError < Error
|
42
|
+
attr_reader :event_name
|
43
|
+
|
44
|
+
def initialize(event_name:)
|
45
|
+
@event_name = event_name
|
46
|
+
super(default_message)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def default_message
|
52
|
+
<<~MSG
|
53
|
+
#{event_name.inspect} is not a valid event name. Only symbols can be
|
54
|
+
registered.
|
55
|
+
MSG
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Raised when trying to register the same event name a second time
|
60
|
+
class AlreadyRegisteredEventError < Error
|
61
|
+
attr_reader :event_name, :registration
|
62
|
+
|
63
|
+
def initialize(event_name:, registration:)
|
64
|
+
@event_name = event_name
|
65
|
+
@registration = registration
|
66
|
+
super(default_message)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def default_message
|
72
|
+
<<~MSG
|
73
|
+
Can't register #{event_name} event as it's already registered.
|
74
|
+
|
75
|
+
The registration happened at:
|
76
|
+
|
77
|
+
#{registration.caller_location}
|
78
|
+
MSG
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Raised when a subscription is not known by a bus
|
83
|
+
class UnknownSubscriptionError < Error
|
84
|
+
attr_reader :subscription, :bus
|
85
|
+
|
86
|
+
def initialize(subscription:, bus:)
|
87
|
+
@subscription = subscription
|
88
|
+
@bus = bus
|
89
|
+
super(default_message)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def default_message
|
95
|
+
<<~MSG
|
96
|
+
#{subscription.inspect} is not a subscription known by bus
|
97
|
+
#{bus.inspect}
|
98
|
+
MSG
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/omnes/event.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/configurable"
|
4
|
+
|
5
|
+
module Omnes
|
6
|
+
# Event mixin for custom classes
|
7
|
+
#
|
8
|
+
# Any instance of a class including this one can be used as a published event
|
9
|
+
# (see {Omnes::Bus#publish}).
|
10
|
+
#
|
11
|
+
# ```
|
12
|
+
# class MyEvent
|
13
|
+
# include Omnes::Event
|
14
|
+
#
|
15
|
+
# attr_reader :event
|
16
|
+
#
|
17
|
+
# def initialize(id:)
|
18
|
+
# @id = id
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# bus = Omnes::Bus.new
|
23
|
+
# bus.register(:my_event)
|
24
|
+
# bus.subscribe(:my_event) do |event|
|
25
|
+
# puts event.id
|
26
|
+
# end
|
27
|
+
# bus.publish(MyEvent.new(1))
|
28
|
+
# ```
|
29
|
+
module Event
|
30
|
+
extend Dry::Configurable
|
31
|
+
|
32
|
+
# Generates the event name for an event instance
|
33
|
+
#
|
34
|
+
# It returns the underscored class name, with an `Event` suffix removed if
|
35
|
+
# present. E.g:
|
36
|
+
#
|
37
|
+
# - Foo -> `:foo`
|
38
|
+
# - FooEvent -> `:foo`
|
39
|
+
# - FooBar -> `:foo_bar`
|
40
|
+
# - FBar -> `:f_bar`
|
41
|
+
# - Foo::Bar -> `:foo_bar`
|
42
|
+
#
|
43
|
+
# You can also use your custom name builder. It needs to be something
|
44
|
+
# callable taking the instance as argument and returning a {Symbol}:
|
45
|
+
#
|
46
|
+
# ```
|
47
|
+
# my_name_builder = ->(instance) { instance.class.name.to_sym }
|
48
|
+
# Omnes.config.event.name_builder = my_name_builder
|
49
|
+
# ```
|
50
|
+
#
|
51
|
+
# @return [Symbol]
|
52
|
+
DEFAULT_NAME_BUILDER = lambda do |instance|
|
53
|
+
instance.class.name
|
54
|
+
.chomp("Event")
|
55
|
+
.gsub(/([[:alpha:]])([[:upper:]])/, '\1_\2')
|
56
|
+
.gsub("::", "_")
|
57
|
+
.downcase
|
58
|
+
.to_sym
|
59
|
+
end
|
60
|
+
|
61
|
+
setting :name_builder, default: DEFAULT_NAME_BUILDER
|
62
|
+
|
63
|
+
# Event name
|
64
|
+
#
|
65
|
+
# @return [Symbol]
|
66
|
+
#
|
67
|
+
# @see DEFAULT_NAME_BUILDER
|
68
|
+
def omnes_event_name
|
69
|
+
Omnes::Event.config.name_builder.(self)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Omnes
|
4
|
+
# Execution of an {Omnes::Subscription}
|
5
|
+
#
|
6
|
+
# When an event is published, it executes all matching subscriptions. Every
|
7
|
+
# single execution is represented as an instance of this class. It contains
|
8
|
+
# the result value of the subscriptions along with helpful metadata as the
|
9
|
+
# time of the execution or a benchmark for it.
|
10
|
+
#
|
11
|
+
# You'll most likely interact with this class for debugging or logging
|
12
|
+
# purposes through a {Omnes::Publication} returned on {Omnes::Bus#publish}.
|
13
|
+
class Execution
|
14
|
+
# The subscription to which the execution belongs
|
15
|
+
#
|
16
|
+
# @return [Omnes::Subscription]
|
17
|
+
attr_reader :subscription
|
18
|
+
|
19
|
+
# The value returned by the {#subscription}'s callback
|
20
|
+
#
|
21
|
+
# @return [Any]
|
22
|
+
attr_reader :result
|
23
|
+
|
24
|
+
# Benchmark for the {#subscription}'s callback
|
25
|
+
#
|
26
|
+
# @return [Benchmark::Tms]
|
27
|
+
attr_reader :benchmark
|
28
|
+
|
29
|
+
# Time of execution
|
30
|
+
#
|
31
|
+
# @return [Time]
|
32
|
+
attr_reader :time
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def initialize(subscription:, result:, benchmark:, time: Time.now.utc)
|
36
|
+
@subscription = subscription
|
37
|
+
@result = result
|
38
|
+
@benchmark = benchmark
|
39
|
+
@time = time
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Omnes
|
4
|
+
# The result of publishing an event
|
5
|
+
#
|
6
|
+
# It encapsulates a published {Omnes::Event} as well as the
|
7
|
+
# {Omnes::Execution}s it originated.
|
8
|
+
#
|
9
|
+
# This class is useful mainly for debugging and logging purposes. An
|
10
|
+
# instance of it is returned on {Omnes::Bus#publish}.
|
11
|
+
class Publication
|
12
|
+
# Published event
|
13
|
+
#
|
14
|
+
# @return [#name]
|
15
|
+
attr_reader :event
|
16
|
+
|
17
|
+
# Subscription executions that the publication originated
|
18
|
+
#
|
19
|
+
# @return [Array<Omnes::Execution>]
|
20
|
+
attr_reader :executions
|
21
|
+
|
22
|
+
# Location for the event caller
|
23
|
+
#
|
24
|
+
# It's usually set by {Omnes::Bus#publish}, and it points to the caller of
|
25
|
+
# that method.
|
26
|
+
#
|
27
|
+
# @return [Thread::Backtrace::Location]
|
28
|
+
attr_reader :caller_location
|
29
|
+
|
30
|
+
# Time of the event publication
|
31
|
+
#
|
32
|
+
# @return [Time]
|
33
|
+
attr_reader :time
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def initialize(event:, executions:, caller_location:, time:)
|
37
|
+
@event = event
|
38
|
+
@executions = executions
|
39
|
+
@caller_location = caller_location
|
40
|
+
@time = time
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "omnes/errors"
|
4
|
+
|
5
|
+
module Omnes
|
6
|
+
# Registry of known event names
|
7
|
+
#
|
8
|
+
# Before publishing or subscribing to an event, its name must be registered to
|
9
|
+
# the instance associated with the bus (see {Omnes::Bus#register}).
|
10
|
+
class Registry
|
11
|
+
# Wraps the registration of an event
|
12
|
+
class Registration
|
13
|
+
# @!attribute [r] event_name
|
14
|
+
# @return [Symbol]
|
15
|
+
attr_reader :event_name
|
16
|
+
|
17
|
+
# @!attribute [r] caller_location
|
18
|
+
# @return [Thread::Backtrace::Location]
|
19
|
+
attr_reader :caller_location
|
20
|
+
|
21
|
+
def initialize(event_name:, caller_location:)
|
22
|
+
@event_name = event_name
|
23
|
+
@caller_location = caller_location
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!attribute [r] registrations
|
28
|
+
# @return [Array<Omnes::Registry::Registration>]
|
29
|
+
attr_reader :registrations
|
30
|
+
|
31
|
+
def initialize(registrations: [])
|
32
|
+
@registrations = registrations
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def register(event_name, caller_location: caller_locations(1)[0])
|
37
|
+
raise InvalidEventNameError.new(event_name: event_name) unless valid_event_name?(event_name)
|
38
|
+
|
39
|
+
registration = registration(event_name)
|
40
|
+
raise AlreadyRegisteredEventError.new(event_name: event_name, registration: registration) if registration
|
41
|
+
|
42
|
+
Registration.new(event_name: event_name, caller_location: caller_location).tap do |reg|
|
43
|
+
@registrations << reg
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Removes an event name from the registry
|
48
|
+
#
|
49
|
+
# @param event_name [Symbol]
|
50
|
+
def unregister(event_name)
|
51
|
+
check_event_name(event_name)
|
52
|
+
|
53
|
+
@registrations.delete_if { |regs| regs.event_name == event_name }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns an array with all registered event names
|
57
|
+
#
|
58
|
+
# @return [Array<Symbol>]
|
59
|
+
def event_names
|
60
|
+
registrations.map(&:event_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the registration, if present, for the event name
|
64
|
+
#
|
65
|
+
# @param event_name [Symbol]
|
66
|
+
#
|
67
|
+
# @return [Omnes::Registry::Registration, nil]
|
68
|
+
def registration(event_name)
|
69
|
+
registrations.find { |reg| reg.event_name == event_name }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns whether a given event name is registered
|
73
|
+
#
|
74
|
+
# Use {#check_event_name} for a raising version of it.
|
75
|
+
#
|
76
|
+
# @param event_name [Symbol]
|
77
|
+
#
|
78
|
+
# @return [Boolean]
|
79
|
+
def registered?(event_name)
|
80
|
+
!registration(event_name).nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks whether given event name is present in the registry
|
84
|
+
#
|
85
|
+
# Use {#registered?} for a predicate version of it.
|
86
|
+
#
|
87
|
+
# @param event_name [Symbol]
|
88
|
+
#
|
89
|
+
# @raise [UnknownEventError] if the event is not registered
|
90
|
+
def check_event_name(event_name)
|
91
|
+
return if registered?(event_name)
|
92
|
+
|
93
|
+
raise UnknownEventError.new(event_name: event_name, known_events: event_names)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def valid_event_name?(event_name)
|
99
|
+
event_name.is_a?(Symbol)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/configurable"
|
4
|
+
|
5
|
+
module Omnes
|
6
|
+
module Subscriber
|
7
|
+
module Adapter
|
8
|
+
# [ActiveJob](https://edgeguides.rubyonrails.org/active_job_basics.html) adapter
|
9
|
+
#
|
10
|
+
# Builds subscription to be processed as ActiveJob background jobs.
|
11
|
+
#
|
12
|
+
# ActiveJob requires that the argument passed to `#perform` is
|
13
|
+
# serializable. By default, the result of calling `#payload` in the event
|
14
|
+
# is taken.
|
15
|
+
#
|
16
|
+
# ```
|
17
|
+
# class MyJob < ActiveJob
|
18
|
+
# include Omnes::Subscriber
|
19
|
+
#
|
20
|
+
# handle :my_event, with: Adapter::ActiveJob
|
21
|
+
#
|
22
|
+
# def perform(payload)
|
23
|
+
# # do_something
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# bus = Omnes::Bus.new
|
28
|
+
# bus.register(:my_event)
|
29
|
+
# bus.publish(:my_event, "foo" => "bar")
|
30
|
+
# ```
|
31
|
+
# However, you can configure how the event is serialized thanks to the
|
32
|
+
# `serializer:` option. It needs to be something callable taking the
|
33
|
+
# event as argument:
|
34
|
+
#
|
35
|
+
# ```
|
36
|
+
# handle :my_event, with: Adapter::ActiveJob[serializer: :serialized_payload.to_proc]
|
37
|
+
# ```
|
38
|
+
#
|
39
|
+
# You can also globally configure the default serializer:
|
40
|
+
#
|
41
|
+
# ```
|
42
|
+
# Omnes.config.subscriber.adapter.active_job.serializer = :serialized_payload.to_proc
|
43
|
+
# ```
|
44
|
+
module ActiveJob
|
45
|
+
extend Dry::Configurable
|
46
|
+
|
47
|
+
setting :serializer, default: :payload.to_proc
|
48
|
+
|
49
|
+
# @param serializer [#call]
|
50
|
+
def self.[](serializer: config.serializer)
|
51
|
+
Instance.new(serializer: serializer)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def self.call(instance, event)
|
56
|
+
self.[].(instance, event)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
class Instance
|
61
|
+
attr_reader :serializer
|
62
|
+
|
63
|
+
def initialize(serializer:)
|
64
|
+
@serializer = serializer
|
65
|
+
end
|
66
|
+
|
67
|
+
def call(instance, event)
|
68
|
+
instance.class.perform_later(serializer.(event))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|