downstream 1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +122 -0
- data/lib/downstream/config.rb +11 -0
- data/lib/downstream/engine.rb +13 -0
- data/lib/downstream/event.rb +97 -0
- data/lib/downstream/rspec/have_published_event.rb +129 -0
- data/lib/downstream/rspec.rb +3 -0
- data/lib/downstream/version.rb +5 -0
- data/lib/downstream.rb +82 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 44d7614f5509bb04efddc18f5dd26f4030a4262c00e6a370da13797d1efd99a6
|
4
|
+
data.tar.gz: 8ea205b3d718e21b01a82cb65b51ae1870068f1065f679af4e4fe94bcdebe87a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5376229dc224f18496764928e4019bd72791ac660bc90f975abba38fe961e305dd9e6d997573aae7ef9acd3b785659ff9171678d2ccd1dd02f89add6fa5b01cb
|
7
|
+
data.tar.gz: f5a17a4a6f8383b35820914cc658fe97a41b2554df16c2088dcac3857af264e7976c5af3ab5249635e884de1ffe590dd32d270583230e79fa63ad09b5ae5294e
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Michael Merkushin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
[](https://badge.fury.io/rb/downstream)
|
2
|
+
[](https://github.com/bibendi/downstream/actions?query=branch%3Amaster)
|
3
|
+
|
4
|
+
# Downstream
|
5
|
+
|
6
|
+
This gem provides a straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern. The gem allows decreasing decoupling engines with events. An event is a recorded object in the system that reflects an action that the engine performs, and the params that lead to its creation.
|
7
|
+
|
8
|
+
The gem inspired by [`active_event_store`](https://github.com/palkan/active_event_store), and initially based on its codebase. Having said that, it does not store in a database all happened events which ensures simplicity and performance.
|
9
|
+
|
10
|
+
<a href="https://evilmartians.com/?utm_source=bibendi-downstream">
|
11
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem "downstream", "~> 1.0"
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Under the hood it's a wrapper for `ActiveSupport::Notifications`. But it provides a way more handy interface to build reactive apps. Each event has a strict schema described by a separate class. Also, the gem has convenient tooling to write tests.
|
24
|
+
|
25
|
+
### Describe events
|
26
|
+
|
27
|
+
Events are represented by _event classes_, which describe events payloads and identifiers:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class ProfileCreated < Downstream::Event
|
31
|
+
# (optional)
|
32
|
+
# Event identifier is used for streaming events to subscribers.
|
33
|
+
# By default, identifier is equal to underscored class name.
|
34
|
+
# You don't need to specify identifier manually, only for backward compatibility when
|
35
|
+
# class name is changed.
|
36
|
+
self.identifier = "profile_created"
|
37
|
+
|
38
|
+
# Add attributes accessors
|
39
|
+
attributes :user
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
Each event has predefined (_reserved_) fields:
|
44
|
+
- `event_id` – unique event id
|
45
|
+
- `type` – event type (=identifier)
|
46
|
+
|
47
|
+
**NOTE:** events should be in the past tense and describe what happened (e.g. "ProfileCreated", "EventPublished", etc.).
|
48
|
+
|
49
|
+
Events are stored in `app/events` folder.
|
50
|
+
|
51
|
+
### Publish events
|
52
|
+
|
53
|
+
To publish an event you must first create an instance of the event class and call `Downstream.publish` method:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
event = ProfileCompleted.new(user: user)
|
57
|
+
|
58
|
+
# then publish the event
|
59
|
+
Downstream.publish(event)
|
60
|
+
```
|
61
|
+
|
62
|
+
That's it! Your event has been stored and propagated.
|
63
|
+
|
64
|
+
### Subscribe to events
|
65
|
+
|
66
|
+
To subscribe a handler to an event you must use `Downstream.subscribe` method.
|
67
|
+
|
68
|
+
You should do this in your app or engine initializer:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# some/engine.rb
|
72
|
+
|
73
|
+
initializer "my_engine.subscribe_to_events" do
|
74
|
+
# To make sure event store is initialized use load hook
|
75
|
+
# `store` == `Downstream`
|
76
|
+
ActiveSupport.on_load "downstream-events" do |store|
|
77
|
+
store.subscribe MyEventHandler, to: ProfileCreated
|
78
|
+
|
79
|
+
# anonymous handler (could only be synchronous)
|
80
|
+
store.subscribe(to: ProfileCreated) do |name, event|
|
81
|
+
# do something
|
82
|
+
end
|
83
|
+
|
84
|
+
# you can omit event if your subscriber follows the convention
|
85
|
+
# for example, the following subscriber would subscribe to
|
86
|
+
# ProfileCreated event
|
87
|
+
store.subscribe OnProfileCreated::DoThat
|
88
|
+
end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
**NOTE:** event handler **must** be a callable object.
|
93
|
+
|
94
|
+
Although subscriber could be any callable Ruby object, that have specific input format (event); thus we suggest putting subscribers under `app/subscribers/on_<event_type>/<subscriber.rb>`, e.g. `app/subscribers/on_profile_created/create_chat_user.rb`).
|
95
|
+
|
96
|
+
## Testing
|
97
|
+
|
98
|
+
You can test subscribers as normal Ruby objects.
|
99
|
+
|
100
|
+
To test that a given subscriber exists, you can do the following:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
it "is subscribed to some event" do
|
104
|
+
allow(MySubscriberService).to receive(:call)
|
105
|
+
|
106
|
+
event = MyEvent.new(some: "data")
|
107
|
+
|
108
|
+
Downstream.publish event
|
109
|
+
|
110
|
+
expect(MySubscriberService).to have_received(:call).with(event)
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
To test publishing use `have_published_event` matcher:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
expect { subject }.to have_published_event(ProfileCreated).with(user: user)
|
118
|
+
```
|
119
|
+
|
120
|
+
**NOTE:** `have_published_event` only supports block expectations.
|
121
|
+
|
122
|
+
**NOTE 2** `with` modifier works like `have_attributes` matcher (not `contain_exactly`);
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/engine"
|
4
|
+
|
5
|
+
module Downstream
|
6
|
+
class Engine < ::Rails::Engine
|
7
|
+
config.downstream = Downstream.config
|
8
|
+
|
9
|
+
config.to_prepare do
|
10
|
+
ActiveSupport.run_load_hooks("downstream-events", Downstream)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Downstream
|
4
|
+
class Event
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
|
7
|
+
RESERVED_ATTRIBUTES = %i[event_id type].freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_writer :identifier
|
11
|
+
|
12
|
+
def identifier
|
13
|
+
return @identifier if instance_variable_defined?(:@identifier)
|
14
|
+
|
15
|
+
@identifier = name.underscore.tr("/", ".")
|
16
|
+
end
|
17
|
+
|
18
|
+
# define store readers
|
19
|
+
def attributes(*fields)
|
20
|
+
fields.each do |field|
|
21
|
+
raise ArgumentError, "#{field} is reserved" if RESERVED_ATTRIBUTES.include?(field)
|
22
|
+
|
23
|
+
defined_attributes << field
|
24
|
+
|
25
|
+
# TODO: rewrite with define_method
|
26
|
+
class_eval <<~CODE, __FILE__, __LINE__ + 1
|
27
|
+
def #{field}
|
28
|
+
data[:#{field}]
|
29
|
+
end
|
30
|
+
CODE
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def defined_attributes
|
35
|
+
return @defined_attributes if instance_variable_defined?(:@defined_attributes)
|
36
|
+
|
37
|
+
@defined_attributes =
|
38
|
+
if superclass.respond_to?(:defined_attributes)
|
39
|
+
superclass.defined_attributes.dup
|
40
|
+
else
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def i18n_scope
|
46
|
+
:activemodel
|
47
|
+
end
|
48
|
+
|
49
|
+
def human_attribute_name(attr, options = {})
|
50
|
+
attr
|
51
|
+
end
|
52
|
+
|
53
|
+
def lookup_ancestors
|
54
|
+
[self]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :event_id, :data, :errors
|
59
|
+
|
60
|
+
def initialize(event_id: nil, **params)
|
61
|
+
@event_id = event_id || SecureRandom.hex(10)
|
62
|
+
validate_attributes!(params)
|
63
|
+
|
64
|
+
@errors = ActiveModel::Errors.new(self)
|
65
|
+
@data = params
|
66
|
+
end
|
67
|
+
|
68
|
+
def type
|
69
|
+
self.class.identifier
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_h
|
73
|
+
{
|
74
|
+
type: type,
|
75
|
+
event_id: event_id,
|
76
|
+
data: data
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#{self.class.name}<#{type}##{event_id}>, data: #{data}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_attribute_for_validation(attr)
|
85
|
+
data.fetch(attr)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def validate_attributes!(params)
|
91
|
+
unknown_fields = params.keys.map(&:to_sym) - self.class.defined_attributes
|
92
|
+
unless unknown_fields.empty?
|
93
|
+
raise ArgumentError, "Unknown event attributes: #{unknown_fields.join(", ")}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Downstream
|
4
|
+
class HavePublishedEvent < RSpec::Matchers::BuiltIn::BaseMatcher
|
5
|
+
attr_reader :event_class, :attributes
|
6
|
+
|
7
|
+
def initialize(event_class)
|
8
|
+
@event_class = event_class
|
9
|
+
set_expected_number(:exactly, 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
def with(attributes)
|
13
|
+
@attributes = attributes
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def exactly(count)
|
18
|
+
set_expected_number(:exactly, count)
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def at_least(count)
|
23
|
+
set_expected_number(:at_least, count)
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def at_most(count)
|
28
|
+
set_expected_number(:at_most, count)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def times
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def once
|
37
|
+
exactly(:once)
|
38
|
+
end
|
39
|
+
|
40
|
+
def twice
|
41
|
+
exactly(:twice)
|
42
|
+
end
|
43
|
+
|
44
|
+
def thrice
|
45
|
+
exactly(:thrice)
|
46
|
+
end
|
47
|
+
|
48
|
+
def supports_block_expectations?
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def matches?(block)
|
53
|
+
raise ArgumentError, "have_published_event only supports block expectations" unless block.is_a?(Proc)
|
54
|
+
|
55
|
+
events = []
|
56
|
+
namespace = /^#{Downstream.config.namespace}\./
|
57
|
+
ActiveSupport::Notifications.subscribed(->(name, event) { events << event }, namespace) do
|
58
|
+
block.call
|
59
|
+
end
|
60
|
+
|
61
|
+
@matching_events, @unmatching_events =
|
62
|
+
events.partition do |actual_event|
|
63
|
+
(event_class.identifier == actual_event.type) &&
|
64
|
+
(attributes.nil? || attributes_match?(actual_event))
|
65
|
+
end
|
66
|
+
|
67
|
+
@matching_count = @matching_events.size
|
68
|
+
|
69
|
+
case @expectation_type
|
70
|
+
when :exactly then @expected_number == @matching_count
|
71
|
+
when :at_most then @expected_number >= @matching_count
|
72
|
+
when :at_least then @expected_number <= @matching_count
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def failure_message
|
77
|
+
(+"expected to publish #{event_class.identifier} event").tap do |msg|
|
78
|
+
msg << " #{message_expectation_modifier}, but"
|
79
|
+
|
80
|
+
if @unmatching_events.any?
|
81
|
+
msg << " published the following events:"
|
82
|
+
@unmatching_events.each do |unmatching_event|
|
83
|
+
msg << "\n #{unmatching_event.inspect}"
|
84
|
+
end
|
85
|
+
else
|
86
|
+
msg << " haven't published anything"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def failure_message_when_negated
|
92
|
+
"expected not to publish #{event_class.identifier} event"
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def attributes_match?(event)
|
98
|
+
RSpec::Matchers::BuiltIn::HaveAttributes.new(attributes).matches?(event)
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_expected_number(relativity, count)
|
102
|
+
@expectation_type = relativity
|
103
|
+
@expected_number =
|
104
|
+
case count
|
105
|
+
when :once then 1
|
106
|
+
when :twice then 2
|
107
|
+
when :thrice then 3
|
108
|
+
else Integer(count)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def message_expectation_modifier
|
113
|
+
number_modifier = @expected_number == 1 ? "once" : "#{@expected_number} times"
|
114
|
+
case @expectation_type
|
115
|
+
when :exactly then "exactly #{number_modifier}"
|
116
|
+
when :at_most then "at most #{number_modifier}"
|
117
|
+
when :at_least then "at least #{number_modifier}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
RSpec.configure do |config|
|
124
|
+
config.include(Module.new do
|
125
|
+
def have_published_event(*args)
|
126
|
+
Downstream::HavePublishedEvent.new(*args)
|
127
|
+
end
|
128
|
+
end)
|
129
|
+
end
|
data/lib/downstream.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_model"
|
5
|
+
|
6
|
+
require "downstream/config"
|
7
|
+
require "downstream/event"
|
8
|
+
require "downstream/rspec" if defined?(RSpec)
|
9
|
+
|
10
|
+
module Downstream
|
11
|
+
class << self
|
12
|
+
def config
|
13
|
+
@config ||= Config.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def subscribe(subscriber = nil, to: nil, &block)
|
17
|
+
to ||= infer_event_from_subscriber(subscriber) if subscriber.is_a?(Module)
|
18
|
+
|
19
|
+
if to.nil?
|
20
|
+
raise ArgumentError, "Couldn't infer event from subscriber. " \
|
21
|
+
"Please, specify event using `to:` option"
|
22
|
+
end
|
23
|
+
|
24
|
+
subscriber ||= block if block
|
25
|
+
|
26
|
+
if subscriber.nil?
|
27
|
+
raise ArgumentError, "Subsriber must be present"
|
28
|
+
end
|
29
|
+
|
30
|
+
identifier =
|
31
|
+
if to.is_a?(Class) && Event >= to
|
32
|
+
to.identifier
|
33
|
+
else
|
34
|
+
to
|
35
|
+
end
|
36
|
+
|
37
|
+
ActiveSupport::Notifications.subscribe("#{config.namespace}.#{identifier}", subscriber)
|
38
|
+
end
|
39
|
+
|
40
|
+
# temporary subscriptions
|
41
|
+
def subscribed(subscriber, to: nil, &block)
|
42
|
+
to ||= infer_event_from_subscriber(subscriber) if subscriber.is_a?(Module)
|
43
|
+
|
44
|
+
if to.nil?
|
45
|
+
raise ArgumentError, "Couldn't infer event from subscriber. " \
|
46
|
+
"Please, specify event using `to:` option"
|
47
|
+
end
|
48
|
+
|
49
|
+
identifier =
|
50
|
+
if to.is_a?(Class) && Event >= to
|
51
|
+
to.identifier
|
52
|
+
else
|
53
|
+
to
|
54
|
+
end
|
55
|
+
|
56
|
+
ActiveSupport::Notifications.subscribed(subscriber, "#{config.namespace}.#{identifier}", &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def publish(event)
|
60
|
+
ActiveSupport::Notifications.publish("#{config.namespace}.#{event.type}", event)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def infer_event_from_subscriber(subscriber)
|
66
|
+
event_class_name = subscriber.name.split("::").yield_self do |parts|
|
67
|
+
# handle explicti top-level name, e.g. ::Some::Event
|
68
|
+
parts.shift if parts.first.empty?
|
69
|
+
# drop last part – it's a unique subscriber name
|
70
|
+
parts.pop
|
71
|
+
|
72
|
+
parts.last.sub!(/^On/, "")
|
73
|
+
|
74
|
+
parts.join("::")
|
75
|
+
end
|
76
|
+
|
77
|
+
event_class_name.safe_constantize
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
require "downstream/engine"
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: downstream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- merkushin.m.s@gmail.com
|
8
|
+
- dementiev.vm@gmail.com
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2021-10-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '5'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '5'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: appraisal
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '2.2'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '2.2'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '1.16'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '1.16'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: debug
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '1.3'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.3'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '13.0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '13.0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '3.0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: standard
|
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
|
+
description:
|
113
|
+
email:
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- LICENSE.txt
|
119
|
+
- README.md
|
120
|
+
- lib/downstream.rb
|
121
|
+
- lib/downstream/config.rb
|
122
|
+
- lib/downstream/engine.rb
|
123
|
+
- lib/downstream/event.rb
|
124
|
+
- lib/downstream/rspec.rb
|
125
|
+
- lib/downstream/rspec/have_published_event.rb
|
126
|
+
- lib/downstream/version.rb
|
127
|
+
homepage: https://github.com/bibendi/downstream
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata:
|
131
|
+
allowed_push_host: https://rubygems.org
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '2.5'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubygems_version: 3.1.2
|
148
|
+
signing_key:
|
149
|
+
specification_version: 4
|
150
|
+
summary: Straightforward way to implement communication between Rails Engines using
|
151
|
+
the Publish-Subscribe pattern
|
152
|
+
test_files: []
|