pubsub_on_rails 0.0.7 → 1.1.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
- SHA1:
3
- metadata.gz: e3e25ad0c40aca45ee464ada4fd7f99c58b9d85c
4
- data.tar.gz: f120f19a9df7626cfe7fe8322bca917644c0763c
2
+ SHA256:
3
+ metadata.gz: 665f27ae9c49ccdd0f1b87c8f5b788a558ebee2a22848f3ab04e272528df7e17
4
+ data.tar.gz: c7107b6d788c1ac9a0069b104b6e4db211a692ef4a6345c613a62fcbcd306859
5
5
  SHA512:
6
- metadata.gz: 41513d591f90f71e6ffdcbc21483236619d1912b5857d78a754bec3fa6d9180b40f9919fec7e2a17f43d3d3d4b618845e686510f8f423f2f4a801f73abec9167
7
- data.tar.gz: f79dc8a3a235c565c51d1ccaaf0b06aba964bba67b322476d097776095083f989d0b9bd5fbdb482d6e128a16bf6e523f954fa4e4b0e4847e3604e8c0d8fef174
6
+ metadata.gz: 4a0f58bbe6dd2ff109934781c580c3ac5b1a711db532a4f0a61f2a005b3b6fee0d4c89391cf53f6d33c0dc65972ccddb34843e551121053641781499fb7d0d7e
7
+ data.tar.gz: c1895dffa35a785cd78ff6ab62e2e5c5fb10c4937d96b81f7d44cfe63908d101eb30073054ef1dfd17829ebfa72a47f9ee8275e1df813c89ad1d40a7a29c6fb2
data/README.md CHANGED
@@ -16,14 +16,90 @@ This is especially true for applications covering many side effects, integration
16
16
  ```ruby
17
17
  # Gemfile
18
18
 
19
- gem 'pubsub_on_rails', '~> 0.0.3'
19
+ gem 'pubsub_on_rails', '~> 1.1.0'
20
20
 
21
21
  # config/initializers/pub_sub.rb
22
22
 
23
- PubSub::Subscriptions.subscriptions_path = Rails.root.join('config/subscriptions.yml')
24
- PubSub::Subscriptions.load!
23
+ require 'pub_sub/subscriptions_list'
24
+
25
+ Rails.configuration.to_prepare do
26
+ Rails.configuration.event_store = event_store = RailsEventStore::Client.new(
27
+ repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)
28
+ )
29
+
30
+ PubSub::SubscriptionsList.config_path =
31
+ Rails.root.join('config/subscriptions.yml')
32
+ PubSub::SubscriptionsList.load!(event_store)
33
+ end
34
+ ```
35
+
36
+ ## Migrating from 0.0.7 to 1.0.0
37
+
38
+ 1. Update gem to version `1.0.0`
39
+
40
+ ```ruby
41
+ # Gemfile
42
+
43
+ gem 'pubsub_on_rails', '~> 1.0.0'
25
44
  ```
26
45
 
