omnes 0.1.0 → 0.2.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 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: []