downstream 1.5.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: 7596d3ccbd47d258c9b949be9674651c600b75276cd8e38a3bbc021e95ec6214
4
- data.tar.gz: 8c7c3a0525f38c1a2e155153edca9d5edf39b646495dcc9815dd9181ca502987
3
+ metadata.gz: 6117947c15849270ac37c5d1a02877b5903884f224cb87bb10fca63f8535c0b1
4
+ data.tar.gz: 11fcc576683c11ed8bd56fd6f2abc22d7adfd56d39cd33162a41f706debebc0b
5
5
  SHA512:
6
- metadata.gz: a02e6ffa1585f399a003750332bc4807bc1d50675f8a8d50968eb8e39de8a7589da82322225a2383d72416d6c2c5fe67aabb376c6df71e6b952579737b587054
7
- data.tar.gz: 72223db631146986eeb611bd320bae2bf6fc2aa70dc23037a5a8bc820ad84a0015b1b888d51717fb706257ea2a241113552024dde565636adef1951d871286e7
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.
@@ -153,9 +189,9 @@ end
153
189
  # for asynchronous subscriptions
154
190
  it "is subscribed to some event" do
155
191
  event = MyEvent.new(some: "data")
156
- expect { Downstream.publish event }.
157
- to have_enqueued_async_subscriber_for(MySubscriberService).
158
- with(event)
192
+ expect { Downstream.publish event }
193
+ .to have_enqueued_async_subscriber_for(MySubscriberService)
194
+ .with(event)
159
195
  end
