pubsub_on_rails 0.0.1 → 0.0.2

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: e5aeb57501923d0faf35b735734647ab6880660059475126dc4aae2bce6f6d59
4
- data.tar.gz: 7da45ebcbc7c928f93c4960c02bed54585dfe3bfa9c91906bba7070e8f8b45a5
3
+ metadata.gz: 045526c8bade68da2829b9fb777f8a981e8363f1736519f6034ceb6bf7a69f21
4
+ data.tar.gz: 588d2a97661b81c12270d132645e01db957f60066da9c642bc530b1f490384fe
5
5
  SHA512:
6
- metadata.gz: 730b60a32e1f9873e783f9f029f60f1cf9ec105d55e001afba968dd80eee794f74f2f685fe944419780dc381d64a43157f6f51c660022d70766a1d9b7532fe94
7
- data.tar.gz: 69c30741019142f5124e9e98f58723efad81fb74ec918a1ffd7a27822dedd66c74f5db888ac4c97f5cf0296ad84e19cc1ceae46ad1858730740048d666bad954
6
+ metadata.gz: 2d27c973892a272902922d95b64da2a6c92ad66c3a361adb3e19b01a28c4c667400fba1b13021806efaccd7db91eb25f7b1e5466dc76f1c63faff2091c053fe3
7
+ data.tar.gz: 69cbd20d2bd42891d4ac1790673c12155d9f89ce96c385bc47c600a3b54d5a9cc979ba9b62608ff4010d0df8d807942ea2b44266b82fb4db53b5bda2b61f4b17
@@ -2,10 +2,57 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  pubsub_on_rails (0.0.1)
5
+ dry-struct
6
+ wisper (~> 2.0.0)
7
+ wisper-rspec
8
+ wisper-sidekiq
5
9
 
6
10
  GEM
7
11
  remote: https://rubygems.org/
8
12
  specs:
13
+ concurrent-ruby (1.1.5)
14
+ connection_pool (2.2.2)
15
+ dry-configurable (0.8.2)
16
+ concurrent-ruby (~> 1.0)
17
+ dry-core (~> 0.4, >= 0.4.7)
18
+ dry-container (0.7.0)
19
+ concurrent-ruby (~> 1.0)
20
+ dry-configurable (~> 0.1, >= 0.1.3)
21
+ dry-core (0.4.7)
22
+ concurrent-ruby (~> 1.0)
23
+ dry-equalizer (0.2.2)
24
+ dry-inflector (0.1.2)
25
+ dry-logic (1.0.0)
26
+ concurrent-ruby (~> 1.0)
27
+ dry-core (~> 0.2)
28
+ dry-equalizer (~> 0.2)
29
+ dry-struct (1.0.0)
30
+ dry-core (~> 0.4, >= 0.4.3)
31
+ dry-equalizer (~> 0.2)
32
+ dry-types (~> 1.0)
33
+ ice_nine (~> 0.11)
34
+ dry-types (1.0.0)
35
+ concurrent-ruby (~> 1.0)
36
+ dry-container (~> 0.3)
37
+ dry-core (~> 0.4, >= 0.4.4)
38
+ dry-equalizer (~> 0.2, >= 0.2.2)
39
+ dry-inflector (~> 0.1, >= 0.1.2)
40
+ dry-logic (~> 1.0)
41
+ ice_nine (0.11.2)
42
+ rack (2.0.7)
43
+ rack-protection (2.0.5)
44
+ rack
45
+ redis (4.1.1)
46
+ sidekiq (5.2.7)
47
+ connection_pool (~> 2.2, >= 2.2.2)
48
+ rack (>= 1.5.0)
49
+ rack-protection (>= 1.5.0)
50
+ redis (>= 3.3.5, < 5)
51
+ wisper (2.0.0)
52
+ wisper-rspec (1.1.0)
53
+ wisper-sidekiq (1.2.0)
54
+ sidekiq (>= 4.1)
55
+ wisper
9
56
 
10
57
  PLATFORMS
11
58
  ruby
@@ -14,4 +61,4 @@ DEPENDENCIES
14
61
  pubsub_on_rails!
15
62
 
16
63
  BUNDLED WITH
