downstream 1.6.0 → 2.0.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: 1e21fb36c5475aee1858bcdbfa9f99db92a62059f74605906f95034e8661df0e
4
- data.tar.gz: 0bf3825da8014d5a0daed6cbdcf21303d11b9657af42d96e55e5fe4488c0981b
3
+ metadata.gz: 6117947c15849270ac37c5d1a02877b5903884f224cb87bb10fca63f8535c0b1
4
+ data.tar.gz: 11fcc576683c11ed8bd56fd6f2abc22d7adfd56d39cd33162a41f706debebc0b
5
5
  SHA512:
6
- metadata.gz: b8b80db17bd482235eff304bf087c1eecd0e3a1a605cf4590f3c19cae725c1170cd8404dcab2b0603d67a726e53e08fc140069b38a4dcaf9e490a816a04e5b4d
7
- data.tar.gz: 188c160278a9578615796d867833b1377bc1664b5ad1f750d4cd21d6eea3828eef956bf0a9570c0889d0b6a1b240c783df9bdb9c340768bc89893b3bdf00a392
6
+ metadata.gz: 8228d50071d6929e3ec64cdfe1a46c81a7a4045b043329044d731e746a7da0e4f1363a70420d04e5db1c5a66166bc8732245c1c10ccfcec4563ce382b277becc
7
+ data.tar.gz: 30e6fbf37793b809158a128db70cd24121d39ac83510ad024db68874d38485f5b576b9d9d1a4d5775092401ccc8f22dae66b7d557836065055da974922479f60
data/README.md CHANGED
@@ -59,6 +59,22 @@ Each event has predefined (_reserved_) fields:
59
59
 
60
60
  Events are stored in `app/events` folder.
61
61
 
62
+ You can also define events using the Data-interface:
63
+
64
+ ```ruby
65
+ ProfileCreated = Downstream::Event.define(:user)
66
+
67
+ # or with an explicit identifier
68
+ ProfileCreated = Downstream::Event.define(:user) do
69
+ self.identifier = "user.profile_created"
70
+ end
71
+ ```
72
+
73
+ Date-events provide the same interface as regular events but use Data classes for keeping event payloads (`event.data`) and are frozen (as well as their derivatives, such as `event.to_h`).
74
+
75
+ > [!NOTE]
76
+ > Data-events are only available in Ruby 3.2+.
77
+
62
78
  ### Publish events
63
79
 
64
80
  To publish an event you must first create an instance of the event class and call `Downstream.publish` method:
@@ -127,6 +143,26 @@ store.subscribe OnProfileCreated::DoThat, async: {queue: :low_priority}
127
143
 
128
144
  **NOTE:** all subscribers are synchronous by default
129
145
 
146
+ ### Subscriber classes
147
+
148
+ You can also use subscriber objects based on `Downstream::Subscriber` class. For example:
149
+
150
+ ```ruby
151
+ class CRMSubscriber < Downstream::Subscriber
152
+ def profile_created(event)
153
+ # handle "profile_created" event
154
+ end
155
+
156
+ def project_created(event)
157
+ # handle "project_created" event
158
+ end
159
+ end
160
+
161
+ store.subscribe CRMSubscriber, async: true
162
+ ```
163
+
164
+ The subscriber object allows you to subscribe to multiple events at once: each public method is considered an event handler for the same named event.
165
+
130
166
  ## Testing
131
167
 
132
168
  You can test subscribers as normal Ruby objects.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Downstream
4
+ class DataEvent < Event
5
+ class << self
6
+ attr_writer :data_class
7
+
8
+ def data_class
9
+ return @data_class if @data_class
10
+
11
+ @data_class = superclass.data_class
12
+ end
13
+
14
+ undef_method :attributes
15
+ undef_method :defined_attributes
16
+ end
17
+
18
+ def initialize(event_id: nil, **attrs)
19
+ @event_id = event_id || SecureRandom.hex(10)
20
+ @data = self.class.data_class.new(**attrs)
21
+ freeze
22
+ end
23
+
24
+ def to_h
25
+ {
26
+ type:,
27
+ event_id:,
28
+ data: data.to_h.freeze
29
+ }.freeze
30
+ end
31
+ end
32
+ end
@@ -6,6 +6,19 @@ module Downstream
6
6
  class Engine < ::Rails::Engine
7
7
  config.downstream = Downstream.config
8
8
 
9
+ ::GlobalID::Locator.use "downstream" do |gid|
10
+ params = gid.params.each_with_object({}) do |(key, value), memo|
11
+ memo[key.to_sym] = if value.is_a?(String) && value.start_with?("gid://")
12
+ GlobalID::Locator.locate(value)
13
+ else
14
+ value
15
+ end
16
+ end
17
+
18
+ gid.model_name.constantize
19
+ .new(event_id: gid.model_id, **params)
20
+ end
21
+
9
22
  config.to_prepare do
10
23
  Downstream.pubsub.reset
