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 +4 -4
- data/README.md +36 -0
- data/lib/downstream/data_event.rb +32 -0
- data/lib/downstream/engine.rb +13 -0
- data/lib/downstream/event.rb +16 -15
- 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 +8 -11
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.
|
@@ -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,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)
|
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
|
@@ -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
|
-
autorequire:
|
10
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,14 +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: '
|
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: '
|
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.
|
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
|