17
- 1.16.1
64
+ 1.17.2
@@ -0,0 +1,438 @@
1
+ # PubSub on Rails
2
+
3
+ PubSub on Rails is a gem facilitating opinionated approach to leveraging publish/subscribe messaging pattern in Ruby on Rails applications.
4
+
5
+ There are many programming techniques that are powerful yet complex. The beauty of publish/subscribe patterns is that it is powerful while staying simple.
6
+
7
+ Instead of using callbacks or directly and explicitly executing series of actions, action execution is requested using an event object combined with event subscription.
8
+ This helps in keeping code isolation high, and therefore makes large codebases maintainable and testable.
9
+
10
+ While it has little to do with event sourcing, it encompasses a couple of ideas related to domain-driven development.
11
+ Therefore it is only useful in applications in which domains/bounded-contexts can be identified.
12
+ This is especially true for applications covering many side effects, integrations and complex business logic.
13
+
14
+ ## Installation
15
+
16
+ ```ruby
17
+ # Gemfile
18
+
19
+ gem 'pubsub_on_rails', '~> 0.0.1'
20
+
21
+ # config/initializers/pub_sub.rb
22
+
23
+ PubSub::Subscriptions.subscriptions_path = Rails.root.join('config/subscriptions.yml')
24
+ PubSub::Subscriptions.load!
25
+ ```
26
+
27
+ ## Entities
28
+
29
+ There are five entities that are core to PubSub on Rails: domains, events, event publishers, event handlers and subscriptions.
30
+
31
+ ### Domain
32
+
33
+ Domain is simply a named context in application. You can refer to it as "module", "subsystem", "engine", whatever you like.
34
+ Good names for domains are "ordering", "messaging", "logging", "accounts", "logistics" etc.
35
+ Your app does not need to have code isolated inside domains, but using Component-Based Rails Applications concept (CBRA) sounds like a nice idea to be combined with PubSub on Rails.
36
+
37
+ Domain example:
38
+
39
+ ```ruby
40
+ # app/domains/messaging.rb
41
+
42
+ module Messaging
43
+ extend PubSub::Domain
44
+ end
45
+ ```
46
+
47
+ ### Event
48
+
49
+ Event is basically an object indicating that something has happened (event has occured).
50
+ There are two important things that need to be considered when planning an event: its **name** and its **payload** (fields).
51
+
52
+ Name of event should describe an action that has just happened, also it should be namespaced with the name of the domain it has occurred within.
53
+ Some examples of good event names: `Ordering::OrderCancelled`, `Messaging::IncorrectLoginNotificationSent`, `Accounts::UserCreated`, `Bookings::CheckinDateChanged`, `Reporting::MonthlySalesReportGenerationRequested`
54
+
55
+ Payload of event is just simple set of fields that should convey critical information related to the event.
56
+ As the payload is very important for each event (it acts as a contract between publisher and handler), PubSub on Rails leverages `Dry::Struct` and `Dry::Types` to ensure both presence and correct type of attributes events are created with.
57
+ It is a good rule of a thumb not to create too many fields for each event and just start with the minimal set. It is easy to add more fields to event's payload later (while it might be cumbersome to remove or change them).
58
+
59
+ Event example:
60
+
61
+ ```ruby
62
+ # app/events/ordering/order_created_event.rb
63
+
64
+ module Ordering
65
+ class OrderCreatedEvent < PubSub::DomainEvent
66
+ attribute :order_id, Types::Strict::Integer
67
+ attribute :customer_id, Types::Strict::Integer
68
+ attribute :line_items, Types::Strict::Array
69
+ attribute :total_amount, Types::Strict::Float
70
+ attribute :comment, Types::Strict::String.optional
71
+ end
72
+ end
73
+ ```
74
+
75
+ ### Event publisher
76
+
77
+ Event publisher is any class capable of emitting an event.
78
+ Usually a great places to start emitting events are model callbacks, service objects or event handlers.
79
+ It is very preferable to emit one specific event from only one place, as in most cases this makes the most sense and makes the whole solution more comprehensible.
80
+
81
+ Event publisher example:
82
+
83
+ ```ruby
84
+ # app/models/order.rb
85
+
86
+ class Order < ApplicationRecord
87
+ include PubSub::Emit
88
+
89
+ belongs_to :customer
90
+ has_many :line_items
91
+
92
+ #...
93
+
94
+ after_create do
95
+ emit(:ordering__order_created, order_id: id)
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### Event handler
101
+
102
+ Event handler is a class that encapsulates logic that should be executed in reaction to event being emitted.
103
+ One event can be handled by many handlers, but only one unique handler within each domain.
104
+ Event handlers can be executed synchronously or asynchronously. The latter is recommended for both performance and error-recovery reasons.
105
+
106
+ Event handler example:
107
+
108
+ ```ruby
109
+ # app/event_handlers/messaging/ordering_order_created_handler.rb
110
+
111
+ module Messaging
112
+ class OrderingOrderCreatedHandler < PubSub::DomainEventHandler
113
+ def call
114
+ OrderMailer.order_creation_notification(order).deliver_now
115
+ end
116
+
117
+ private
118
+
119
+ def order
120
+ Order.find(event_data.order_id)
121
+ end
122
+ end
123
+ end
124
+ ```
125
+
126
+ All fields of event's payload are accessible through `event_data` method, which is a simple struct.
127
+
128
+ #### Conditionally processing events
129
+
130
+ If in any case you would like to control if given handler should be executed or not (maybe using feature flags), you can override `#process_event?` method.
131
+
132
+ ```ruby
133
+ # app/event_handlers/messaging/ordering_order_created_handler.rb
134
+
135
+ module Messaging
136
+ class OrderingOrderCreatedHandler < PubSub::DomainEventHandler
137
+ # ...
138
+
139
+ private
140
+
141
+ def process_event?
142
+ Features.notifications_enabled?
143
+ end
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### Subscription
149
+
150
+ Subscription is "the glue", the binds events with their corresponding handlers.
151
+ Each subscription binds one or all events with one handler.
152
+ Subscription defines if given handler should be executed in synchronous or asynchronous way.
153
+
154
+ Subscription example:
155
+
156
+ ```yaml
157
+ # config/subscriptions.yml
158
+
159
+ messaging:
160
+ ordering__order_created: async
161
+ ```
162
+
163
+ ## Testing
164
+
165
+ Most of entities in Pub/Sub approach should be tested, yet both domain and event classes can be tested implicitly.
166
+ It is recommended to start testing from testing subscription itself, then ensure that both event emission and handling are in place. Depending on situation the recommended order may change though.
167
+
168
+ ### RSpec
169
+
170
+ The recommended RSpec configuration is as follows:
171
+
172
+ ```ruby
173
+ # spec/support/pub_sub.rb
174
+
175
+ require 'pub_sub/testing'
176
+
177
+ RSpec.configure do |config|
178
+ config.include Wisper::RSpec::BroadcastMatcher
179
+ config.include PubSub::Testing::SubscriptionsHelper
180
+ config.include PubSub::Testing::EventDataHelper
181
+
182
+ config.before(:suite) do
183
+ PubSub::Testing::SubscriptionsHelper.clear_wisper_subscriptions!
184
+ end
185
+
186
+ config.around(:each, subscribers: true) do |example|
187
+ domain_name = example.metadata[:described_class].to_s.underscore
188
+ PubSub.subscriptions.register(domain_name)
189
+ example.run
190
+ clear_wisper_subscriptions!
191
+ end
192
+ end
193
+ ```
194
+
195
+ ### Testing subscription
196
+
197
+ Testing subscription is as easy as telling what domains should subscribe to what event in what way.
198
+ Example of `subscribe_to` matcher can be found [here](https://gist.github.com/stevo/bd11c8dae812c919de6a61d1292dbfe1).
199
+ Example:
200
+
201
+ ```ruby
202
+ RSpec.describe Messaging, subscribers: true do
203
+ it { is_expected.to subscribe_to(:ordering__order_created).asynchronously }
204
+ end
205
+ ```
206
+
207
+ ### Testing publishers
208
+
209
+ To test publisher it is crucial to test if event was emitted (broadcasted) under certain conditions (if any).
210
+
211
+ Example:
212
+
213
+ ```ruby
214
+ RSpec.describe Order do
215
+ describe 'after_create' do
216
+ it 'emits ordering__order_created' do
217
+ customer = create(:customer)
218
+ line_items = create_list(:line_item, 2)
219
+
220
+ expect {
221
+ Order.create(
222
+ customer: customer,
223
+ total_amount: 100.99,
224
+ comment: 'Small order',
225
+ line_items: line_items
226
+ )
227
+ }.to broadcast(
228
+ :ordering__order_created,
229
+ order_id: fetch_next_id_for(Order),
230
+ total_amount: 100.99,
231
+ comment: 'Small order',
232
+ line_items: line_items
233
+ )
234
+ end
235
+ end
236
+ end
237
+ ```
238
+
239
+ ### Testing handlers
240
+
241
+ Handlers can be tested by testing their `call!` method, that calls `call` behind the scenes.
242
+ To ensure event payload contract is met, please use `event_data_for` helper to build event payload hash.
243
+ It will instantiate event object behind the scenes to ensure it exists and its payload requirements are met.
244
+
245
+ Example:
246
+
247
+ ```ruby
248
+ module Messaging
249
+ RSpec.describe OrderingOrderCreatedHandler do
250
+ describe '#call!' do
251
+ it 'delivers order creation notification' do
252
+ order = create(:order)
253
+ event_data = event_data_for(
254
+ 'ordering__order_created',
255
+ order_id: order.id,
256
+ total_amount: 100.99,
257
+ comment: 'Small order',
258
+ line_items: [build(:line_item)]
259
+ )
260
+ order_creation_notification = double(:order_creation_notification, deliver_now: true)
261
+ allow(OrderMailer).to receive(:order_creation_notification).
262
+ with(order).and_return(order_creation_notification)
263
+
264
+ OrderingOrderCreatedHandler.new(event_data).call!
265
+
266
+ expect(order_creation_notification).to have_received(:deliver_now)
267
+ end
268
+ end
269
+ end
270
+ end
271
+ ```
272
+ ### Integration testing
273
+
274
+ By default all subscriptions are cleared in testing environment to enforce testing in isolation.
275
+ To enable integration testing, a test can be wrapped in block provided by `with_subscription_to` helper.
276
+ This way events emitted by code executed within this block will be handled by handlers from domains provided as arguments to `with_subscription_to` helper.
277
+
278
+ ```ruby
279
+ it 'does something' do
280
+ with_subscription_to('messaging', 'logging') do
281
+ # setup, subject, assertions etc.
282
+ end
283
+ end
284
+ ```
285
+
286
+ ### Subscriptions linting
287
+
288
+ It is a common problem to implement a publisher and handler and forget about implementing subscription.
289
+ Without proper integration testing the problem might stay undetected before identified (hopefully) during manual testing.
290
+ This is where subscriptions linting comes into play. All existing event handlers will be verified against registered subscriptions during linting process.
291
+ In case of any mismatch, exception will be raised.
292
+
293
+ To lint subscriptions, place `PubSub::Subscriptions.lint!` for instance in your `rails_helper.rb` or some initializer of choice.
294
+
295
+ ## Logger
296
+
297
+ Even though default domain always routes event subscriptions to correspondingly named event handlers, it is possible to implement domains that will route subscriptions in the different way.
298
+ The simplest way is to define it manually:
299
+
300
+ ```ruby
301
+ # app/domains/logging.rb
302
+
303
+ module Messaging
304
+ def self.ordering__order_created(event_payload)
305
+ # whatever you need goes here
306
+ end
307
+ end
308
+ ```
309
+
310
+ This technique can be useful for instance for logging
311
+
312
+ ```ruby
313
+ # app/domains/logging.rb
314
+
315
+ module Logging
316
+ def self.event_logger
317
+ @event_logger ||= Logger.new("#{Rails.root}/log/#{Rails.env}_event_logger.log")
318
+ end
319
+
320
+ def self.method_missing(method_name, *event_data)
321
+ event_logger.info("Evt: #{method_name}: \n#{event_data.map(&:to_json).join(', ')}\n\n")
322
+ end
323
+
324
+ def self.respond_to_missing?(method_name, include_private = false)
325
+ method_name.to_s.start_with?(/[a-z_]+__/) || super
326
+ end
327
+ end
328
+ ```
329
+
330
+ ```yaml
331
+ # config/subscriptions.yml
332
+
333
+ logging:
334
+ all_events: sync
335
+ ```
336
+
337
+ # `emit` vs `broadcast`
338
+
339
+ PubSub on Rails leverages `wisper` and `wisper-sidekiq` under the hood.
340
+ This is why instead of using `emit`, you can broadcast events by using wisper's `broadcast` method.
341
+
342
+ ```ruby
343
+ # app/models/order.rb
344
+
345
+ class Order < ApplicationRecord
346
+ include Wisper::Publisher
347
+
348
+ #...
349
+
350
+ after_create do
351
+ broadcast(
352
+ :ordering__guest_checked_in,
353
+ order_id: id,
354
+ line_items: line_items,
355
+ total_amount: total_amount,
356
+ comment: comment
357
+ )
358
+ end
359
+ end
360
+ ```
361
+
362
+ Why `emit` then? `emit` provides a couple of simplifications that make emitting events easier and more reliable.
363
+
364
+ ## Payload verification
365
+
366
+ Every time event is emitted, its payload is supplied to corresponding event class and is verified.
367
+ This ensures that whenever we emit event we can be sure its payload is matching specification.
368
+
369
+ Example:
370
+
371
+ ```ruby
372
+ module Accounts
373
+ class PersonCreatedEvent < DomainEvent
374
+ attribute :person_id, Types::Strict::Integer
375
+ end
376
+ end
377
+ ```
378
+
379
+ * `emit(:accounts__person_created, person_id: 1)` is ok
380
+ * `emit(:accounts__person_created)` will result in ```PubSub::EventEmission::EventPayloadArgumentMissing: Event [Accounts::PersonCreatedEvent] expects [person_id] payload attribute to be either exposed as [person] method in emitting object or provided as argument```
381
+ * `emit(:accounts__person_created, person_id: 'abc')` will result in ```Dry::Struct::Error: [Accounts::PersonCreatedEvent.new] "abc" (String) has invalid type for :person_id violates constraints (type?(Integer, "abc") failed)```
382
+
383
+ ## Automatic event name prefixing
384
+
385
+ When you namespace your code to match your domain names, you can skip prefixing an event name with domain name when emitting it.
386
+
387
+ ```ruby
388
+ # app/models/oriering/order.rb
389
+
390
+ module Ordering
391
+ class Order < ApplicationRecord
392
+ include PubSub::Emit
393
+
394
+ after_create do
395
+ emit(:order_created, order_id: id)
396
+ # emit(:ordering__order_created, order_id: id) # this will work as well
397
+ end
398
+ end
399
+ end
400
+ ```
401
+
402
+ ## Automatic event payload population
403
+
404
+ Whenever you emit an event, it will try to populate its payload with data using public interface of object it is emitted from within.
405
+
406
+ ```ruby
407
+ # app/models/oriering/order.rb
408
+
409
+ module Ordering
410
+ class Order < ApplicationRecord
411
+ include PubSub::Emit
412
+
413
+ after_create do
414
+ emit(:order_created, order_id: id)
415
+ # emit(
416
+ # :ordering__order_created,
417
+ # order_id: id, # `self` does not implement `order_id`, therefore value has to be provided explicitly here
418
+ # total_amount: total_amount, # attribute matches the name of method on `self`, therefore it can be skipped
419
+ # comment: comment # same here
420
+ # )
421
+ end
422
+ end
423
+ end
424
+ ```
425
+
426
+ ## Manually broadcasting events
427
+
428
+ If for some reason you need to safely broadcast some event, the best way is to instantiate its class and call `#broadcast!` on it.
429
+
430
+ Example:
431
+
432
+ ```ruby
433
+ Ordering::OrderCreatedEvent.new(order_id: 1, total_amount: 25, line_items: []).broadcast!
434
+ ```
435
+
436
+ # TODO
437
+
438
+ - Dynamic event classes
@@ -31,8 +31,7 @@ module PubSub
31
31
  subscriptions.each do |event_name, subscription_type|
32
32
  options = {}
33
33
  options[:on] = event_name unless event_name == 'all_events'
34
- options[:async] = true if subscription_type == 'async'
35
- options[:broadcaster] = :run_once if subscription_type == 'run_once'
34
+ options[:broadcaster] = subscription_type == 'sync' ? :default : subscription_type.to_sym
36
35
 
37
36
  Wisper.subscribe("::#{domain_name.camelize}".constantize, options)
38
37
  end
@@ -1,3 +1,3 @@
1
1
  module PubSub
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubsub_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stevo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-06 00:00:00.000000000 Z
11
+ date: 2019-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-struct
@@ -76,6 +76,7 @@ files:
76
76
  - Gemfile
77
77
  - Gemfile.lock
78
78
  - LICENSE.md
79
+ - README.md
79
80
  - lib/pub_sub.rb
80
81
  - lib/pub_sub/domain.rb
81
82
  - lib/pub_sub/domain_event.rb