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.
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omnes/errors"
4
+
5
+ module Omnes
6
+ module Subscriber
7
+ module Adapter
8
+ class Method
9
+ # Raised when trying to subscribe to a missing method
10
+ class UnknownMethodSubscriptionAttemptError < Omnes::Error
11
+ attr_reader :method_name
12
+
13
+ # @api private
14
+ def initialize(method_name:)
15
+ @method_name = method_name
16
+ super(default_message)
17
+ end
18
+
19
+ private
20
+
21
+ def default_message
22
+ <<~MSG
23
+ You tried to subscribe an unexisting "#{method_name}" method. Event
24
+ handlers need to be public methods on the subscriber class.
25
+ MSG
26
+ end
27
+ end
28
+
29
+ # Raised when trying to subscribe to a private method
30
+ class PrivateMethodSubscriptionAttemptError < Omnes::Error
31
+ attr_reader :method_name
32
+
33
+ # @api private
34
+ def initialize(method_name:)
35
+ @method_name = method_name
36
+ super(default_message)
37
+ end
38
+
39
+ private
40
+
41
+ def default_message
42
+ <<~MSG
43
+ You tried to subscribe "#{method_name}" private method. Event handlers
44
+ need to be public methods on the subscriber class.
45
+ MSG
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omnes/subscriber/adapter/method/errors"
4
+
5
+ module Omnes
6
+ module Subscriber
7
+ module Adapter
8
+ # Builds a callback from a method of the instance
9
+ #
10
+ # You can use instance of this class as the adapter:
11
+ #
12
+ # ```ruby
13
+ # handle :foo, with: Adapter::Method.new(:foo)
14
+ # ```
15
+ #
16
+ # However, you can short-circuit with a {Symbol}.
17
+ #
18
+ # ```ruby
19
+ # handle :foo, with: :foo
20
+ # ```
21
+ class Method
22
+ attr_reader :name
23
+
24
+ def initialize(name)
25
+ @name = name
26
+ end
27
+
28
+ # @api private
29
+ def call(instance)
30
+ check_method(instance)
31
+
32
+ ->(event) { instance.method(name).(event) }
33
+ end
34
+
35
+ private
36
+
37
+ def check_method(instance)
38
+ raise PrivateMethodSubscriptionAttemptError.new(method_name: name) if instance.private_methods.include?(name)
39
+
40
+ raise UnknownMethodSubscriptionAttemptError.new(method_name: name) unless instance.methods.include?(name)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+
5
+ module Omnes
6
+ module Subscriber
7
+ module Adapter
8
+ # [Sidekiq](https://sidekiq.org/) adapter
9
+ #
10
+ # Builds subscription to be processed as Sidekiq's background jobs.
11
+ #
12
+ # Sidekiq requires that the argument passed to `#perform` is serializable.
13
+ # By default, the result of calling `#payload` in the event is taken.
14
+ #
15
+ # ```
16
+ # class MySubscriber
17
+ # include Omnes::Subscriber
18
+ # include Sidekiq::Job
19
+ #
20
+ # handle :my_event, with: Adapter::Sidekiq
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
+ #
32
+ # However, you can configure how the event is serialized thanks to the
33
+ # `serializer:` option. It needs to be something callable taking the
34
+ # event as argument:
35
+ #
36
+ # ```
37
+ # handle :my_event, with: Adapter::Sidekiq[serializer: :serialized_payload.to_proc]
38
+ # ```
39
+ #
40
+ # You can also globally configure the default serializer:
41
+ #
42
+ # ```
43
+ # Omnes.config.subscriber.adapter.sidekiq.serializer = :serialized_payload.to_proc
44
+ # ```
45
+ #
46
+ # You can delay the callback execution from the publication time with the
47
+ # {.in} method (analogous to {Sidekiq::Job.perform_in}).
48
+ #
49
+ # @example
50
+ # handle :my_event, with: Adapter::Sidekiq.in(60)
51
+ module Sidekiq
52
+ extend Dry::Configurable
53
+
54
+ setting :serializer, default: :payload.to_proc
55
+
56
+ # @param serializer [#call]
57
+ def self.[](serializer: config.serializer)
58
+ Instance.new(serializer: serializer)
59
+ end
60
+
61
+ # @api private
62
+ def self.call(instance, event)
63
+ self.[].(instance, event)
64
+ end
65
+
66
+ # @param seconds [Integer]
67
+ def self.in(seconds)
68
+ self.[].in(seconds)
69
+ end
70
+
71
+ # @api private
72
+ class Instance
73
+ attr_reader :serializer
74
+
75
+ def initialize(serializer:)
76
+ @serializer = serializer
77
+ end
78
+
79
+ def call(instance, event)
80
+ instance.class.perform_async(serializer.(event))
81
+ end
82
+
83
+ def in(seconds)
84
+ lambda do |instance, event|
85
+ instance.class.perform_in(seconds, serializer.(event))
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omnes/subscriber/adapter/active_job"
4
+ require "omnes/subscriber/adapter/method"
5
+ require "omnes/subscriber/adapter/sidekiq"
6
+
7
+ module Omnes
8
+ module Subscriber
9
+ # Adapters to build {Omnes::Subscription}'s callbacks
10
+ #
11
+ # Adapters need to implement a method `#call` taking the instance of
12
+ # {Omnes::Subscriber} and the event.
13
+ #
14
+ # Alternatively, they can be curried and only take the instance as an
15
+ # argument, returning a one-argument callable taking the event.
16
+ module Adapter
17
+ # @api private
18
+ # TODO: Simplify when when we can take callables and Proc in a polymorphic
19
+ # way: https://bugs.ruby-lang.org/issues/18644
20
+ # > builder.to_proc.curry[instance]
21
+ def self.Type(value)
22
+ case value
23
+ when Symbol
24
+ Type(Method.new(value))
25
+ when Proc
26
+ value
27
+ else
28
+ value.method(:call)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omnes/errors"
4
+
5
+ module Omnes
6
+ module Subscriber
7
+ # Raised when subscribing the same subscriber instance to the same bus twice
8
+ class MultipleSubscriberSubscriptionAttemptError < Omnes::Error
9
+ # @api private
10
+ def initialize
11
+ super(default_message)
12
+ end
13
+
14
+ private
15
+
16
+ def default_message
17
+ <<~MSG
18
+ Omnes::Subscriber#subscribe_to method can only be called once for a
19
+ given instance on a given bus
20
+ MSG
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omnes/subscriber/adapter"
4
+ require "omnes/subscriber/errors"
5
+ require "omnes/subscription"
6
+
7
+ module Omnes
8
+ module Subscriber
9
+ # @api private
10
+ class State
11
+ attr_reader :subscription_definitions, :calling_cache, :autodiscover_strategy
12
+
13
+ def initialize(autodiscover_strategy:, subscription_definitions: [], calling_cache: [])
14
+ @subscription_definitions = subscription_definitions
15
+ @calling_cache = calling_cache
16
+ @autodiscover_strategy = autodiscover_strategy
17
+ end
18
+
19
+ def call(bus, instance)
20
+ raise MultipleSubscriberSubscriptionAttemptError if already_called?(bus, instance)
21
+
22
+ autodiscover_subscription_definitions(bus, instance) unless autodiscover_strategy.nil?
23
+
24
+ definitions = subscription_definitions.map { |defn| defn.(bus) }
25
+
26
+ subscribe_definitions(definitions, bus, instance).tap do
27
+ mark_as_called(bus, instance)
28
+ end
29
+ end
30
+
31
+ def add_subscription_definition(&block)
32
+ @subscription_definitions << block
33
+ end
34
+
35
+ private
36
+
37
+ def already_called?(bus, instance)
38
+ calling_cache.include?([bus, instance])
39
+ end
40
+
41
+ def mark_as_called(bus, instance)
42
+ @calling_cache << [bus, instance]
43
+ end
44
+
45
+ def autodiscover_subscription_definitions(bus, instance)
46
+ bus.registry.event_names.each do |event_name|
47
+ method_name = autodiscover_strategy.(event_name)
48
+ next unless instance.respond_to?(method_name, true)
49
+
50
+ add_subscription_definition do |_bus|
51
+ [
52
+ Subscription::SINGLE_EVENT_MATCHER.curry[event_name],
53
+ Adapter.Type(Adapter::Method.new(method_name))
54
+ ]
55
+ end
56
+ end
57
+ end
58
+
59
+ def subscribe_definitions(definitions, bus, instance)
60
+ matcher_with_callbacks = definitions.map do |(matcher, adapter)|
61
+ [matcher, adapter.curry[instance]]
62
+ end
63
+
64
+ matcher_with_callbacks.map { |matcher, callback| bus.subscribe_with_matcher(matcher, callback) }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "omnes/subscriber/adapter"
5
+ require "omnes/subscriber/state"
6
+ require "omnes/subscription"
7
+
8
+ module Omnes
9
+ # Supscriptions provider for a {Omnes::Bus}
10
+ #
11
+ # This module allows an including class to use its context to create event
12
+ # handlers.
13
+ #
14
+ # In its simplest form, you can match an event to a method in the class.
15
+ #
16
+ # ```
17
+ # class MySubscriber
18
+ # include Omnes::Subscriber
19
+ #
20
+ # handle :foo, with: :my_handler
21
+ #
22
+ # def my_handler(event)
23
+ # # do_something
24
+ # end
25
+ # end
26
+ # ```
27
+ #
28
+ # Equivalent to the subscribe methods in {Omnes::Bus}, you can also subscribe
29
+ # to all events or use a custom matcher:
30
+ #
31
+ # ```
32
+ # class MySubscriber
33
+ # include Omnes::Subscriber
34
+ #
35
+ # handle_all with: :my_handler_one
36
+ # handle_with_matcher my_matcher, with: :my_handler_two
37
+ #
38
+ # def my_handler_one(event)
39
+ # # do_something
40
+ # end
41
+ #
42
+ # def my_handler_two(event)
43
+ # # do_something_else
44
+ # end
45
+ # end
46
+ # ```
47
+ #
48
+ # Another option is to let the event handlers be automatically discovered. You
49
+ # need to enable the `autodiscover` feature and prefix the event name with
50
+ # `on_` for your handler name.
51
+ #
52
+ # ```
53
+ # class MySubscriber
54
+ # include Omnes::Subscriber[
55
+ # autodiscover: true
56
+ # ]
57
+ #
58
+ # def on_foo(event)
59
+ # # do_something
60
+ # end
61
+ # end
62
+ # ```
63
+ #
64
+ # If you prefer, you can make `autodiscover` on by default:
65
+ #
66
+ # ```
67
+ # Omnes.config.subscriber.autodiscover = true
68
+ # ```
69
+ #
70
+ # You can specify your own autodiscover strategy. It must be something
71
+ # callable, transforming the event name into the handler name.
72
+ #
73
+ # ```
74
+ # AUTODISCOVER_STRATEGY = ->(event_name) { event_name }
75
+ #
76
+ # class MySubscriber
77
+ # include Omnes::Subscriber[
78
+ # autodiscover: true,
79
+ # autodiscover_strategy: AUTODISCOVER_STRATEGY
80
+ # ]
81
+ #
82
+ # def foo(event)
83
+ # # do_something
84
+ # end
85
+ # end
86
+ # ```
87
+ # You're not limited to using method names as event handlers. You can create
88
+ # your own adapters from the subscriber instance context.
89
+ #
90
+ # ```
91
+ # ADAPTER = lambda do |instance, event|
92
+ # event.foo? ? instance.foo_true(event) : instance.foo_false(event)
93
+ # end
94
+ #
95
+ # class MySubscriber
96
+ # include Omnes::Subscriber
97
+ #
98
+ # handle :my_event, with: ADAPTER
99
+ #
100
+ # def foo_true(event)
101
+ # # do_something
102
+ # end
103
+ #
104
+ # def foo_false(event)
105
+ # # do_something_else
106
+ # end
107
+ # end
108
+ # ```
109
+ #
110
+ # Subscriber adapters can be leveraged to build integrations with background
111
+ # job libraries. See {Omnes::Subscriber::Adapter} for what comes shipped with
112
+ # the library.
113
+ #
114
+ # Once you've defined the event handlers, you can subscribe to a {Omnes::Bus}
115
+ # instance:
116
+ #
117
+ # ```
118
+ # MySubscriber.new.subscribe_to(bus)
119
+ # ```
120
+ #
121
+ # Notice that a subscriber instance can only be subscribed once to the same
122
+ # bus. However, you can subscribe distinct instances to the same bus or the
123
+ # same instance to different buses.
124
+ module Subscriber
125
+ extend Dry::Configurable
126
+
127
+ # @api private
128
+ ON_PREFIX_STRATEGY = ->(event_name) { :"on_#{event_name}" }
129
+
130
+ setting :autodiscover, default: false
131
+
132
+ setting :autodiscover_strategy, default: ON_PREFIX_STRATEGY
133
+
134
+ # Includes with options
135
+ #
136
+ # ```
137
+ # include Omnes::Subscriber[autodiscover: true]
138
+ # ```
139
+ #
140
+ # Use regular `include Omnes::Subscriber` in case you want to use the
141
+ # defaults (which can be changed through configuration).
142
+ #
143
+ # @param autodiscover [Boolean]
144
+ # @param autodiscover_strategy [#call]
145
+ def self.[](autodiscover: config.autodiscover, autodiscover_strategy: config.autodiscover_strategy)
146
+ Module.new(autodiscover_strategy: autodiscover ? autodiscover_strategy : nil)
147
+ end
148
+
149
+ # @api private
150
+ def self.included(klass)
151
+ klass.include(self.[])
152
+ end
153
+
154
+ # @api private
155
+ class Module < ::Module
156
+ attr_reader :autodiscover_strategy
157
+
158
+ def initialize(autodiscover_strategy:)
159
+ @autodiscover_strategy = autodiscover_strategy
160
+ super()
161
+ end
162
+
163
+ def included(klass)
164
+ klass.instance_variable_set(:@_mutex, Mutex.new)
165
+ klass.instance_variable_set(:@_state, State.new(autodiscover_strategy: autodiscover_strategy))
166
+ klass.extend(ClassMethods)
167
+ klass.include(InstanceMethods)
168
+ end
169
+ end
170
+
171
+ # Instance methods included in a {Omnes::Subscriber}
172
+ module InstanceMethods
173
+ # Subscribes event handlers to a bus
174
+ #
175
+ # @param bus [Omnes::Bus]
176
+ #
177
+ # @return [Omnes::Subscriber::Subscribers]
178
+ #
179
+ # @raise [Omnes::Subscriber::UnknownMethodSubscriptionAttemptError] when
180
+ # subscribing a method that doesn't exist
181
+ # @raise [Omnes::Subscriber::PrivateMethodSubscriptionAttemptError] when
182
+ # trying to subscribe a method that is private
183
+ # @raise [Omnes::Subscriber::DuplicateSubscriptionAttemptError] when
184
+ # trying to subscribe to the same event with the same method more than once
185
+ def subscribe_to(bus)
186
+ self.class.instance_variable_get(:@_state).public_send(:call, bus, self)
187
+ end
188
+ end
189
+
190
+ # Included DSL methods for a {Omnes::Subscriber}
191
+ module ClassMethods
192
+ # Match a single event name
193
+ #
194
+ # @param event_name [Symbol]
195
+ # @param with [Symbol, #call] Public method in the class or an adapter
196
+ def handle(event_name, with:)
197
+ @_mutex.synchronize do
198
+ @_state.add_subscription_definition do |bus|
199
+ bus.registry.check_event_name(event_name)
200
+ [Subscription::SINGLE_EVENT_MATCHER.curry[event_name], Adapter.Type(with)]
201
+ end
202
+ end
203
+ end
204
+
205
+ # Handles all events
206
+ #
207
+ # @param with [Symbol, #call] Public method in the class or an adapter
208
+ def handle_all(with:)
209
+ @_mutex.synchronize do
210
+ @_state.add_subscription_definition do |_bus|
211
+ [Subscription::ALL_EVENTS_MATCHER, Adapter.Type(with)]
212
+ end
213
+ end
214
+ end
215
+
216
+ # Handles events with a custom matcher
217
+ #
218
+ # @param matcher [#call]
219
+ # @param with [Symbol, #call] Public method in the class or an adapter
220
+ def handle_with_matcher(matcher, with:)
221
+ @_mutex.synchronize do
222
+ @_state.add_subscription_definition do |_bus|
223
+ [matcher, Adapter.Type(with)]
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+ require "omnes/execution"
5
+
6
+ module Omnes
7
+ # Subscription to an event
8
+ #
9
+ # An instance of it is returned on {Omnes::Bus} subscription methods.
10
+ #
11
+ # Usually, it isn't used directly beyond as a reference to unsubscribe.
12
+ #
13
+ # ```
14
+ # bus = Omnes::Bus.new
15
+ # bus.register(:foo)
16
+ # subscription = bus.subscribe(:foo) { |_event| do_something }
17
+ # bus.unsubscribe(subscription)
18
+ # ```
19
+ class Subscription
20
+ SINGLE_EVENT_MATCHER = lambda do |subscribed, candidate|
21
+ subscribed == candidate.omnes_event_name
22
+ end
23
+
24
+ ALL_EVENTS_MATCHER = ->(_candidate) { true }
25
+
26
+ # @api private
27
+ attr_reader :matcher, :callback
28
+
29
+ # @api private
30
+ def initialize(matcher:, callback:)
31
+ @matcher = matcher
32
+ @callback = callback
33
+ end
34
+
35
+ # @api private
36
+ def call(event)
37
+ result = nil
38
+ benchmark = Benchmark.measure do
39
+ result = @callback.(event)
40
+ end
41
+
42
+ Execution.new(subscription: self, result: result, benchmark: benchmark)
43
+ end
44
+
45
+ # @api private
46
+ def matches?(candidate)
47
+ matcher.(candidate)
48
+ end
49
+
50
+ # Returns self within a single-item array
51
+ #
52
+ # This method can be helpful to act polymorphic to an array of subscriptions
53
+ # from an {Omnes::Subscriber}, usually for testing purposes.
54
+ def subscriptions
55
+ [self]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnes
4
+ # Event with a payload defined at publication time
5
+ #
6
+ # An instance of it is automatically created on {Omnes::Bus#publish} when a
7
+ # name and payload are given.
8
+ #
9
+ # @example
10
+ # bus = Omnes::Bus.new
11
+ # bus.register(:foo)
12
+ # bus.subscribe(:foo) do |event|
13
+ # puts event[:bar]
14
+ # end
15
+ # bus.publish(:foo, bar: 'bar') # it'll generate an instance of this class
16
+ class UnstructuredEvent
17
+ # Name of the event
18
+ #
19
+ # @return [Symbol]
20
+ attr_reader :omnes_event_name
21
+
22
+ # Information made available to the matching subscriptions
23
+ #
24
+ # @return [Hash]
25
+ attr_reader :payload
26
+
27
+ # @api private
28
+ def initialize(payload:, omnes_event_name:)
29
+ @payload = payload
30
+ @omnes_event_name = omnes_event_name
31
+ end
32
+
33
+ # Delegates to {#payload}
34
+ #
35
+ # @param key [Any]
36
+ #
37
+ # @return Any
38
+ def [](key)
39
+ payload[key]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnes
4
+ VERSION = "0.1.0"
5
+ end