omnes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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