omnes 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68a21237b0a6749b982b68b732e134e79198654e92bc92824053c103280dd2bc
4
- data.tar.gz: a282239e0f4eb501e3eb113066f1753ebfc859364cddbf07fc9200efbcad6bad
3
+ metadata.gz: fa683052724423d76ee0b417e84e2d0ed954c47b50382eead7e8e85942eea8c0
4
+ data.tar.gz: e0690f6d8852c9ab3ff1d4e2fb7c6e68f82aca657fe6b684a503f14a085e0efc
5
5
  SHA512:
6
- metadata.gz: 929b7a25696e1eaeb3ece983d46ccada028530f859483ebf80a65ce0b603360696a7b8304dfb8d801d543f26206024163154baa6223991a79f06fa58329cf2eb
7
- data.tar.gz: fe5a4306002b44153992db90f13c54b76b211cfb7eb22975dfe2eb01a8fafd04bd7421dad414e2f82b38c5af00b0c466f52a624f0c0ce27f3ac1e7e711dbb382
6
+ metadata.gz: 41a41801b5b133640bca4c98f004163382fe6747848595a70fa1404c9638cca304cb8e95c0f7b6b81ebcebe15df1df32a4c28274b252d38d8d66fac572d988dd
7
+ data.tar.gz: 76a23f76d4a007cfadcf59140168934f2653804f65c7cbdad21a70e3dcd71177438b4eb2174da4b421d07fe694d793da271a5e40c2e61a2d5b77bd915e8869ce
data/CHANGELOG.md CHANGED
@@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.2.0] - 2022-04-15
8
+
9
+ ### Added
10
+ - Be able to fetch subscriptions by id from the bus [#1](https://github.com/nebulab/omnes/pull/1).
11
+ - Use ad-hoc configuration system (and make Omnes zero-deps) [#2](https://github.com/nebulab/omnes/pull/2).
12
+ - Bind a publication context to subscriptions [#3](https://github.com/nebulab/omnes/pull/3).
8
13
 
9
14
  ## [0.1.0] - 2022-03-23
10
15
 
11
- [Unreleased]: https://github.com/nebulab/omnes/compare/v0.1.0...HEAD
16
+ [Unreleased]: https://github.com/nebulab/omnes/compare/v0.2.0...HEAD
17
+ [0.2.0]: https://github.com/nebulab/omnes/compare/v0.1.0...v0.2.0
18
+ [0.1.0]: https://github.com/nebulab/omnes/releases/tag/v0.1.0
data/Gemfile.lock CHANGED
@@ -2,7 +2,6 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  omnes (0.1.0)
5
- dry-configurable (~> 0.14)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
@@ -19,11 +18,6 @@ GEM
19
18
  concurrent-ruby (1.1.9)
20
19
  connection_pool (2.2.5)
21
20
  diff-lcs (1.5.0)
22
- dry-configurable (0.14.0)
23
- concurrent-ruby (~> 1.0)
24
- dry-core (~> 0.6)
25
- dry-core (0.7.1)
26
- concurrent-ruby (~> 1.0)
27
21
  globalid (1.0.0)
28
22
  activesupport (>= 5.0)
29
23
  i18n (1.10.0)
data/README.md CHANGED
@@ -37,7 +37,7 @@ The following examples will use the direct `Omnes::Bus` instance. The only
37
37
  difference for the mixing use case is that the methods are directly called in
38
38
  the including instance.
39
39
 
40
- ### Registering events
40
+ ## Registering events
41
41
 
42
42
  Before being able to work with a given event, its name (which must be a
43
43
  `Symbol`) must be registered:
@@ -46,9 +46,9 @@ Before being able to work with a given event, its name (which must be a
46
46
  bus.register(:order_created)
47
47
  ```
48
48
 
49
- ### Publishing events
49
+ ## Publishing events
50
50
 
51
- An event can be anything responding to a method `:name`, which must match with a
51
+ An event can be anything responding to a method `:omnes_event_name`, which must match with a
52
52
  registered name.
53
53
 
54
54
  Typically, there're two main ways to generate events.
@@ -73,8 +73,7 @@ features, such as event persistence, can't be reliably built on top of them.
73
73
 
74
74
  You can also publish an instance of a class including
75
75
  [`Omnes::Event`](lib/omnes/event.rb). The only fancy thing it provides is an
76
- OOTB event name generated based on the class name. In fact, you can use
77
- anything responding to `#omnes_event_name`.
76
+ OOTB event name generated based on the class name.
78
77
 
79
78
  ```ruby
80
79
  class OrderCreatedEvent
@@ -105,7 +104,7 @@ Omnes.config.event.name_builder = event_name_as_class
105
104
  Instance-backed events provide a well-defined structure, and other features,
106
105
  like event persistence, can be added on top of them.
107
106
 
108
- ### Subscribing to events
107
+ ## Subscribing to events
109
108
 
110
109
  You can subscribe to a specific event to run some code whenever it's published.
111
110
  The event is yielded to the subscription block:
@@ -150,7 +149,7 @@ bus.subscribe(:order_created, OrderCreationEmailSubscription.new)
150
149
  However, see [Event subscribers](#event-subscribers) section bellow for a more powerful way
151
150
  to define standalone event handlers.
152
151
 
153
- #### Global subscriptions
152
+ ### Global subscriptions
154
153
 
155
154
  You can also create a subscription that will run for all events:
156
155
 
@@ -170,7 +169,7 @@ end
170
169
  bus.subscribe_to_all(LogEventsSubscription.new)
171
170
  ```
172
171
 
173
- #### Custom matcher subscriptions
172
+ ### Custom matcher subscriptions
174
173
 
175
174
  Custom event matchers can be defined. A matcher is something responding to
176
175
  `#call` and taking the event as an argument. It must return `true` or `false`
@@ -184,6 +183,22 @@ bus.subscribe_with_matcher(ORDER_EVENTS_MATCHER) do |event|
184
183
  end
185
184
  ```
186
185
 
186
+ ### Referencing subscriptions
187
+
188
+ For all subscription methods we've seen, an `Omnes::Subscription` instance is
189
+ returned. Holding that reference can be useful for [debugging](#debugging) and
190
+ [testing](#testing) purposes.
191
+
192
+ Often though, you won't have the reference at hand when you need it.
193
+ Thankfully, you can provide a subscription identifier on subscription time and
194
+ use it later to fetch the subscription instance from the bus. A subscription
195
+ identifier needs to be a `Symbol`:
196
+
197
+ ```ruby
198
+ bus.subscribe(:order_created, OrderCreationEmailSubscription.new, id: :order_created_email)
199
+ subscription = bus.subscription(:send_confirmation_email)
200
+ ```
201
+
187
202
  ## Event subscribers
188
203
 
189
204
  Events subscribers offer a way to define event subscriptions from a custom
@@ -207,7 +222,11 @@ class OrderCreationEmailSubscriber
207
222
  service.send(number: event.number, email: event.user_email)
208
223
  end
209
224
  end
225
+ ```
210
226
 
227
+ You add the subscriptions by calling the `#subscribe_to` method on an instance:
228
+
229
+ ```ruby
211
230
  OrderCreationEmailSubscriber.new.subscribe_to(bus)
212
231
  ```
213
232
 
@@ -246,6 +265,21 @@ class OrderSubscriber
246
265
  end
247
266
  ```
248
267
 
268
+ Likewise, you can provide [identifiers to reference
269
+ subscriptions](#referencing-subscriptions):
270
+
271
+ ```ruby
272
+ handle :order_created, with: :send_confirmation_email, id: :order_creation_email_subscriber
273
+ ```
274
+
275
+ As you can subscribe multiple instances of a subscriber to the same bus, you
276
+ might need to create a different identifier for each of them. For those cases,
277
+ you can pass a lambda taking the subscriber instance:
278
+
279
+ ```ruby
280
+ handle :order_created, with: :send_confirmation_email, id: ->(subscriber) { :"#{subscriber.id}_order_creation_email_subscriber" }
281
+ ```
282
+
249
283
  ### Autodiscovering event handlers
250
284
 
251
285
  You can let the event handlers to be automatically discovered.You need to
@@ -292,6 +326,12 @@ class OrderCreationEmailSubscriber
292
326
  end
293
327
  ```
294
328
 
329
+ The strategy can also be globally set:
330
+
331
+ ```ruby
332
+ Omnes.config.subscriber.autodiscover_strategy = AUTODISCOVER_STRATEGY
333
+ ```
334
+
295
335
  ### Adapters
296
336
 
297
337
  Subscribers are not limited to use a method as event handler. They can interact
@@ -389,7 +429,8 @@ Omnes.config.subscriber.adapter.active_job.serializer = :serialized_payload.to_p
389
429
  #### Custom adapters
390
430
 
391
431
  Custom adapters can be built. They need to implement a method `#call` taking
392
- the instance of `Omnes::Subscriber` and the event.
432
+ the instance of `Omnes::Subscriber`, the event and, optionally, the publication
433
+ context (see [debugging subscriptions](#subscription)).
393
434
 
394
435
  Here's a custom adapter executing a subscriber method in a different
395
436
  thread (we add an extra argument for the method name, and we partially apply it
@@ -402,7 +443,6 @@ end
402
443
 
403
444
  class OrderCreationEmailSubscriber
404
445
  include Omnes::Subscriber
405
- include Sidekiq::Job
406
446
 
407
447
  handle :order_created, with: THREAD_ADAPTER.curry[:order_created]
408
448
 
@@ -413,8 +453,8 @@ end
413
453
  ```
414
454
 
415
455
  Alternatively, adapters can be curried and only take the instance as an
416
- argument, returning a one-argument callable taking the event. For instance, we
417
- could also have defined the thread adapter like this:
456
+ argument, returning a callable taking the event. For instance, we could also
457
+ have defined the thread adapter like this:
418
458
 
419
459
  ```ruby
420
460
  class ThreadAdapter
@@ -440,10 +480,8 @@ handle :order_created, with: ThreadAdapter.new(:order_created)
440
480
 
441
481
  ### Unsubscribing
442
482
 
443
- When you create a subscription, an instance of
444
- [`Omnes::Subscription`](lib/omnes/subscription.rb) is returned (or an array for
445
- `Omnes::Subscriber`). It can be used to unsubscribe it in case you need to
446
- debug some behavior.
483
+ You can unsubscribe a given subscription by passing its
484
+ [reference](#referencing-subscriptions) to `Omnes::Bus#unsubscribe`:
447
485
 
448
486
  ```ruby
449
487
  subscription = bus.subscribe(:order_created, OrderCreationEmailSubscription.new)
@@ -478,10 +516,10 @@ When you publish an event, you get back an
478
516
  attributes that allow observing what happened:
479
517
 
480
518
  - `#event` contains the event instance that has been published.
481
- - `#caller_location` refers to the publication caller.
482
- - `#time` is the time stamp for the publication.
483
519
  - `#executions` contains an array of
484
520
  `Omnes::Execution`(lib/omnes/execution.rb). Read more below.
521
+ - `#context` is an instance of
522
+ [`Omnes::PublicationContext`](lib/omnes/publication_context.rb).
485
523
 
486
524
  `Omnes::Execution` represents a subscription individual execution. It contains
487
525
  the following attributes:
@@ -491,6 +529,40 @@ the following attributes:
491
529
  - `#benchmark` of the operation.
492
530
  - `#time` is the time where the execution started.
493
531
 
532
+ `Omnes::PublicationContext` represents the shared context for all triggered
533
+ executions. See [Subscription][#subscription] for details.
534
+
535
+ ### Subscription
536
+
537
+ If your subscription block or callable object takes a second argument, it'll
538
+ contain an instance of an
539
+ [`Omnes::PublicationContext`](lib/omnes/publication_context.rb). It allows you
540
+ to inspect what triggered a given execution from within that execution code. It
541
+ contains:
542
+
543
+ - `#caller_location` refers to the publication caller.
544
+ - `#time` is the time stamp for the publication.
545
+
546
+ ```ruby
547
+ class OrderCreationEmailSubscriber
548
+ include Omnes::Subscriber
549
+
550
+ handle :order_created, with: :send_confirmation_email
551
+
552
+ def send_confirmation_email(event, publication_context)
553
+ # debugging
554
+ abort(publication_context.caller_location.inspect)
555
+
556
+ OrderCreationEmail.send(number: event.number, email: event.user_email)
557
+ end
558
+ end
559
+ ```
560
+
561
+ In case you're developing your own async adapter, you can call `#serialized` on
562
+ an instance of `Omnes::PublicationContext` to get a serialized version of it.
563
+ It'll return a `Hash` with `"caller_location"` and `"time"` keys, and the
564
+ respective `String` representations as values.
565
+
494
566
  ## Testing
495
567
 
496
568
  Ideally, you wouldn't need big setups to test your event-driven behavior. You
@@ -521,11 +593,30 @@ end
521
593
  bus.publish(:order_deleted, number: order.number) # `deletion_subscription` will run
522
594
  ```
523
595
 
524
- Remember that the array of created subscriptions is returned on `Omnes::Subscriber#subscribe_to`.
596
+ Remember that you can get previous [subscription
597
+ references](#referencing-subscriptions) thanks to
598
+ subscription identifiers.
525
599
 
526
600
  There's also a specialized `Omnes::Bus#performing_nothing` method that runs no
527
601
  subscriptions for the duration of the block.
528
602
 
603
+ ## Configuration
604
+
605
+ We've seen the relevant configurable settings in the corresponding sections.
606
+ You can also access the configuration in the habitual block syntax:
607
+
608
+ ```ruby
609
+ Omnes.configure do |config|
610
+ config.subscriber.adapter.sidekiq.serializer = :serialized_payload.to_proc
611
+ end
612
+ ```
613
+
614
+ Finally, nested settings can also be set directly from the affected class. E.g.:
615
+
616
+ ```ruby
617
+ Omnes::Subscriber::Adapter::Sidekiq.config.serializer = :serialized_payload.to_proc
618
+ ```
619
+
529
620
  ## Recipes
530
621
 
531
622
  ### Rails
data/lib/omnes/bus.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "omnes/publication"
4
+ require "omnes/publication_context"
4
5
  require "omnes/registry"
5
6
  require "omnes/subscription"
6
7
  require "omnes/unstructured_event"
@@ -22,8 +23,8 @@ module Omnes
22
23
  # bus.register(:foo)
23
24
  # ```
24
25
  #
25
- # An event can be anything responding to a method `:name` which, needless to
26
- # say, must match with a registered name.
26
+ # An event can be anything responding to a method `:omes_event_name` which,
27
+ # needless to say, must match with a registered name.
27
28
  #
28
29
  # Typically, there're two main ways to generate events.
29
30
  #
@@ -104,6 +105,21 @@ module Omnes
104
105
  #
105
106
  # bus.subscribe_with_matcher(matcher, MySubscription.new)
106
107
  # ```
108
+ #
109
+ # For all previous subscription methods, a subscription object is returned.
110
+ # You can supply a subscription id to it to be able to fetch it from the bus
111
+ # later on:
112
+ #
113
+ # ```
114
+ # subscription = bus.subscribe(:foo, MySubscription.new, id: :foo_sub)
115
+ # bus.subscription(:foo_sub) == subscription #=> true
116
+ # ```
117
+ #
118
+ # A subscription can be referenced when you want to unsubscribe:
119
+ #
120
+ # ```
121
+ # bus.unsubscribe(subscription)
122
+ # ```
107
123
  class Bus
108
124
  # @api private
109
125
  def self.EventType(value, **payload)
@@ -168,13 +184,13 @@ module Omnes
168
184
  publication_time = Time.now.utc
169
185
  event = self.class.EventType(event, **payload)
170
186
  registry.check_event_name(event.omnes_event_name)
171
- executions = execute_subscriptions_for_event(event)
187
+ publication_context = PublicationContext.new(caller_location: caller_location, time: publication_time)
188
+ executions = execute_subscriptions_for_event(event, publication_context)
172
189
 
173
190
  Publication.new(
174
191
  event: event,
175
192
  executions: executions,
176
- caller_location: caller_location,
177
- time: publication_time
193
+ context: publication_context
178
194
  )
179
195
  end
180
196
 
@@ -182,37 +198,42 @@ module Omnes
182
198
  #
183
199
  # @param event_name [Symbol] Name of the event
184
200
  # @param callable [#call] Subscription callback taking the event
201
+ # @param id [Symbol] Unique identifier for the subscription
185
202
  # @yield [event] Subscription callback if callable is not given
186
203
  #
187
204
  # @return [Omnes::Subscription]
188
205
  #
189
206
  # @raise [Omnes::UnknownEventError] When event name has not been registered
190
- def subscribe(event_name, callable = nil, &block)
207
+ def subscribe(event_name, callable = nil, id: Subscription.random_id, &block)
191
208
  registry.check_event_name(event_name)
192
209
 
193
- subscribe_with_matcher(Subscription::SINGLE_EVENT_MATCHER.curry[event_name], callable, &block)
210
+ subscribe_with_matcher(Subscription::SINGLE_EVENT_MATCHER.curry[event_name], callable, id: id, &block)
194
211
  end
195
212
 
196
213
  # Adds a subscription for all events
197
214
  #
198
215
  # @param callable [#call] Subscription callback taking the event
216
+ # @param id [Symbol] Unique identifier for the subscription
199
217
  # @yield [event] Subscription callback if callable is not given
200
218
  #
201
219
  # @return [Omnes::Subscription]
202
- def subscribe_to_all(callable = nil, &block)
203
- subscribe_with_matcher(Subscription::ALL_EVENTS_MATCHER, callable, &block)
220
+ def subscribe_to_all(callable = nil, id: Subscription.random_id, &block)
221
+ subscribe_with_matcher(Subscription::ALL_EVENTS_MATCHER, callable, id: id, &block)
204
222
  end
205
223
 
206
224
  # Adds a subscription with given matcher
207
225
  #
208
226
  # @param matcher [#call] Callable taking the event and returning a boolean
209
227
  # @param callable [#call] Subscription callback taking the event
228
+ # @param id [Symbol] Unique identifier for the subscription
210
229
  # @yield [event] Subscription callback if callable is not given
211
230
  #
212
231
  # @return [Omnes::Subscription]
213
- def subscribe_with_matcher(matcher, callable = nil, &block)
232
+ def subscribe_with_matcher(matcher, callable = nil, id: Subscription.random_id, &block)
233
+ raise DuplicateSubscriptionIdError.new(id: id, bus: self) if subscription(id)
234
+
214
235
  callback = callable || block
215
- Subscription.new(matcher: matcher, callback: callback).tap do |subscription|
236
+ Subscription.new(matcher: matcher, callback: callback, id: id).tap do |subscription|
216
237
  @subscriptions << subscription
217
238
  end
218
239
  end
@@ -257,11 +278,20 @@ module Omnes
257
278
  performing_only(&block)
258
279
  end
259
280
 
281
+ # Fetch a subscription by its identifier
282
+ #
283
+ # @param id [Symbol] Subscription identifier
284
+ #
285
+ # @return [Omnes::Subscription]
286
+ def subscription(id)
287
+ subscriptions.find { |subscription| subscription.id == id }
288
+ end
289
+
260
290
  private
261
291
 
262
- def execute_subscriptions_for_event(event)
292
+ def execute_subscriptions_for_event(event, publication_context)
263
293
  subscriptions_for_event(event).map do |subscription|
264
- subscription.(event)
294
+ subscription.(event, publication_context)
265
295
  end
266
296
  end
267
297
 
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnes
4
+ # Ad-hoc configurable behavior for Omnes
5
+ #
6
+ # Example:
7
+ #
8
+ # ```
9
+ # Omnes.configure do |config|
10
+ # config.event.name_builder = MY_NAME_BUILDER
11
+ # end
12
+ # ```
13
+ #
14
+ # or
15
+ #
16
+ # ```
17
+ # Omnes::Event.config.name_builder = MY_NAME_BUILDER
18
+ # ```
19
+ module Configurable
20
+ # Class where readers and writers are defined
21
+ class Config
22
+ # @api private
23
+ attr_reader :settings
24
+
25
+ # @api private
26
+ def initialize
27
+ @_mutex = Mutex.new
28
+ @settings = {}
29
+ end
30
+
31
+ # @api private
32
+ def add_setting(name, default)
33
+ @_mutex.synchronize do
34
+ @settings[name] = default
35
+ define_setting_reader(name)
36
+ define_setting_writter(name)
37
+ end
38
+ end
39
+
40
+ # @api private
41
+ def add_nesting(constant, name = default_nesting_name(constant))
42
+ @_mutex.synchronize do
43
+ define_nesting_reader(constant, name)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def define_setting_reader(name)
50
+ define_singleton_method(name) { @settings[name] }
51
+ end
52
+
53
+ def define_setting_writter(name)
54
+ define_singleton_method(:"#{name}=") do |value|
55
+ @_mutex.synchronize do
56
+ @settings[name] = value
57
+ end
58
+ end
59
+ end
60
+
61
+ def define_nesting_reader(constant, name)
62
+ define_singleton_method(name) { constant.config }
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ def self.extended(klass)
68
+ klass.instance_variable_set(:@config, Config.new)
69
+ end
70
+
71
+ # Returns the configuration class
72
+ #
73
+ # Use this class to access readers and writers for the defined settings or
74
+ # nested configurations
75
+ #
76
+ # @return [Configurable::Config]
77
+ def config
78
+ @config
79
+ end
80
+
81
+ # Yields the configuration class
82
+ #
83
+ # @see #config
84
+ def configure
85
+ yield @config
86
+ end
87
+
88
+ # @api private
89
+ def setting(name, default:)
90
+ config.add_setting(name, default)
91
+ end
92
+
93
+ # @api private
94
+ def nest_config(constant, name: default_nesting_name(constant))
95
+ config.add_nesting(constant, name)
96
+ end
97
+
98
+ private
99
+
100
+ def default_nesting_name(constant)
101
+ constant.name
102
+ .split("::")
103
+ .last
104
+ .gsub(/([[:alpha:]])([[:upper:]])/, '\1_\2')
105
+ .downcase
106
+ end
107
+ end
108
+ end
data/lib/omnes/errors.rb CHANGED
@@ -98,4 +98,42 @@ module Omnes
98
98
  MSG
99
99
  end
100
100
  end
101
+
102
+ # Raised when given subscription id is already in use
103
+ class DuplicateSubscriptionIdError < Error
104
+ attr_reader :id, :bus
105
+
106
+ def initialize(id:, bus:)
107
+ @id = id
108
+ @bus = bus
109
+ super(default_message)
110
+ end
111
+
112
+ private
113
+
114
+ def default_message
115
+ <<~MSG
116
+ #{id} has already been used as a subscription identifier
117
+ MSG
118
+ end
119
+ end
120
+
121
+ # Raised when trying to set an invalid subscription identifier
122
+ class InvalidSubscriptionNameError < Error
123
+ attr_reader :id
124
+
125
+ def initialize(id:)
126
+ @id = id
127
+ super(default_message)
128
+ end
129
+
130
+ private
131
+
132
+ def default_message
133
+ <<~MSG
134
+ #{id.inspect} is not a valid subscription identifier. Only symbols are
135
+ #allowed.
136
+ MSG
137
+ end
138
+ end
101
139
  end
data/lib/omnes/event.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable"
3
+ require "omnes/configurable"
4
4
 
5
5
  module Omnes
6
6
  # Event mixin for custom classes
@@ -27,7 +27,7 @@ module Omnes
27
27
  # bus.publish(MyEvent.new(1))
28
28
  # ```
29
29
  module Event
30
- extend Dry::Configurable
30
+ extend Configurable
31
31
 
32
32
  # Generates the event name for an event instance
33
33
  #
@@ -19,25 +19,16 @@ module Omnes
19
19
  # @return [Array<Omnes::Execution>]
20
20
  attr_reader :executions
21
21
 
22
- # Location for the event caller
22
+ # Publication context, shared by all triggered executions
23
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
24
+ # @return [Omnes::PublicationContext]
25
+ attr_reader :context
34
26
 
35
27
  # @api private
36
- def initialize(event:, executions:, caller_location:, time:)
28
+ def initialize(event:, executions:, context:)
37
29
  @event = event
38
30
  @executions = executions
39
- @caller_location = caller_location
40
- @time = time
31
+ @context = context
41
32
  end
42
33
  end
43
34
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnes
4
+ # Context for an event publication
5
+ #
6
+ # An instance of this class is shared between all the executions that are
7
+ # triggered by the publication of a given event. It's provided to the
8
+ # subscriptions as their second argument when they take it.
9
+ #
10
+ # This class is useful mainly for debugging and logging purposes.
11
+ class PublicationContext
12
+ # Location for the event publisher
13
+ #
14
+ # It's set by {Omnes::Bus#publish}, and it points to the caller of that
15
+ # method.
16
+ #
17
+ # @return [Thread::Backtrace::Location]
18
+ attr_reader :caller_location
19
+
20
+ # Time of the event publication
21
+ #
22
+ # @return [Time]
23
+ attr_reader :time
24
+
25
+ # @api private
26
+ def initialize(caller_location:, time:)
27
+ @caller_location = caller_location
28
+ @time = time
29
+ end
30
+
31
+ # Serialized version of a publication context
32
+ #
33
+ # @return Hash<String, String>
34
+ def serialized
35
+ {
36
+ "caller_location" => caller_location.to_s,
37
+ "time" => time.to_s
38
+ }
39
+ end
40
+ end
41
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable"
3
+ require "omnes/configurable"
4
4
 
5
5
  module Omnes
6
6
  module Subscriber
@@ -42,7 +42,7 @@ module Omnes
42
42
  # Omnes.config.subscriber.adapter.active_job.serializer = :serialized_payload.to_proc
43
43
  # ```
44
44
  module ActiveJob
45
- extend Dry::Configurable
45
+ extend Configurable
46
46
 
47
47
  setting :serializer, default: :payload.to_proc
48
48
 
@@ -52,8 +52,8 @@ module Omnes
52
52
  end
53
53
 
54
54
  # @api private
55
- def self.call(instance, event)
56
- self.[].(instance, event)
55
+ def self.call(instance, event, publication_context)
56
+ self.[].(instance, event, publication_context)
57
57
  end
58
58
 
59
59
  # @api private
@@ -64,8 +64,12 @@ module Omnes
64
64
  @serializer = serializer
65
65
  end
66
66
 
67
- def call(instance, event)
68
- instance.class.perform_later(serializer.(event))
67
+ def call(instance, event, publication_context)
68
+ if Subscription.takes_publication_context?(instance.method(:perform))
69
+ instance.class.perform_later(serializer.(event), publication_context.serialized)
70
+ else
71
+ instance.class.perform_later(serializer.(event))
72
+ end
69
73
  end
70
74
  end
71
75
  end
@@ -7,7 +7,7 @@ module Omnes
7
7
  module Adapter
8
8
  # Builds a callback from a method of the instance
9
9
  #
10
- # You can use instance of this class as the adapter:
10
+ # You can use an instance of this class as the adapter:
11
11
  #
12
12
  # ```ruby
13
13
  # handle :foo, with: Adapter::Method.new(:foo)
@@ -29,7 +29,7 @@ module Omnes
29
29
  def call(instance)
30
30
  check_method(instance)
31
31
 
32
- ->(event) { instance.method(name).(event) }
32
+ instance.method(name)
33
33
  end
34
34
 
35
35
  private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable"
3
+ require "omnes/configurable"
4
4
 
5
5
  module Omnes
6
6
  module Subscriber
@@ -49,7 +49,7 @@ module Omnes
49
49
  # @example
50
50
  # handle :my_event, with: Adapter::Sidekiq.in(60)
51
51
  module Sidekiq
52
- extend Dry::Configurable
52
+ extend Configurable
53
53
 
54
54
  setting :serializer, default: :payload.to_proc
55
55
 
@@ -59,8 +59,8 @@ module Omnes
59
59
  end
60
60
 
61
61
  # @api private
62
- def self.call(instance, event)
63
- self.[].(instance, event)
62
+ def self.call(instance, event, publication_context)
63
+ self.[].(instance, event, publication_context)
64
64
  end
65
65
 
66
66
  # @param seconds [Integer]
@@ -76,15 +76,29 @@ module Omnes
76
76
  @serializer = serializer
77
77
  end
78
78
 
79
- def call(instance, event)
80
- instance.class.perform_async(serializer.(event))
79
+ def call(instance, event, publication_context)
80
+ if takes_publication_context?(instance)
81
+ instance.class.perform_async(serializer.(event), publication_context.serialized)
82
+ else
83
+ instance.class.perform_async(serializer.(event))
84
+ end
81
85
  end
82
86
 
83
87
  def in(seconds)
84
- lambda do |instance, event|
85
- instance.class.perform_in(seconds, serializer.(event))
88
+ lambda do |instance, event, publication_context|
89
+ if takes_publication_context?(instance)
90
+ instance.class.perform_in(seconds, serializer.(event), publication_context.serialized)
91
+ else
92
+ instance.class.perform_in(seconds, serializer.(event))
93
+ end
86
94
  end
87
95
  end
96
+
97
+ private
98
+
99
+ def takes_publication_context?(instance)
100
+ Subscription.takes_publication_context?(instance.method(:perform))
101
+ end
88
102
  end
89
103
  end
90
104
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "omnes/configurable"
3
4
  require "omnes/subscriber/adapter/active_job"
4
5
  require "omnes/subscriber/adapter/method"
5
6
  require "omnes/subscriber/adapter/sidekiq"
@@ -14,6 +15,11 @@ module Omnes
14
15
  # Alternatively, they can be curried and only take the instance as an
15
16
  # argument, returning a one-argument callable taking the event.
16
17
  module Adapter
18
+ extend Configurable
19
+
20
+ nest_config Sidekiq
21
+ nest_config ActiveJob
22
+
17
23
  # @api private
18
24
  # TODO: Simplify when when we can take callables and Proc in a polymorphic
19
25
  # way: https://bugs.ruby-lang.org/issues/18644
@@ -10,6 +10,11 @@ module Omnes
10
10
  class State
11
11
  attr_reader :subscription_definitions, :calling_cache, :autodiscover_strategy
12
12
 
13
+ # @api private
14
+ def self.IdType(value)
15
+ value.respond_to?(:call) ? value : ->(_instance) { value }
16
+ end
17
+
13
18
  def initialize(autodiscover_strategy:, subscription_definitions: [], calling_cache: [])
14
19
  @subscription_definitions = subscription_definitions
15
20
  @calling_cache = calling_cache
@@ -21,7 +26,7 @@ module Omnes
21
26
 
22
27
  autodiscover_subscription_definitions(bus, instance) unless autodiscover_strategy.nil?
23
28
 
24
- definitions = subscription_definitions.map { |defn| defn.(bus) }
29
+ definitions = subscription_definitions.map { |defn| defn.(bus, instance) }
25
30
 
26
31
  subscribe_definitions(definitions, bus, instance).tap do
27
32
  mark_as_called(bus, instance)
@@ -50,18 +55,19 @@ module Omnes
50
55
  add_subscription_definition do |_bus|
51
56
  [
52
57
  Subscription::SINGLE_EVENT_MATCHER.curry[event_name],
53
- Adapter.Type(Adapter::Method.new(method_name))
58
+ Adapter.Type(Adapter::Method.new(method_name)),
59
+ Subscription.random_id
54
60
  ]
55
61
  end
56
62
  end
57
63
  end
58
64
 
59
65
  def subscribe_definitions(definitions, bus, instance)
60
- matcher_with_callbacks = definitions.map do |(matcher, adapter)|
61
- [matcher, adapter.curry[instance]]
66
+ matcher_with_callbacks = definitions.map do |(matcher, adapter, id)|
67
+ [matcher, adapter.curry[instance], id]
62
68
  end
63
69
 
64
- matcher_with_callbacks.map { |matcher, callback| bus.subscribe_with_matcher(matcher, callback) }
70
+ matcher_with_callbacks.map { |matcher, callback, id| bus.subscribe_with_matcher(matcher, callback, id: id) }
65
71
  end
66
72
  end
67
73
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable"
4
3
  require "omnes/subscriber/adapter"
5
4
  require "omnes/subscriber/state"
6
5
  require "omnes/subscription"
@@ -122,14 +121,14 @@ module Omnes
122
121
  # bus. However, you can subscribe distinct instances to the same bus or the
123
122
  # same instance to different buses.
124
123
  module Subscriber
125
- extend Dry::Configurable
124
+ extend Configurable
126
125
 
127
126
  # @api private
128
127
  ON_PREFIX_STRATEGY = ->(event_name) { :"on_#{event_name}" }
129
128
 
130
129
  setting :autodiscover, default: false
131
-
132
130
  setting :autodiscover_strategy, default: ON_PREFIX_STRATEGY
131
+ nest_config Adapter
133
132
 
134
133
  # Includes with options
135
134
  #
@@ -193,11 +192,12 @@ module Omnes
193
192
  #
194
193
  # @param event_name [Symbol]
195
194
  # @param with [Symbol, #call] Public method in the class or an adapter
196
- def handle(event_name, with:)
195
+ # @param id [Symbol] Unique identifier for the subscription
196
+ def handle(event_name, with:, id: Subscription.random_id)
197
197
  @_mutex.synchronize do
198
- @_state.add_subscription_definition do |bus|
198
+ @_state.add_subscription_definition do |bus, instance|
199
199
  bus.registry.check_event_name(event_name)
200
- [Subscription::SINGLE_EVENT_MATCHER.curry[event_name], Adapter.Type(with)]
200
+ [Subscription::SINGLE_EVENT_MATCHER.curry[event_name], Adapter.Type(with), State.IdType(id).(instance)]
201
201
  end
202
202
  end
203
203
  end
@@ -205,10 +205,11 @@ module Omnes
205
205
  # Handles all events
206
206
  #
207
207
  # @param with [Symbol, #call] Public method in the class or an adapter
208
- def handle_all(with:)
208
+ # @param id [Symbol] Unique identifier for the subscription
209
+ def handle_all(with:, id: Subscription.random_id)
209
210
  @_mutex.synchronize do
210
- @_state.add_subscription_definition do |_bus|
211
- [Subscription::ALL_EVENTS_MATCHER, Adapter.Type(with)]
211
+ @_state.add_subscription_definition do |_bus, instance|
212
+ [Subscription::ALL_EVENTS_MATCHER, Adapter.Type(with), State.IdType(id).(instance)]
212
213
  end
213
214
  end
214
215
  end
@@ -217,10 +218,11 @@ module Omnes
217
218
  #
218
219
  # @param matcher [#call]
219
220
  # @param with [Symbol, #call] Public method in the class or an adapter
220
- def handle_with_matcher(matcher, with:)
221
+ # @param id [Symbol] Unique identifier for the subscription
222
+ def handle_with_matcher(matcher, with:, id: Subscription.random_id)
221
223
  @_mutex.synchronize do
222
- @_state.add_subscription_definition do |_bus|
223
- [matcher, Adapter.Type(with)]
224
+ @_state.add_subscription_definition do |_bus, instance|
225
+ [matcher, Adapter.Type(with), State.IdType(id).(instance)]
224
226
  end
225
227
  end
226
228
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "benchmark"
4
4
  require "omnes/execution"
5
+ require "securerandom"
5
6
 
6
7
  module Omnes
7
8
  # Subscription to an event
@@ -24,19 +25,39 @@ module Omnes
24
25
  ALL_EVENTS_MATCHER = ->(_candidate) { true }
25
26
 
26
27
  # @api private
27
- attr_reader :matcher, :callback
28
+ def self.random_id
29
+ SecureRandom.uuid.to_sym
30
+ end
31
+
32
+ # @api private
33
+ def self.takes_publication_context?(callable)
34
+ callable.parameters.count == 2
35
+ end
28
36
 
29
37
  # @api private
30
- def initialize(matcher:, callback:)
38
+ attr_reader :matcher, :callback, :id
39
+
40
+ # @api private
41
+ def initialize(matcher:, callback:, id:)
42
+ raise Omnes::InvalidSubscriptionNameError.new(id: id) unless id.is_a?(Symbol)
43
+
31
44
  @matcher = matcher
32
45
  @callback = callback
46
+ @id = id
33
47
  end
34
48
 
35
49
  # @api private
36
- def call(event)
50
+ def call(event, publication_context)
37
51
  result = nil
38
52
  benchmark = Benchmark.measure do
39
- result = @callback.(event)
53
+ # work around Ruby not being able to tell remaining arity for a curried
54
+ # function (or uncurrying), because we want to be able to create subscriber
55
+ # adapters partially applying the subscriber instance
56
+ result = begin
57
+ @callback.(event, publication_context)
58
+ rescue ArgumentError
59
+ @callback.(event)
60
+ end
40
61
  end
41
62
 
42
63
  Execution.new(subscription: self, result: result, benchmark: benchmark)
data/lib/omnes/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnes
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/omnes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "omnes/bus"
4
+ require "omnes/configurable"
4
5
  require "omnes/event"
5
6
  require "omnes/subscriber"
6
7
  require "omnes/version"
@@ -33,43 +34,10 @@ require "omnes/version"
33
34
  # Refer to {Omnes::Subscriber} for how to provide event handlers through methods
34
35
  # defined in a class.
35
36
  module Omnes
36
- # Shortcut to access the configuration for different Omnes components
37
- #
38
- # TODO: Make automation for it
39
- #
40
- # @return [Omnes::Config]
41
- def self.config
42
- Config
43
- end
44
-
45
- # Wrapper for the configuration of Omnes components
46
- module Config
47
- # {Omnes::Subscriber} configuration
48
- #
49
- # @return [Dry::Configurable::Config]
50
- def self.subscriber
51
- Omnes::Subscriber.config.tap do |klass|
52
- klass.define_singleton_method(:adapter) do
53
- Module.new do
54
- def self.sidekiq
55
- Omnes::Subscriber::Adapter::Sidekiq.config
56
- end
37
+ extend Configurable
57
38
 
58
- def self.active_job
59
- Omnes::Subscriber::Adapter::ActiveJob.config
60
- end
61
- end
62
- end
63
- end
64
- end
65
-
66
- # {Omnes::Event} configuration
67
- #
68
- # @return [Dry::Configurable::Config]
69
- def self.event
70
- Omnes::Event.config
71
- end
72
- end
39
+ nest_config Subscriber
40
+ nest_config Event
73
41
 
74
42
  # @api private
75
43
  def self.included(klass)
data/omnes.gemspec CHANGED
@@ -36,8 +36,6 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
- spec.add_dependency "dry-configurable", "~> 0.14"
40
-
41
39
  spec.add_development_dependency "activejob", "~> 7.0"
42
40
  spec.add_development_dependency "redcarpet", "~> 3.5"
43
41
  spec.add_development_dependency "sidekiq", "~> 6.4"
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc Busqué
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-23 00:00:00.000000000 Z
11
+ date: 2022-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: dry-configurable
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '0.14'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '0.14'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: activejob
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -109,10 +95,12 @@ files:
109
95
  - bin/setup
110
96
  - lib/omnes.rb
111
97
  - lib/omnes/bus.rb
98
+ - lib/omnes/configurable.rb
112
99
  - lib/omnes/errors.rb
113
100
  - lib/omnes/event.rb
114
101
  - lib/omnes/execution.rb
115
102
  - lib/omnes/publication.rb
103
+ - lib/omnes/publication_context.rb
116
104
  - lib/omnes/registry.rb
117
105
  - lib/omnes/subscriber.rb
118
106
  - lib/omnes/subscriber/adapter.rb
@@ -135,7 +123,7 @@ metadata:
135
123
  source_code_uri: https://github.com/nebulab/omnes
136
124
  changelog_uri: https://github.com/nebulab/omnes/CHANGELOG.md
137
125
  rubygems_mfa_required: 'true'
138
- post_install_message:
126
+ post_install_message:
139
127
  rdoc_options: []
140
128
  require_paths:
141
129
  - lib
@@ -150,8 +138,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
138
  - !ruby/object:Gem::Version
151
139
  version: '0'
152
140
  requirements: []
153
- rubygems_version: 3.2.20
154
- signing_key:
141
+ rubygems_version: 3.1.2
142
+ signing_key:
155
143
  specification_version: 4
156
144
  summary: Pub/Sub for ruby
157
145
  test_files: []