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 +4 -4
- data/README.md +39 -3
- data/lib/downstream/data_event.rb +32 -0
- data/lib/downstream/engine.rb +14 -0
- data/lib/downstream/event.rb +16 -15
- data/lib/downstream/pubsub_adapters/abstract_pubsub.rb +4 -0
- data/lib/downstream/pubsub_adapters/stateless/pubsub.rb +13 -1
- data/lib/downstream/rspec/have_published_event.rb +1 -1
- data/lib/downstream/subscriber.rb +32 -0
- data/lib/downstream/version.rb +1 -1
- data/lib/downstream.rb +30 -14
- metadata +16 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6117947c15849270ac37c5d1a02877b5903884f224cb87bb10fca63f8535c0b1
|
4
|
+
data.tar.gz: 11fcc576683c11ed8bd56fd6f2abc22d7adfd56d39cd33162a41f706debebc0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/downstream/engine.rb
CHANGED
@@ -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
|
data/lib/downstream/event.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
-
|
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
|
data/lib/downstream/version.rb
CHANGED
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
51
|
-
to ||=
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
75
|
+
"#{config.namespace}.#{identifier}"
|
76
|
+
end
|
65
77
|
end
|
66
78
|
|
67
|
-
def
|
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
|
87
|
+
# drop last part—it'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:
|
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
|
-
|
10
|
-
bindir: exe
|
9
|
+
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
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: '
|
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: '
|
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: '
|
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.
|
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
|