omnes 0.1.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 +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
|