46
+ 2. Run Rails Event Store migrations
47
+
48
+ **MySQL**
49
+ ```
50
+ bin/rails generate rails_event_store_active_record:migration
51
+ bin/rails db:migrate
52
+ ```
53
+
54
+ **PostgreSQL**
55
+ ```
56
+ bin/rails generate rails_event_store_active_record:migration --data-type=jsonb
57
+ bin/rails db:migrate
58
+ ```
59
+
60
+ 3. Update initializer to use Rails Event Store Client
61
+
62
+ ```ruby
63
+ # config/initializers/pub_sub.rb
64
+
65
+ require 'pub_sub/subscriptions_list'
66
+
67
+ Rails.configuration.to_prepare do
68
+ Rails.configuration.event_store = event_store = RailsEventStore::Client.new(
69
+ repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)
70
+ )
71
+
72
+ PubSub::SubscriptionsList.config_path =
73
+ Rails.root.join('config/subscriptions.yml')
74
+ PubSub::SubscriptionsList.load!(event_store)
75
+ end
76
+ ```
77
+
78
+ 4. Override `EventWorker` or override `EventHandlerBuilder` if needed
79
+
80
+ For example when you want to have different workers for different events:
81
+
82
+ ```ruby
83
+ # config/initializers/pub_sub.rb
84
+
85
+ PubSub::EventHandlerBuilder.class_eval do
86
+ def call(event)
87
+ if async?
88
+ if class_name.to_s.include?('MyType')
89
+ SingleThreadEventWorker.perform_in(2.seconds, class_name.to_s, event.event_id)
90
+ else
91
+ EventWorker.perform_in(2.seconds, class_name.to_s, event.event_id)
92
+ end
93
+ else
94
+ class_name.new(event).call!
95
+ end
96
+ end
97
+ end
98
+ ```
99
+
100
+ 5. Add event objects for Rails Event Store streams. Check [Event](README.md#Event) section.
101
+ 6. Update test cases to use new matchers. Check [Testing](README.md#Testing) section.
102
+
27
103
  ## Entities
28
104
 
29
105
  There are five entities that are core to PubSub on Rails: domains, events, event publishers, event handlers and subscriptions.
@@ -40,7 +116,6 @@ Domain example:
40
116
  # app/domains/messaging.rb
41
117
 
42
118
  module Messaging
43
- extend PubSub::Domain
44
119
  end
45
120
  ```
46
121
 
@@ -61,13 +136,37 @@ Event example:
61
136
  ```ruby
62
137
  # app/events/ordering/order_created_event.rb
63
138
 
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
139
+ module PubSub
140
+ module Ordering
141
+ class OrderCreatedEvent < PubSub::EventWithType
142
+ schema do
143
+ attribute :order_id, Types::Strict::Integer
144
+ attribute :customer_id, Types::Strict::Integer
145
+ attribute :line_items, Types::Strict::Array
146
+ attribute :total_amount, Types::Strict::Float
147
+ attribute :comment, Types::Strict::String.optional
148
+ end
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ Since we are using Rails Event Store to handle events, it gives us a possibility to create **stream** of events. We can treat them as sub-list of events. To be able to use that functionality we need to declare which streams given event should be part of. By default we add event to stream based on its name. In case of our example it is `ordering__order_created`. We can provide also custom streams even based on some additional data from the event attributes (for example to group all events related to given order).
155
+
156
+ Event example:
157
+
158
+ ```ruby
159
+ # app/events/rails_event_store/ordering/order_created_event.rb
160
+
161
+ module PubSub
162
+ module Ordering
163
+ class OrderCreatedEvent < PubSub::EventWithType
164
+ def stream_names
165
+ [
166
+ "order__#{data[:order_id]}"
167
+ ]
168
+ end
169
+ end
71
170
  end
72
171
  end
73
172
  ```
@@ -171,42 +270,41 @@ The recommended RSpec configuration is as follows:
171
270
 
172
271
  ```ruby
173
272
  # spec/support/pub_sub.rb
174
-
273
+
175
274
  require 'pub_sub/testing'
176
275
 
177
276
  RSpec.configure do |config|
178
- config.include Wisper::RSpec::BroadcastMatcher
179
- config.include PubSub::Testing::SubscriptionsHelper
277
+ config.include PubSub::Testing::RailsEventStore
180
278
  config.include PubSub::Testing::EventDataHelper
181
279
 
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)
280
+ config.around(:each, in_memory_res_client: true) do |example|
281
+ current_event_store = Rails.configuration.event_store
282
+ Rails.configuration.event_store = RubyEventStore::Client.new(
283
+ repository: RubyEventStore::InMemoryRepository.new
284
+ )
189
285
  example.run
190
- clear_wisper_subscriptions!
286
+ Rails.configuration.event_store = current_event_store
191
287
  end
192
288
  end
193
289
  ```
194
290
 
291
+ This will allow you to use `in_memory_res_client` which will not create object (event) in the database and do not call all dependent logic (handlers).
292
+
195
293
  ### Testing subscription
196
294
 
197
295
  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).
296
+
199
297
  Example:
200
298
 
201
299
  ```ruby
202
- RSpec.describe Messaging, subscribers: true do
300
+ RSpec.describe Messaging do
203
301
  it { is_expected.to subscribe_to(:ordering__order_created).asynchronously }
204
302
  end
205
303
  ```
206
304
 
207
305
  ### Testing publishers
208
306
 
209
- To test publisher it is crucial to test if event was emitted (broadcasted) under certain conditions (if any).
307
+ To test publisher it is crucial to test if event was emitted under certain conditions (if any).
210
308
 
211
309
  Example:
212
310
 
@@ -217,20 +315,21 @@ RSpec.describe Order do
217
315
  customer = create(:customer)
218
316
  line_items = create_list(:line_item, 2)
219
317
 
220
- expect {
221
- Order.create(
222
- customer: customer,
223
- total_amount: 100.99,
224
- comment: 'Small order',
225
- line_items: line_items
318
+ Order.create(
319
+ customer: customer,
320
+ total_amount: 100.99,
321
+ comment: 'Small order',
322
+ line_items: line_items
323
+ )
324
+
325
+ expect(event_store).to have_published(
326
+ an_event(PubSub::Ordering::OrderCreatedEvent).with_data(
327
+ order_id: fetch_next_id_for(Order),
328
+ total_amount: 100.99,
329
+ comment: 'Small order',
330
+ line_items: line_items
226
331
  )
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
- )
332
+ ).in_stream('ordering__order_created')
234
333
  end
235
334
  end
236
335
  end
@@ -269,19 +368,6 @@ module Messaging
269
368
  end
270
369
  end
271
370
  ```
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
371
 
286
372
  ### Subscriptions linting
287
373
 
@@ -334,44 +420,21 @@ logging:
334
420
  all_events: sync
335
421
  ```
336
422
 
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
423
  ## Payload verification
365
424
 
366
- Every time event is emitted, its payload is supplied to corresponding event class and is verified.
425
+ Every time event is emitted, its payload is supplied to corresponding `Dry::Struct` event class and is verified.
367
426
  This ensures that whenever we emit event we can be sure its payload is matching specification.
368
427
 
369
428
  Example:
370
429
 
371
430
  ```ruby
372
- module Accounts
373
- class PersonCreatedEvent < DomainEvent
374
- attribute :person_id, Types::Strict::Integer
431
+ module PubSub
432
+ module Accounts
433
+ class PersonCreatedEvent < PubSub::EventWithType
434
+ schema do
435
+ attribute :person_id, Types::Strict::Integer
436
+ end
437
+ end
375
438
  end
376
439
  end
377
440
  ```
@@ -401,7 +464,7 @@ end
401
464
 
402
465
  ## Automatic event payload population
403
466
 
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.
467
+ 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
468
 
406
469
  ```ruby
407
470
  # app/models/oriering/order.rb
@@ -423,16 +486,6 @@ module Ordering
423
486
  end
424
487
  ```
425
488
 
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
489
  # TODO
437
490
 
438
491
  - Dynamic event classes
@@ -1,28 +1,4 @@
1
- require 'pub_sub/pure_event'
2
-
3
1
  module PubSub
4
- class DomainEvent < PureEvent
5
- attribute :event_trace_id, Types::Strict::String.default {
6
- EventTrace.trace_id ||= SecureRandom.hex(8)
7
- }
8
- attribute :event_id, Types::Strict::String.default {
9
- SecureRandom.hex(8)
10
- }
11
- attribute :trigger_id, Types::Strict::String.default {
12
- EventTrace.last_event_id || 'ORIGIN'
13
- }
14
-
15
- def initialize(*args)
16
- super(*args)
17
- EventTrace.last_event_id = event_id
18
- end
19
-
20
- private
21
-
22
- def attributes_to_broadcast
23
- super.
24
- except(:event_trace_id, :event_id, :trigger_id).
25
- merge(event_uid: "#{event_trace_id}-#{trigger_id}-#{event_id}")
26
- end
2
+ class DomainEvent < Dry::Struct
27
3
  end
28
4
  end
@@ -1,7 +1,7 @@
1
1
  module PubSub
2
2
  class DomainEventHandler
3
- def initialize(*args)
4
- @event_data_hash = args.extract_options!
3
+ def initialize(event)
4
+ @event = event
5
5
  end
6
6
 
7
7
  def call
@@ -14,7 +14,7 @@ module PubSub
14
14
 
15
15
  private
16
16
 
17
- attr_reader :event_data_hash
17
+ attr_reader :event
18
18
 
19
19
  def process_event?
20
20
  true
@@ -23,5 +23,9 @@ module PubSub
23
23
  def event_data
24
24
  @event_data ||= OpenStruct.new(event_data_hash)
25
25
  end
26
+
27
+ def event_data_hash
28
+ event.data
29
+ end
26
30
  end
27
31
  end
data/lib/pub_sub/emit.rb CHANGED
@@ -4,13 +4,14 @@ require 'pub_sub/event_emission'
4
4
  module PubSub
5
5
  module Emit
6
6
  def emit(event_name, explicit_payload = {})
7
+ abstract_event_class = explicit_payload.delete(:abstract_event_class)
7
8
  event_class = EventClassFactory.build(
8
9
  event_name,
9
10
  domain_name: self.class.name.deconstantize.demodulize,
10
- abstract_event_class: explicit_payload.delete(:abstract_event_class)
11
+ abstract_event_class:
11
12
  )
12
13
 
13
- EventEmission.new(event_class, explicit_payload, self).call
14
+ EventEmission.new(abstract_event_class, event_class, event_name, explicit_payload, self).call
14
15
  end
15
16
  end
16
17
  end
@@ -5,8 +5,8 @@ module PubSub
5
5
  def self.build(event_name, domain_name: nil, abstract_event_class: nil)
6
6
  new(
7
7
  event_name,
8
- domain_name: domain_name,
9
- abstract_event_class: abstract_event_class
8
+ domain_name:,
9
+ abstract_event_class:
10
10
  ).build_event_class
11
11
  end
12
12
 
@@ -17,9 +17,13 @@ module PubSub
17
17
  end
18
18
 
19
19
  def build_event_class
20
+ event_class = res_event_class_name.safe_constantize
21
+
22
+ return event_class if event_class.present?
23
+
20
24
  event_class = event_class_name.safe_constantize
21
25
 
22
- return event_class unless event_class.nil?
26
+ return event_class if event_class.present?
23
27
 
24
28
  if abstract_event_class.nil?
25
29
  raise(EventClassDoesNotExist, event_class_name)
@@ -46,12 +50,16 @@ module PubSub
46
50
 
47
51
  def event_name_with_domain
48
52
  if event_name_includes_domain?
49
- event_name.to_s.sub('__', '/')
53
+ event_name.to_s.downcase.sub('__', '/')
50
54
  else
51
- "#{domain_name}/#{event_name}"
55
+ [domain_name&.underscore, event_name].compact.join('/')
52
56
  end
53
57
  end
54
58
 
59
+ def res_event_class_name
60
+ @res_event_class_name ||= "pub_sub/#{event_name_with_domain}_event".classify
61
+ end
62
+
55
63
  def event_class_name
56
64
  @event_class_name ||= "#{event_name_with_domain}_event".classify
57
65
  end
@@ -1,44 +1,82 @@
1
1
  require 'pub_sub/payload_attribute'
2
2
 
3
3
  module PubSub
4
- class EventEmission
5
- EventPayloadArgumentMissing = Class.new(StandardError)
4
+ Error = Class.new(StandardError)
5
+ EventMissing = Class.new(Error)
6
+ EventPayloadArgumentMissing = Class.new(Error)
6
7
 
7
- def initialize(event_class, explicit_payload, context)
8
+ class EventEmission
9
+ def initialize(abstract_event_class, event_class, event_name, explicit_payload, context)
10
+ @abstract_event_class = abstract_event_class
8
11
  @event_class = event_class
12
+ @event_name = event_name
9
13
  @explicit_payload = explicit_payload
10
14
  @context = context
11
15
  end
12
16
 
13
17
  def call
14
- event_class.new(full_payload).broadcast!
18
+ if event_class.ancestors.include?(PubSub::EventWithType)
19
+ event_store.publish(event, stream_name:)
20
+ else
21
+ raise(EventMissing, event_name)
22
+ end
15
23
  end
16
24
 
17
25
  private
18
26
 
19
- attr_reader :event_class, :explicit_payload, :context
27
+ attr_reader :abstract_event_class, :event_class, :event_name, :explicit_payload, :context
28
+
29
+ def event
30
+ event_class.new(data: full_payload)
31
+ end
32
+
33
+ def event_name_includes_domain?
34
+ event_name.to_s.include?('__')
35
+ end
36
+
37
+ def stream_name
38
+ return event_name.to_s.downcase if event_name_includes_domain?
39
+
40
+ "#{domain}__#{event_name}"
41
+ end
20
42
 
21
- def event_payload_attribute_names
22
- event_class.attribute_names
43
+ def domain
44
+ if abstract_event_class
45
+ abstract_event_class.name.deconstantize.underscore
46
+ else
47
+ context.class.name.deconstantize.demodulize.underscore
48
+ end
23
49
  end
24
50
 
25
51
  # rubocop:disable Metrics/MethodLength
26
52
  def full_payload
27
- event_payload_attribute_names.each_with_object({}) do |attribute_name, result|
28
- result[attribute_name] = PayloadAttribute.new(attribute_name, explicit_payload, context).get
29
- rescue PayloadAttribute::CannotEvaluate => cannot_evaluate_error
30
- if event_class.schema.key(attribute_name).default?
31
- next
32
- else
33
- raise(
34
- EventPayloadArgumentMissing,
35
- "Event [#{event_class.name}] expects [#{attribute_name}] payload attribute to be" \
36
- " either exposed as [#{cannot_evaluate_error.message}] method in emitting object" \
37
- ' or provided as argument'
38
- )
39
- end
53
+ attribute_names.each_with_object({}) do |attribute_name, result|
54
+ result[attribute_name] = PayloadAttribute.new(
55
+ attribute_name, explicit_payload, context
56
+ ).get
57
+ rescue PayloadAttribute::CannotEvaluate => e
58
+ next if schema.key(attribute_name).default?
59
+
60
+ raise(
61
+ EventPayloadArgumentMissing,
62
+ "Event [#{event_class.name}] expects [#{attribute_name}] " \
63
+ "payload attribute to be either exposed as [#{e.message}] method in emitting object " \
64
+ 'or provided as argument'
65
+ )
40
66
  end
41
67
  end
42
68
  # rubocop:enable Metrics/MethodLength
69
+
70
+ def attribute_names
71
+ (abstract_event_class || event_class.instance_variable_get(:@schema_validator)).attribute_names
72
+ end
73
+
74
+ def schema
75
+ (abstract_event_class || event_class.instance_variable_get(:@schema_validator)).schema
76
+ end
77
+
78
+ def event_store
79
+ Rails.configuration.event_store
80
+ end
43
81
  end
44
82
  end
@@ -0,0 +1,30 @@
1
+ module PubSub
2
+ class EventHandlerBuilder
3
+ def initialize(class_name, subscription_type)
4
+ @class_name = class_name
5
+ @subscription_type = subscription_type.to_sym
6
+ end
7
+
8
+ def call(event)
9
+ if async?
10
+ EventWorker.perform_async(class_name.to_s, event.event_id)
11
+ else
12
+ class_name.new(event).call!
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ attr_reader :class_name, :subscription_type
19
+
20
+ def ==(other)
21
+ class_name == other.class_name && subscription_type == other.subscription_type
22
+ end
23
+
24
+ private
25
+
26
+ def async?
27
+ subscription_type == :async
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ module PubSub
2
+ class EventWithType < RailsEventStore::Event
3
+ def initialize(event_id: SecureRandom.uuid, metadata: nil, data: {})
4
+ super(
5
+ event_id:,
6
+ metadata:,
7
+ data: self.class.instance_variable_get(:@schema_validator).new(
8
+ data.deep_symbolize_keys
9
+ ).attributes
10
+ )
11
+ end
12
+
13
+ def stream_names
14
+ []
15
+ end
16
+
17
+ def self.schema(&block)
18
+ instance_variable_set(:@schema_validator, Class.new(Dry::Struct, &block))
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module PubSub
2
+ class EventWorker
3
+ include Sidekiq::Job
4
+
5
+ def perform(class_name, event_id)
6
+ class_name.constantize.new(
7
+ event_store.read.event(event_id)
8
+ ).call!
9
+ end
10
+
11
+ private
12
+
13
+ def event_store
14
+ Rails.configuration.event_store
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module PubSub
2
+ class SubscriptionsLinter
3
+ MissingSubscriptions = Class.new(StandardError)
4
+
5
+ def initialize(subscriptions)
6
+ @subscriptions = subscriptions
7
+ end
8
+
9
+ def lint!
10
+ raise MissingSubscriptions, error_message if missing_subscriptions.present?
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :subscriptions
16
+
17
+ def error_message
18
+ "The following subscriptions are missing: \n#{missing_subscriptions.join("\n")}"
19
+ end
20
+
21
+ def missing_subscriptions
22
+ (handled_subscription_names - all_subscription_names)
23
+ end
24
+
25
+ # :reek:UtilityFunction, :reek:DuplicateMethodCall
26
+ def handled_subscription_names
27
+ Dir[Rails.root.join('app/event_handlers/*/*.rb')].map do |file_path|
28
+ file_path.
29
+ sub(Rails.root.join('app/event_handlers/').to_s, '').
30
+ sub('_handler.rb', '')
31
+ end
32
+ end
33
+
34
+ def all_subscription_names
35
+ subscriptions.flat_map do |domain_name, subscriptions|
36
+ subscriptions.keys.map do |event_name|
37
+ "#{domain_name}/#{event_name.sub('__', '_')}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ require 'pub_sub/subscriptions_linter'
2
+ require 'pub_sub/event_handler_builder'
3
+
4
+ module PubSub
5
+ class SubscriptionsList
6
+ include Singleton
7
+
8
+ cattr_accessor :config_path
9
+ self.config_path = 'config/subscriptions.yml'
10
+
11
+ def self.load!(event_store)
12
+ instance.event_store = event_store
13
+ instance.load!
14
+ end
15
+
16
+ def self.lint!
17
+ instance.lint!
18
+ end
19
+
20
+ attr_accessor :event_store
21
+
22
+ def load!
23
+ domain_subscriptions.each do |domain_name, subscriptions|
24
+ subscriptions.each do |event_name, subscription_type|
25
+ if event_name == 'all_events'
26
+ subscribe_to_all_events(domain_name, subscription_type)
27
+ else
28
+ subscribe_to_event(domain_name, event_name, subscription_type)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def lint!
35
+ SubscriptionsLinter.new(domain_subscriptions).lint!
36
+ end
37
+
38
+ def initialize
39
+ @domain_subscriptions = YAML.load_file(self.class.config_path)
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :domain_subscriptions
45
+
46
+ def subscribe_to_all_events(domain_name, subscription_type)
47
+ handler_class = "#{domain_name.camelize}Handler".constantize
48
+ event_store.subscribe_to_all_events(
49
+ EventHandlerBuilder.new(handler_class, subscription_type)
50
+ )
51
+ end
52
+
53
+ def subscribe_to_event(domain_name, event_name, subscription_type)
54
+ event_domain, name = event_name.split('__').map(&:camelize)
55
+ event_class = "PubSub::#{event_domain}::#{name}Event".constantize
56
+ handler_class = "#{domain_name.camelize}::#{event_domain}#{name}Handler".constantize
57
+ event_store.subscribe(
58
+ EventHandlerBuilder.new(handler_class, subscription_type), to: [event_class]
59
+ )
60
+ end
61
+ end
62
+ end
@@ -2,10 +2,15 @@ module PubSub
2
2
  module Testing
3
3
  module EventDataHelper
4
4
  def event_data_for(event_name, **payload)
5
- EventClassFactory.
6
- build(event_name, abstract_event_class: payload.delete(:abstract_event_class)).
7
- new(payload).
8
- attributes
5
+ event_class = PubSub::EventClassFactory.build(
6
+ event_name, abstract_event_class: payload.delete(:abstract_event_class)
7
+ )
8
+
9
+ if event_class.ancestors.include?(PubSub::EventWithType)
10
+ event_class.new(data: payload)
11
+ else
12
+ event_class.new(payload).attributes
13
+ end
9
14
  end
10
15
  end
11
16
  end
@@ -0,0 +1,9 @@
1
+ module PubSub
2
+ module Testing
3
+ module RailsEventStore
4
+ def event_store
5
+ Rails.configuration.event_store
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ RSpec::Matchers.define :subscribe_to do |event_name|
2
+ match do |domain|
3
+ handler_class = build_handler_class(event_name, domain)
4
+ event_class = build_event_class(event_name)
5
+ subscription_type = async? ? :async : :sync
6
+
7
+ expect(
8
+ PubSub::EventHandlerBuilder.new(handler_class, subscription_type)
9
+ ).to have_subscribed_to_events(event_class).in(event_store)
10
+ end
11
+
12
+ chain :asynchronously do
13
+ @asynchronously = true
14
+ end
15
+
16
+ private
17
+
18
+ def build_handler_class(event_name, domain)
19
+ handler_name = event_name.to_s.sub('__', '/').camelize
20
+ handler_name.remove!('::')
21
+ "#{domain.name}::#{handler_name}Handler".constantize
22
+ end
23
+
24
+ def build_event_class(event_name)
25
+ event_class_name = event_name.to_s.sub('__', '/').camelize
26
+ "PubSub::#{event_class_name}Event".constantize
27
+ end
28
+
29
+ def async?
30
+ @asynchronously
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
- require 'wisper/rspec/matchers'
1
+ require 'pub_sub/testing/rails_event_store'
2
2
  require 'pub_sub/testing/event_data_helper'
3
- require 'pub_sub/testing/subscription_helpers'
3
+ require 'pub_sub/testing/subscribe_to'
@@ -1,3 +1,3 @@
1
1
  module PubSub
2
- VERSION = '0.0.7'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -1,11 +1,9 @@
1
- require 'wisper'
2
- require 'wisper/sidekiq'
1
+ require 'rails_event_store'
3
2
 
4
3
  require 'pub_sub/dry'
5
4
  require 'pub_sub'
6
5
  require 'pub_sub/emit'
7
- require 'pub_sub/subscriptions'
8
- require 'pub_sub/event_trace'
9
- require 'pub_sub/domain'
6
+ require 'pub_sub/event_with_type'
7
+ require 'pub_sub/subscriptions_list'
10
8
  require 'pub_sub/domain_event'
11
9
  require 'pub_sub/domain_event_handler'
Binary file
Binary file
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'pub_sub/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'pubsub_on_rails'
8
+ s.version = PubSub::VERSION
9
+ s.authors = ['Stevo']
10
+ s.email = ['b.kosmowski@selleo.com']
11
+ s.homepage = 'https://github.com/Selleo/pubsub_on_rails'
12
+ s.licenses = ['MIT']
13
+ s.summary = 'Opinionated publish-subscribe pattern for ruby and rails'
14
+ s.description = 'Opinionated publish-subscribe pattern for ruby and rails'
15
+
16
+ s.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
17
+ s.platform = Gem::Platform::RUBY
18
+ s.require_paths = ['lib']
19
+ s.add_dependency 'dry-struct'
20
+ s.add_dependency 'sidekiq'
21
+ s.add_dependency 'rails_event_store'
22
+ s.add_dependency 'ruby_event_store-rspec'
23
+ 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.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stevo
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-10 00:00:00.000000000 Z
11
+ date: 2023-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-struct
@@ -25,21 +25,21 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: wisper
28
+ name: sidekiq
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 2.0.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 2.0.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: wisper-sidekiq
42
+ name: rails_event_store
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: wisper-rspec
56
+ name: ruby_event_store-rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -74,32 +74,35 @@ extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
76
  - Gemfile
77
- - Gemfile.lock
78
77
  - LICENSE.md
79
78
  - README.md
80
79
  - lib/pub_sub.rb
81
- - lib/pub_sub/domain.rb
82
80
  - lib/pub_sub/domain_event.rb
83
81
  - lib/pub_sub/domain_event_handler.rb
84
82
  - lib/pub_sub/dry.rb
85
83
  - lib/pub_sub/emit.rb
86
84
  - lib/pub_sub/event_class_factory.rb
87
85
  - lib/pub_sub/event_emission.rb
88
- - lib/pub_sub/event_trace.rb
89
- - lib/pub_sub/linter.rb
86
+ - lib/pub_sub/event_handler_builder.rb
87
+ - lib/pub_sub/event_with_type.rb
88
+ - lib/pub_sub/event_worker.rb
90
89
  - lib/pub_sub/payload_attribute.rb
91
- - lib/pub_sub/pure_event.rb
92
- - lib/pub_sub/subscriptions.rb
90
+ - lib/pub_sub/subscriptions_linter.rb
91
+ - lib/pub_sub/subscriptions_list.rb
93
92
  - lib/pub_sub/testing.rb
94
93
  - lib/pub_sub/testing/event_data_helper.rb
95
- - lib/pub_sub/testing/subscription_helpers.rb
94
+ - lib/pub_sub/testing/rails_event_store.rb
95
+ - lib/pub_sub/testing/subscribe_to.rb
96
96
  - lib/pub_sub/version.rb
97
97
  - lib/pubsub_on_rails.rb
98
+ - pubsub_on_rails-0.0.7.gem
99
+ - pubsub_on_rails-1.0.0.gem
100
+ - pubsub_on_rails.gemspec
98
101
  homepage: https://github.com/Selleo/pubsub_on_rails
99
102
  licenses:
100
103
  - MIT
101
104
  metadata: {}
102
- post_install_message:
105
+ post_install_message:
103
106
  rdoc_options: []
104
107
  require_paths:
105
108
  - lib
@@ -114,9 +117,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
117
  - !ruby/object:Gem::Version
115
118
  version: '0'
116
119
  requirements: []
117
- rubyforge_project:
118
- rubygems_version: 2.6.14
119
- signing_key:
120
+ rubygems_version: 3.4.12
121
+ signing_key:
120
122
  specification_version: 4
121
123
  summary: Opinionated publish-subscribe pattern for ruby and rails
122
124
  test_files: []
data/Gemfile.lock DELETED
@@ -1,64 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- pubsub_on_rails (0.0.1)
5
- dry-struct
6
- wisper (~> 2.0.0)
7
- wisper-rspec
8
- wisper-sidekiq
9
-
10
- GEM
11
- remote: https://rubygems.org/
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
56
-
57
- PLATFORMS
58
- ruby
59
-
60
- DEPENDENCIES
61
- pubsub_on_rails!
62
-
63
- BUNDLED WITH
64
- 1.17.2
@@ -1,25 +0,0 @@
1
- module PubSub
2
- module Domain
3
- def method_missing(event_or_method_name, *event_data)
4
- if handler_name(event_or_method_name)
5
- event_payload = event_data.extract_options!
6
- EventTrace.load_from(event_payload.delete(:event_uid))
7
- const_get(handler_name(event_or_method_name)).new(event_payload).call!
8
- else
9
- super
10
- end
11
- end
12
-
13
- def handler_name(event_name)
14
- return nil unless event_name.to_s.start_with?(/[a-z_]+__/)
15
- "#{event_name.to_s.camelize}Handler"
16
- end
17
-
18
- def respond_to_missing?(event_or_method_name, include_private = false)
19
- return super unless handler_name(event_or_method_name)
20
- const_get(handler_name(event_or_method_name))
21
- rescue NameError
22
- super
23
- end
24
- end
25
- end
@@ -1,18 +0,0 @@
1
- module PubSub
2
- class EventTrace < ActiveSupport::CurrentAttributes
3
- EVENT_TRACE_UID_REGEX = /^(?<trace_id>\w+)-(?<trigger_id>\w+)-(?<event_id>\w+)$/
4
- private_constant :EVENT_TRACE_UID_REGEX
5
-
6
- attribute :trace_id
7
- attribute :last_event_id
8
-
9
- def self.load_from(event_trace_uid)
10
- match_data = event_trace_uid&.match(EVENT_TRACE_UID_REGEX)
11
-
12
- if match_data
13
- self.trace_id = match_data['trace_id']
14
- self.last_event_id = match_data['event_id']
15
- end
16
- end
17
- end
18
- end
@@ -1,39 +0,0 @@
1
- class Linter
2
- MissingSubscriptions = Class.new(StandardError)
3
-
4
- def initialize(config)
5
- @config = config
6
- end
7
-
8
- def lint!
9
- raise MissingSubscriptions, error_message if missing_subscriptions.present?
10
- end
11
-
12
- private
13
-
14
- def error_message
15
- "The following subscriptions are missing: \n#{missing_subscriptions.join("\n")}"
16
- end
17
-
18
- def missing_subscriptions
19
- (handlers_list - subscriptions_list)
20
- end
21
-
22
- attr_reader :config
23
-
24
- def subscriptions_list
25
- config.flat_map do |domain_name, subscriptions|
26
- subscriptions.keys.map do |event_name|
27
- "#{domain_name}/#{event_name.sub('__', '_')}"
28
- end
29
- end
30
- end
31
-
32
- def handlers_list
33
- Dir[Rails.root.join('app/event_handlers/*/*.rb')].map do |file_path|
34
- file_path.
35
- sub("#{Rails.root}/app/event_handlers/", '').
36
- sub('_handler.rb', '')
37
- end
38
- end
39
- end
@@ -1,19 +0,0 @@
1
- module PubSub
2
- class PureEvent < Dry::Struct
3
- include Wisper::Publisher
4
-
5
- def broadcast!
6
- broadcast(event_name, attributes_to_broadcast)
7
- end
8
-
9
- private
10
-
11
- def attributes_to_broadcast
12
- attributes
13
- end
14
-
15
- def event_name
16
- self.class.name.underscore.sub('/', '__').chomp('_event')
17
- end
18
- end
19
- end
@@ -1,49 +0,0 @@
1
- require 'pub_sub/linter'
2
-
3
- module PubSub
4
- mattr_accessor :subscriptions
5
-
6
- class Subscriptions
7
- include Singleton
8
-
9
- cattr_accessor :subscriptions_path
10
- self.subscriptions_path = 'config/subscriptions.yml'
11
-
12
- def self.load!
13
- PubSub.subscriptions = Subscriptions.instance
14
- PubSub.subscriptions.register(:all)
15
- end
16
-
17
- def self.lint!
18
- instance.lint!
19
- end
20
-
21
- def lint!
22
- Linter.new(config).lint!
23
- end
24
-
25
- def initialize
26
- @config = YAML.load_file(self.class.subscriptions_path)
27
- end
28
-
29
- def register(scope = :all)
30
- (scope == :all ? config : config.slice(scope.to_s)).each do |domain_name, subscriptions|
31
- subscriptions.each do |event_name, subscription_type|
32
- options = {}
33
- options[:on] = event_name unless event_name == 'all_events'
34
- options[:broadcaster] = subscription_type == 'sync' ? :default : subscription_type.to_sym
35
-
36
- Wisper.subscribe("::#{domain_name.camelize}".constantize, options)
37
- end
38
- end
39
- end
40
-
41
- def clear!
42
- Wisper.clear
43
- end
44
-
45
- private
46
-
47
- attr_reader :config
48
- end
49
- end
@@ -1,24 +0,0 @@
1
- module PubSub
2
- module Testing
3
- module SubscriptionsHelper
4
- def with_subscription_to(*domains)
5
- domains.each do |domain|
6
- PubSub.subscriptions.register(domain)
7
- end
8
- yield
9
- clear_wisper_subscriptions!
10
- end
11
-
12
- def subscribe_logger!
13
- PubSub.subscriptions.register(:logging)
14
- end
15
- module_function :subscribe_logger!
16
-
17
- def clear_wisper_subscriptions!
18
- PubSub.subscriptions.clear!
19
- subscribe_logger!
20
- end
21
- module_function :clear_wisper_subscriptions!
22
- end
23
- end
24
- end