11
24
  ActiveSupport.run_load_hooks("downstream-events", Downstream)
@@ -1,18 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- GlobalID::Locator.use "downstream" do |gid|
4
- params = gid.params.each_with_object({}) do |(key, value), memo|
5
- memo[key.to_sym] = if value.is_a?(String) && value.start_with?("gid://")
6
- GlobalID::Locator.locate(value)
7
- else
8
- value
9
- end
10
- end
11
-
12
- gid.model_name.constantize
13
- .new(event_id: gid.model_id, **params)
14
- end
15
-
16
3
  module Downstream
17
4
  class Event
18
5
  extend ActiveModel::Naming
@@ -26,7 +13,7 @@ module Downstream
26
13
  def identifier
27
14
  return @identifier if instance_variable_defined?(:@identifier)
28
15
 
29
- @identifier = name.underscore.tr("/", ".")
16
+ @identifier = name.underscore.tr("/", ".").gsub(/_event$/, "")
30
17
  end
31
18
 
32
19
  # define store readers
@@ -56,6 +43,20 @@ module Downstream
56
43
  end
57
44
  end
58
45
 
46
+ def define(*fields, &)
47
+ fields.each do |field|
48
+ raise ArgumentError, "#{field} is reserved" if RESERVED_ATTRIBUTES.include?(field)
49
+ end
50
+
51
+ data_class = ::Data.define(*fields)
52
+
53
+ Class.new(DataEvent, &).tap do
54
+ _1.data_class = data_class
55
+
56
+ _1.delegate(*fields, to: :data)
57
+ end
58
+ end
59
+
59
60
  def i18n_scope
60
61
  :activemodel
61
62
  end
@@ -94,7 +95,7 @@ module Downstream
94
95
  end
95
96
 
96
97
  def to_global_id
97
- new_data = data.each_with_object({}) do |(key, value), memo|
98
+ new_data = data.to_h.each_with_object({}) do |(key, value), memo|
98
99
  memo[key] = if value.respond_to?(:to_global_id)
99
100
  value.to_global_id
100
101
  else
@@ -74,7 +74,7 @@ module Downstream
74
74
  end
75
75
 
76
76
  def failure_message
77
- (+"expected to publish #{event_class.identifier} event").tap do |msg|
77
+ "expected to publish #{event_class.identifier} event".tap do |msg|
78
78
  msg << " #{message_expectation_modifier}, but haven't published"
79
79
  end
80
80
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Downstream
4
+ class Subscriber
5
+ class << self
6
+ # All public names are considered event handlers
7
+ # (same concept as action_names in controllers/mailers)
8
+ def event_names
9
+ @event_names ||= begin
10
+ # All public instance methods of this class, including ancestors
11
+ methods = (public_instance_methods(true) -
12
+ # Except for public instance methods of Base and its ancestors
13
+ Downstream.public_instance_methods(true) +
14
+ # Be sure to include shadowed public instance methods of this class
15
+ public_instance_methods(false)).uniq.map(&:to_s)
16
+ methods.to_set
17
+ end
18
+ end
19
+
20
+ # Downstream subscriber interface
21
+ def call(event)
22
+ new.process_event(event)
23
+ end
24
+ end
25
+
26
+ def process_event(event)
27
+ # TODO: callbacks? instrumentation?
28
+ # TODO: namespaced events?
29
+ public_send(event.type, event)
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Downstream
4
- VERSION = "1.6.0"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/downstream.rb CHANGED
@@ -8,6 +8,8 @@ require "after_commit_everywhere"
8
8
 
9
9
  require "downstream/config"
10
10
  require "downstream/event"
11
+ require "downstream/data_event"
12
+ require "downstream/subscriber"
11
13
  require "downstream/pubsub_adapters/abstract_pubsub"
12
14
  require "downstream/subscriber_job"
13
15
 
@@ -27,18 +29,26 @@ module Downstream
27
29
  subscriber ||= block if block
28
30
  raise ArgumentError, "Subsriber must be present" if subscriber.nil?
29
31
 
30
- identifier = construct_identifier(subscriber, to)
32
+ construct_identifiers(subscriber, to).map do
33
+ pubsub.subscribe(_1, subscriber, async: async)
34
+ end.then do
35
+ next _1.first if _1.size == 1
31
36
 
32
- pubsub.subscribe(identifier, subscriber, async: async)
37
+ _1
38
+ end
33
39
  end
34
40
 
35
41
  # temporary subscriptions
36
42
  def subscribed(subscriber, to: nil, &block)
37
43
  raise ArgumentError, "Subsriber must be present" if subscriber.nil?
38
44
 
39
- identifier = construct_identifier(subscriber, to)
45
+ construct_identifiers(subscriber, to).map do
46
+ pubsub.subscribed(_1, subscriber, &block)
47
+ end.then do
48
+ next _1.first if _1.size == 1
40
49
 