160
196
  ```
161
197
 
@@ -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,7 +6,21 @@ 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
23
+ Downstream.pubsub.reset
10
24
  ActiveSupport.run_load_hooks("downstream-events", Downstream)
11
25
  end
12
26
  end
@@ -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
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Downstream
4
4
  class AbstractPubsub
5
+ def reset
6
+ raise NotImplementedError
7
+ end
8
+
5
9
  def subscribe(identifier, callable)
6
10
  raise NotImplementedError
7
11
  end
@@ -6,8 +6,20 @@ require_relative "subscriber"
6
6
  module Downstream
7
7
  module Stateless
8
8
  class Pubsub < AbstractPubsub
9
+ def initialize
10
+ @subscribers = []
11
+ end
12
+
13
+ def reset
14
+ @subscribers.each(&:unsubscribe)
15
+ @subscribers.clear
16
+ end
17
+
9
18
  def subscribe(identifier, callable, async: false)
10
- Subscriber.new(callable, async: async).tap { |s| s.subscribe(identifier) }
19
+ Subscriber.new(callable, async: async).tap do |s|
20
+ s.subscribe(identifier)
21
+ @subscribers << s
22
+ end
11
23
  end
12
24
 
13
25
  def subscribed(identifier, callable, &block)
@@ -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.5.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.5.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
- bindir: exe
9
+ bindir: bin
11
10
  cert_chain: []
12
- date: 2024-04-26 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,28 +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'
56
- - !ruby/object:Gem::Dependency
57
- name: appraisal
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - "~>"
61
- - !ruby/object:Gem::Version
62
- version: '2.2'
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - "~>"
68
- - !ruby/object:Gem::Version
69
- version: '2.2'
54
+ version: '7'
70
55
  - !ruby/object:Gem::Dependency
71
56
  name: bundler
72
57
  requirement: !ruby/object:Gem::Requirement
@@ -95,48 +80,20 @@ dependencies:
95
80
  - - "~>"
96
81
  - !ruby/object:Gem::Version
97
82
  version: '1.3'
98
- - !ruby/object:Gem::Dependency
99
- name: debug
100
- requirement: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - "~>"
103
- - !ruby/object:Gem::Version
104
- version: '1.3'
105
- type: :development
106
- prerelease: false
107
- version_requirements: !ruby/object:Gem::Requirement
108
- requirements:
109
- - - "~>"
110
- - !ruby/object:Gem::Version
111
- version: '1.3'
112
83
  - !ruby/object:Gem::Dependency
113
84
  name: rake
114
85
  requirement: !ruby/object:Gem::Requirement
115
86
  requirements:
116
- - - "~>"
87
+ - - ">="
117
88
  - !ruby/object:Gem::Version
118
89
  version: '13.0'
119
90
  type: :development
120
91
  prerelease: false
121
92
  version_requirements: !ruby/object:Gem::Requirement
122
93
  requirements:
123
- - - "~>"
94
+ - - ">="
124
95
  - !ruby/object:Gem::Version
125
96
  version: '13.0'
126
- - !ruby/object:Gem::Dependency
127
- name: rspec
128
- requirement: !ruby/object:Gem::Requirement
129
- requirements:
130
- - - "~>"
131
- - !ruby/object:Gem::Version
132
- version: '3.0'
133
- type: :development
134
- prerelease: false
135
- version_requirements: !ruby/object:Gem::Requirement
136
- requirements:
137
- - - "~>"
138
- - !ruby/object:Gem::Version
139
- version: '3.0'
140
97
  - !ruby/object:Gem::Dependency
141
98
  name: rspec-rails
142
99
  requirement: !ruby/object:Gem::Requirement
@@ -151,36 +108,6 @@ dependencies:
151
108
  - - "~>"
152
109
  - !ruby/object:Gem::Version
153
110
  version: '6.0'
154
- - !ruby/object:Gem::Dependency
155
- name: sqlite3
156
- requirement: !ruby/object:Gem::Requirement
157
- requirements:
158
- - - "~>"
159
- - !ruby/object:Gem::Version
160
- version: 1.4.0
161
- type: :development
162
- prerelease: false
163
- version_requirements: !ruby/object:Gem::Requirement
164
- requirements:
165
- - - "~>"
166
- - !ruby/object:Gem::Version
167
- version: 1.4.0
168
- - !ruby/object:Gem::Dependency
169
- name: standard
170
- requirement: !ruby/object:Gem::Requirement
171
- requirements:
172
- - - "~>"
173
- - !ruby/object:Gem::Version
174
- version: '1.3'
175
- type: :development
176
- prerelease: false
177
- version_requirements: !ruby/object:Gem::Requirement
178
- requirements:
179
- - - "~>"
180
- - !ruby/object:Gem::Version
181
- version: '1.3'
182
- description:
183
- email:
184
111
  executables: []
185
112
  extensions: []
186
113
  extra_rdoc_files: []
@@ -189,6 +116,7 @@ files:
189
116
  - README.md
190
117
  - lib/downstream.rb
191
118
  - lib/downstream/config.rb
119
+ - lib/downstream/data_event.rb
192
120
  - lib/downstream/engine.rb
193
121
  - lib/downstream/event.rb
194
122
  - lib/downstream/pubsub_adapters/abstract_pubsub.rb
@@ -197,14 +125,19 @@ files:
197
125
  - lib/downstream/rspec.rb
198
126
  - lib/downstream/rspec/have_enqueued_async_subscriber_for.rb
199
127
  - lib/downstream/rspec/have_published_event.rb
128
+ - lib/downstream/subscriber.rb
200
129
  - lib/downstream/subscriber_job.rb
201
130
  - lib/downstream/version.rb
202
131
  homepage: https://github.com/palkan/downstream
203
132
  licenses:
204
133
  - MIT
205
134
  metadata:
135
+ bug_tracker_uri: http://github.com/palkan/downstream/issues
136
+ changelog_uri: https://github.com/palkan/downstream/blob/master/CHANGELOG.md
137
+ documentation_uri: http://github.com/palkan/downstream
138
+ homepage_uri: http://github.com/palkan/downstream
139
+ source_code_uri: http://github.com/palkan/downstream
206
140
  allowed_push_host: https://rubygems.org
207
- post_install_message:
208
141
  rdoc_options: []
209
142
  require_paths:
210
143
  - lib
@@ -212,15 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
212
145
  requirements:
213
146
  - - ">="
214
147
  - !ruby/object:Gem::Version
215
- version: '2.7'
148
+ version: '3.1'
216
149
  required_rubygems_version: !ruby/object:Gem::Requirement
217
150
  requirements:
218
151
  - - ">="
219
152
  - !ruby/object:Gem::Version
220
153
  version: '0'
221
154
  requirements: []
222
- rubygems_version: 3.4.19
223
- signing_key:
155
+ rubygems_version: 3.6.9
224
156
  specification_version: 4
225
157
  summary: Straightforward way to implement communication between Rails Engines using
226
158
  the Publish-Subscribe pattern