41
- pubsub.subscribed(identifier, subscriber, &block)
50
+ _1
51
+ end
42
52
  end
43
53
 
44
54
  def publish(event)
@@ -47,28 +57,34 @@ module Downstream
47
57
 
48
58
  private
49
59
 
50
- def construct_identifier(subscriber, to)
51
- to ||= infer_event_from_subscriber(subscriber) if subscriber.is_a?(Module)
60
+ def construct_identifiers(subscriber, to)
61
+ to ||= infer_events_from_subscriber(subscriber) if subscriber.is_a?(Module)
52
62
 
53
63
  if to.nil?
54
64
  raise ArgumentError, "Couldn't infer event from subscriber. " \
55
65
  "Please, specify event using `to:` option"
56
66
  end
57
67
 
58
- identifier = if to.is_a?(Class) && Event >= to # rubocop:disable Style/YodaCondition
59
- to.identifier
60
- else
61
- to
62
- end
68
+ Array(to).map do
69
+ identifier = if _1.is_a?(Class) && Event >= _1 # rubocop:disable Style/YodaCondition
70
+ _1.identifier
71
+ else
72
+ _1
73
+ end
63
74
 
64
- "#{config.namespace}.#{identifier}"
75
+ "#{config.namespace}.#{identifier}"
76
+ end
65
77
  end
66
78
 
67
- def infer_event_from_subscriber(subscriber)
79
+ def infer_events_from_subscriber(subscriber)
80
+ if subscriber.is_a?(Class) && Subscriber >= subscriber # rubocop:disable Style/YodaCondition
81
+ return subscriber.event_names
82
+ end
83
+
68
84
  event_class_name = subscriber.name.split("::").yield_self do |parts|
69
85
  # handle explicti top-level name, e.g. ::Some::Event
70
86
  parts.shift if parts.first.empty?
71
- # drop last part – it's a unique subscriber name
87
+ # drop last partit's a unique subscriber name
72
88
  parts.pop
73
89
 
74
90
  parts.last.sub!(/^On/, "")
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: downstream
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - merkushin.m.s@gmail.com
8
8
  - dementiev.vm@gmail.com
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2025-02-14 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: after_commit_everywhere
@@ -45,14 +44,14 @@ dependencies:
45
44
  requirements:
46
45
  - - ">="
47
46
  - !ruby/object:Gem::Version
48
- version: '6'
47
+ version: '7'
49
48
  type: :runtime
50
49
  prerelease: false
51
50
  version_requirements: !ruby/object:Gem::Requirement
52
51
  requirements:
53
52
  - - ">="
54
53
  - !ruby/object:Gem::Version
55
- version: '6'
54
+ version: '7'
56
55
  - !ruby/object:Gem::Dependency
57
56
  name: bundler
58
57
  requirement: !ruby/object:Gem::Requirement
@@ -109,8 +108,6 @@ dependencies:
109
108
  - - "~>"
110
109
  - !ruby/object:Gem::Version
111
110
  version: '6.0'
112
- description:
113
- email:
114
111
  executables: []
115
112
  extensions: []
116
113
  extra_rdoc_files: []
@@ -119,6 +116,7 @@ files:
119
116
  - README.md
120
117
  - lib/downstream.rb
121
118
  - lib/downstream/config.rb
119
+ - lib/downstream/data_event.rb
122
120
  - lib/downstream/engine.rb
123
121
  - lib/downstream/event.rb
124
122
  - lib/downstream/pubsub_adapters/abstract_pubsub.rb
@@ -127,6 +125,7 @@ files:
127
125
  - lib/downstream/rspec.rb
128
126
  - lib/downstream/rspec/have_enqueued_async_subscriber_for.rb
129
127
  - lib/downstream/rspec/have_published_event.rb
128
+ - lib/downstream/subscriber.rb
130
129
  - lib/downstream/subscriber_job.rb
131
130
  - lib/downstream/version.rb
132
131
  homepage: https://github.com/palkan/downstream
@@ -139,7 +138,6 @@ metadata:
139
138
  homepage_uri: http://github.com/palkan/downstream
140
139
  source_code_uri: http://github.com/palkan/downstream
141
140
  allowed_push_host: https://rubygems.org
142
- post_install_message:
143
141
  rdoc_options: []
144
142
  require_paths:
145
143
  - lib
@@ -147,15 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
145
  requirements:
148
146
  - - ">="
149
147
  - !ruby/object:Gem::Version
150
- version: '2.7'
148
+ version: '3.1'
151
149
  required_rubygems_version: !ruby/object:Gem::Requirement
152
150
  requirements:
153
151
  - - ">="
154
152
  - !ruby/object:Gem::Version
155
153
  version: '0'
156
154
  requirements: []
157
- rubygems_version: 3.4.19
158
- signing_key:
155
+ rubygems_version: 3.6.9
159
156
  specification_version: 4
160
157
  summary: Straightforward way to implement communication between Rails Engines using
161
158
  the Publish-Subscribe pattern