with_events 0.1.12
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/MIT-LICENSE +20 -0
- data/README.md +279 -0
- data/Rakefile +29 -0
- data/lib/tasks/with_events/with_events_tasks.rake +13 -0
- data/lib/with_events.rb +18 -0
- data/lib/with_events/aws/message.rb +33 -0
- data/lib/with_events/aws/publisher.rb +38 -0
- data/lib/with_events/aws/topic.rb +59 -0
- data/lib/with_events/event.rb +58 -0
- data/lib/with_events/invoker.rb +33 -0
- data/lib/with_events/stream.rb +101 -0
- data/lib/with_events/trigger.rb +22 -0
- data/lib/with_events/validator.rb +30 -0
- data/lib/with_events/version.rb +5 -0
- data/lib/with_events/worker.rb +58 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3091e63784daa01d3274a7671cd51fb5be39d20
|
4
|
+
data.tar.gz: c107a1982c4443e0592bfd92167e165ca2d19063
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8df5a9dca0608230736415d0bfc03065f241c6efd31b85735cd89d30ea7e16500cb869cb1dfea3810321708abd9298908947338f0c9c817c44ae1938b1228c3a
|
7
|
+
data.tar.gz: d36c94f875c2208ffadfd163a731285b5b24d798610230e950584923b14a599771393a4b7cb35af5109674c15898ee101643a048ec7a9a786af97a3b553fdb5e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Vlad Gramuzov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+

|
2
|
+
|
3
|
+
# WithEvents
|
4
|
+
A simple events system for Ruby apps which supports
|
5
|
+
bi-directional SNS/SQS messaging.
|
6
|
+
|
7
|
+
## Dependencies
|
8
|
+
* Ruby >= 2.3.3
|
9
|
+
* Rake >= 12.3.1
|
10
|
+
* Activesupport >= 4.2.7
|
11
|
+
* Sidekiq >= 3.5.3
|
12
|
+
* Circuitry 3.2
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'with_events'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
```bash
|
23
|
+
$ bundle
|
24
|
+
```
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
```bash
|
28
|
+
$ gem install with_events
|
29
|
+
```
|
30
|
+
|
31
|
+
## Configuration
|
32
|
+
|
33
|
+
### Setting up a Rakefile
|
34
|
+
|
35
|
+
If you are going to use included rake tasks, add this to your
|
36
|
+
Rakefile:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
spec = Gem::Specification.find_by_name 'with_events'
|
40
|
+
load "#{spec.gem_dir}/lib/tasks/with_events/with_events_tasks.rake"
|
41
|
+
```
|
42
|
+
|
43
|
+
### Setting up Circuitry
|
44
|
+
|
45
|
+
If you would like to use SNS/SQS subscribing/publishing features,
|
46
|
+
you need to configure Circuitry gem. Just follow [this instructions](https://github.com/kapost/circuitry#usage).
|
47
|
+
|
48
|
+
## Usage
|
49
|
+
|
50
|
+
### Basic Usage (in-app publish/subscribe)
|
51
|
+
This type of messaging does not require Rakefile or Circuitry configuration.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
require 'with_events'
|
55
|
+
|
56
|
+
class MyHeroClass
|
57
|
+
include WithEvents
|
58
|
+
|
59
|
+
stream :my_lovely_stream do
|
60
|
+
event :game_over,
|
61
|
+
condition: :really_game_over?,
|
62
|
+
callback: :call_me_if_game_over
|
63
|
+
end
|
64
|
+
|
65
|
+
def really_game_over?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def call_me_if_game_over
|
70
|
+
puts 'Game over'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
hero = MyHeroClass.new
|
75
|
+
hero.game_over! if hero.game_over?
|
76
|
+
```
|
77
|
+
|
78
|
+
There might be situations where you will have a lot of events
|
79
|
+
which have pretty same configuration. To make life easier, you
|
80
|
+
can use `configure_all` method, which will aply configuration for
|
81
|
+
all events in the stream.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
require 'with_events'
|
85
|
+
|
86
|
+
class MyHeroClass
|
87
|
+
include WithEvents
|
88
|
+
|
89
|
+
stream :my_lovely_stream do
|
90
|
+
configure_all callback: :call_me_if_game_over
|
91
|
+
|
92
|
+
event :event_one,
|
93
|
+
condition: -> { true }
|
94
|
+
|
95
|
+
event :event_one,
|
96
|
+
condition: -> { false }
|
97
|
+
end
|
98
|
+
|
99
|
+
def really_game_over?
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def call_me_if_game_over
|
104
|
+
puts 'Game over'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
hero = MyHeroClass.new
|
109
|
+
hero.event_one!
|
110
|
+
#=> Game over
|
111
|
+
hero.event_two!
|
112
|
+
#=> Game over
|
113
|
+
```
|
114
|
+
|
115
|
+
### Using with daily/hourly rake triggers for batch processing
|
116
|
+
|
117
|
+
You may want to automate a bit the process of asking resources if
|
118
|
+
they are ready to trigger events (by calling `#*?`). This can
|
119
|
+
be easily done by using `background: true`
|
120
|
+
with `appearance: :daily # or hourly` options.
|
121
|
+
|
122
|
+
`appearance` option sets by which rake task your event may
|
123
|
+
be processed.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
require 'with_events'
|
127
|
+
|
128
|
+
class MyHeroClass
|
129
|
+
include WithEvents
|
130
|
+
|
131
|
+
stream :my_lovely_stream do
|
132
|
+
event :game_over,
|
133
|
+
condition: :really_game_over?,
|
134
|
+
callback: :call_me_if_game_over,
|
135
|
+
background: true,
|
136
|
+
appearance: :daily, # or :hourly
|
137
|
+
batch: User.active.find_each # any Enumerable
|
138
|
+
end
|
139
|
+
|
140
|
+
def really_game_over?
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
def call_me_if_game_over
|
145
|
+
puts 'Game over'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
Schedule for hourly/daily execution
|
150
|
+
```bash
|
151
|
+
$ rake with_events:daily
|
152
|
+
$ rake with_events:hourly
|
153
|
+
```
|
154
|
+
|
155
|
+
### "Third-party" subscriptions
|
156
|
+
|
157
|
+
It is also possible to subscribe to events not only by using
|
158
|
+
a `callback` option:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
WithEvents::Stream.find(:my_lovely_stream).on(:game_over) do
|
162
|
+
# ...
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
**NOTE that this will also subscribe you to SQS/SNS events.**
|
167
|
+
|
168
|
+
### Sending events to SNS/SQS
|
169
|
+
|
170
|
+
You may send messages to SNS/SQS by setting a `topic` option for
|
171
|
+
the stream. In addition, you need to specify `identifier` option.
|
172
|
+
|
173
|
+
`identifier` option (symbol, Proc, Class) allows to identify incoming message and
|
174
|
+
bind an `id` for outgoing ones.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
require 'with_events'
|
178
|
+
|
179
|
+
class MyModel < ActiveRecord::Base
|
180
|
+
include WithEvents
|
181
|
+
|
182
|
+
stream :my_lovely_stream, topic: 'my-topic' do
|
183
|
+
event :game_over,
|
184
|
+
condition: :really_game_over?,
|
185
|
+
callback: :call_me_if_game_over,
|
186
|
+
identifier: :id, # symbol, Proc or Class
|
187
|
+
end
|
188
|
+
|
189
|
+
def really_game_over?
|
190
|
+
true
|
191
|
+
end
|
192
|
+
|
193
|
+
def call_me_if_game_over
|
194
|
+
puts 'Game over'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
```
|
198
|
+
|
199
|
+
### Subscribing to SNS/SQS events
|
200
|
+
|
201
|
+
To subscribe to SNS/SQS events you need to specify `topic` and
|
202
|
+
`finder` options.
|
203
|
+
|
204
|
+
The `finder` option represents invokable type which should return
|
205
|
+
resource identified by `identifier` invokable by the sender.
|
206
|
+
|
207
|
+
**NOTE that subscriber will take the process. Run it in a separate process**
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
require 'with_events'
|
211
|
+
|
212
|
+
class MyClass
|
213
|
+
include WithEvents
|
214
|
+
|
215
|
+
stream :my_lovely_stream, topic: 'my-topic' do
|
216
|
+
event :game_over,
|
217
|
+
condition: :really_game_over?,
|
218
|
+
callback: :call_me_if_game_over,
|
219
|
+
finder: ->(message) { SomeModel.find(message.id) }
|
220
|
+
end
|
221
|
+
|
222
|
+
def really_game_over?
|
223
|
+
true
|
224
|
+
end
|
225
|
+
|
226
|
+
def call_me_if_game_over
|
227
|
+
puts 'Game over'
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
WithEvents::Stream.subscribe # NOTE this line
|
232
|
+
```
|
233
|
+
|
234
|
+
### Supported invokable types
|
235
|
+
* Proc
|
236
|
+
* Symbol
|
237
|
+
* Class
|
238
|
+
|
239
|
+
You may use them for `condition`, `callback`, `identifier` or `finder` options.
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
class CallbackClass
|
243
|
+
def call(resource, *arguments)
|
244
|
+
puts 'Game over'
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
class MyHeroClass
|
249
|
+
include WithEvents
|
250
|
+
|
251
|
+
stream :my_lovely_stream do
|
252
|
+
event :game_over,
|
253
|
+
condition: :really_game_over?,
|
254
|
+
callback: CallbackClass
|
255
|
+
|
256
|
+
event :you_won,
|
257
|
+
condition: -> { really_won? },
|
258
|
+
callback: CallbackClass
|
259
|
+
end
|
260
|
+
|
261
|
+
def really_won?
|
262
|
+
false
|
263
|
+
end
|
264
|
+
|
265
|
+
def really_game_over?
|
266
|
+
true
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
```
|
271
|
+
|
272
|
+
## Contributing
|
273
|
+
If you are going to contribute to this repo, please follow these simple rules:
|
274
|
+
* Cover you wrote with specs
|
275
|
+
* Check you wrote with Rubocop
|
276
|
+
* Use Karma-style comit messages
|
277
|
+
|
278
|
+
## License
|
279
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'RailsEvents'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'bundler/gem_tasks'
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
|
23
|
+
Rake::TestTask.new(:test) do |t|
|
24
|
+
t.libs << 'test'
|
25
|
+
t.pattern = 'test/**/*_test.rb'
|
26
|
+
t.verbose = false
|
27
|
+
end
|
28
|
+
|
29
|
+
task default: :test
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :with_events do
|
4
|
+
desc 'Run daily tasks'
|
5
|
+
task daily: :environment do
|
6
|
+
WithEvents::Trigger.new.call(WithEvents::Trigger::DAILY_APPEARANCE)
|
7
|
+
end
|
8
|
+
|
9
|
+
desc 'Run hourly tasks'
|
10
|
+
task hourly: :environment do
|
11
|
+
WithEvents::Trigger.new.call(WithEvents::Trigger::HOURLY_APPEARANCE)
|
12
|
+
end
|
13
|
+
end
|
data/lib/with_events.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'require_all'
|
5
|
+
|
6
|
+
module WithEvents
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
autoload_all __dir__ + '/with_events'
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def stream(name, options = {}, &block)
|
13
|
+
Stream.find_or_initialize(name, self, options)
|
14
|
+
.reset_configure_all
|
15
|
+
.instance_exec(&block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
|
6
|
+
module WithEvents
|
7
|
+
module Aws
|
8
|
+
class Message
|
9
|
+
attr_reader :event, :stream, :identifier
|
10
|
+
|
11
|
+
alias id identifier
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@options = options.with_indifferent_access
|
15
|
+
@event = @options[:event]
|
16
|
+
@stream = @options[:stream]
|
17
|
+
@identifier = @options[:identifier]
|
18
|
+
end
|
19
|
+
|
20
|
+
def serialize
|
21
|
+
options.deep_transform_keys { |key| key.to_s.camelize(:lower) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_sqs(options = {})
|
25
|
+
new(options.deep_transform_keys { |key| key.to_s.underscore })
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :options
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
|
5
|
+
module WithEvents
|
6
|
+
module Aws
|
7
|
+
class Publisher
|
8
|
+
def initialize(event, resource)
|
9
|
+
@event = event
|
10
|
+
@resource = resource
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish
|
14
|
+
return unless event.identifier && identifier
|
15
|
+
topic.publish(message)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :event, :resource
|
21
|
+
delegate :stream, to: :event
|
22
|
+
|
23
|
+
def identifier
|
24
|
+
@identifier ||= Invoker.new(event.identifier).invoke(resource)
|
25
|
+
end
|
26
|
+
|
27
|
+
def topic
|
28
|
+
@topic ||= Topic.new(stream.topic)
|
29
|
+
end
|
30
|
+
|
31
|
+
def message
|
32
|
+
@message ||= Message.new(event: event.name,
|
33
|
+
stream: stream.name,
|
34
|
+
identifier: identifier)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'circuitry'
|
4
|
+
|
5
|
+
module Circuitry
|
6
|
+
Subscriber.prepend(
|
7
|
+
Module.new do
|
8
|
+
def handle_message_with_middleware(message, &block)
|
9
|
+
middleware.invoke(message.topic.name, message.body) do
|
10
|
+
handle_with_skip_delete(message, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_with_skip_delete(message, &block)
|
15
|
+
catch :skip_delete do
|
16
|
+
handle_message(message, &block)
|
17
|
+
delete_message(message)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
module WithEvents
|
25
|
+
module Aws
|
26
|
+
class Topic
|
27
|
+
def initialize(topic = nil)
|
28
|
+
@topic = topic
|
29
|
+
end
|
30
|
+
|
31
|
+
def publish(message)
|
32
|
+
Circuitry.publish(topic, message.serialize)
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscribe(options = {}, &block)
|
36
|
+
Circuitry.subscribe(options) do |message, topic_name|
|
37
|
+
skip_delete unless positive_result?(topic_name, message, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :topic
|
44
|
+
|
45
|
+
def positive_result?(topic_name, message)
|
46
|
+
message = Message.from_sqs(message)
|
47
|
+
valid_message?(message) && yield(message, topic_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid_message?(message)
|
51
|
+
message.stream && message.event && message.identifier
|
52
|
+
end
|
53
|
+
|
54
|
+
def skip_delete
|
55
|
+
throw :skip_delete
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithEvents
|
4
|
+
class Event
|
5
|
+
attr_reader :name, :identifier, :options, :callback,
|
6
|
+
:condition, :stream, :finder
|
7
|
+
|
8
|
+
##
|
9
|
+
# ==Options:
|
10
|
+
#
|
11
|
+
# +name+ - event name
|
12
|
+
# +klass+ - resource class name
|
13
|
+
# +options[:condition]+ - condition to check whether event can be triggered
|
14
|
+
# +options[:callback]+ - callback to invoke on event
|
15
|
+
# +options[:stream]+ - stream object event belongs to
|
16
|
+
# +options[:identifier]+ - resource identifier (symbol, Proc or Class)
|
17
|
+
# +options[:finder]+ - resource finder (symbol, Proc or Class)
|
18
|
+
# +options[:subscribe]+ - subscribe to SQS queue
|
19
|
+
def initialize(name, klass, options = {})
|
20
|
+
@name = name
|
21
|
+
@klass = klass
|
22
|
+
@options = options
|
23
|
+
@condition = options[:condition]
|
24
|
+
@callback = options[:callback]
|
25
|
+
@stream = options[:stream]
|
26
|
+
@identifier = options[:identifier]
|
27
|
+
@finder = options[:finder]
|
28
|
+
|
29
|
+
define_condition
|
30
|
+
define_callback
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :klass
|
36
|
+
|
37
|
+
def define_condition
|
38
|
+
return unless condition
|
39
|
+
|
40
|
+
klass.instance_exec(self) do |event|
|
41
|
+
define_method("#{event.name}?") do
|
42
|
+
return false unless event.condition
|
43
|
+
Invoker.new(event.condition).invoke(self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def define_callback
|
49
|
+
klass.instance_exec(self) do |event|
|
50
|
+
define_method("#{event.name}!") do
|
51
|
+
event.stream.notify(event, self)
|
52
|
+
return if event.stream.subscribe || !event.callback
|
53
|
+
Invoker.new(event.callback).invoke(self)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithEvents
|
4
|
+
class Invoker
|
5
|
+
def initialize(callable)
|
6
|
+
@callable = callable
|
7
|
+
end
|
8
|
+
|
9
|
+
def invoke(context, *args)
|
10
|
+
return context.instance_exec(*args, &callable) if proc?
|
11
|
+
return callable.new.call(context, *args) if class?
|
12
|
+
return context.public_send(callable, *args) if symbol?(context)
|
13
|
+
|
14
|
+
raise NotImplementedError, 'Argument can not be invoked'
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :callable
|
20
|
+
|
21
|
+
def proc?
|
22
|
+
callable.is_a?(Proc)
|
23
|
+
end
|
24
|
+
|
25
|
+
def class?
|
26
|
+
callable.is_a?(Class) && callable.instance_methods.include?(:call)
|
27
|
+
end
|
28
|
+
|
29
|
+
def symbol?(context)
|
30
|
+
callable.is_a?(Symbol) && context.respond_to?(callable)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithEvents
|
4
|
+
class Stream
|
5
|
+
attr_reader :name, :klass, :events, :watchers, :topic, :subscribe
|
6
|
+
|
7
|
+
def initialize(name, klass, options = {})
|
8
|
+
@name = name
|
9
|
+
@klass = klass
|
10
|
+
@events = []
|
11
|
+
@watchers = {}
|
12
|
+
@topic = options[:topic]
|
13
|
+
@configuration = {}
|
14
|
+
@subscribe = options[:subscribe]
|
15
|
+
|
16
|
+
self.class.streams << self
|
17
|
+
end
|
18
|
+
|
19
|
+
def event(name, options = {})
|
20
|
+
events <<
|
21
|
+
Event.new(name, klass, options.merge(configuration).merge(stream: self))
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset_configure_all
|
25
|
+
@configuration = {}
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def configure_all(options = {})
|
30
|
+
@configuration = options
|
31
|
+
end
|
32
|
+
|
33
|
+
def on(name, &block)
|
34
|
+
watchers[name] ||= []
|
35
|
+
watchers[name] << block
|
36
|
+
end
|
37
|
+
|
38
|
+
def notify(event, resource)
|
39
|
+
notify_sqs(event, resource) if topic
|
40
|
+
notify_watchers(event, resource)
|
41
|
+
end
|
42
|
+
|
43
|
+
def notify_watchers(event, resource)
|
44
|
+
return if watchers[event.name].nil?
|
45
|
+
watchers[event.name].each { |watcher| resource.instance_exec(&watcher) }
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
attr_accessor :subscribed
|
50
|
+
|
51
|
+
def streams
|
52
|
+
@streams ||= []
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_or_initialize(name, klass, options = {})
|
56
|
+
find(name) || new(name, klass, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def find(name)
|
60
|
+
streams.find { |s| s.name == name }
|
61
|
+
end
|
62
|
+
|
63
|
+
def subscribe
|
64
|
+
return if subscribed || !streams.find { |s| s.topic && s.subscribe }
|
65
|
+
self.subscribed = true
|
66
|
+
|
67
|
+
Aws::Topic.new.subscribe(async: true, timeout: 0) do |message, topic|
|
68
|
+
selected = stream_events(message, topic)
|
69
|
+
selected.each { |event| notify_event(event, message) }.size.positive?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def stream_events(message, topic_name)
|
76
|
+
stream = find(message.stream.to_sym)
|
77
|
+
return [] unless stream&.subscribe && stream&.topic&.to_s == topic_name
|
78
|
+
stream.events.select { |event| valid_event?(event, message) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def valid_event?(event, message)
|
82
|
+
event.finder && event.callback && message.event.to_sym == event.name
|
83
|
+
end
|
84
|
+
|
85
|
+
def notify_event(event, message)
|
86
|
+
context = Invoker.new(event.finder)
|
87
|
+
.invoke(TOPLEVEL_BINDING.eval('self'), message)
|
88
|
+
event.stream.notify_watchers(event, context)
|
89
|
+
Invoker.new(event.callback).invoke(context)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :configuration
|
96
|
+
|
97
|
+
def notify_sqs(event, resource)
|
98
|
+
Aws::Publisher.new(event, resource).publish
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithEvents
|
4
|
+
class Trigger
|
5
|
+
include WithEvents::Validator
|
6
|
+
|
7
|
+
def call(appearance)
|
8
|
+
Stream.streams.each do |stream|
|
9
|
+
process_stream(stream, appearance.to_sym)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def process_stream(stream, appearance)
|
16
|
+
stream.events.each do |event|
|
17
|
+
next unless valid_event?(event, appearance)
|
18
|
+
WithEvents::Worker.perform_async(stream.name, event.name, appearance)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithEvents
|
4
|
+
module Validator
|
5
|
+
HOURLY_APPEARANCE = :hourly
|
6
|
+
DAILY_APPEARANCE = :daily
|
7
|
+
|
8
|
+
def valid_event?(event, appearance)
|
9
|
+
background_event?(event) &&
|
10
|
+
valid_appearance?(event, appearance) &&
|
11
|
+
valid_batch?(event)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def valid_batch?(event)
|
17
|
+
event.options[:batch].is_a?(Proc)
|
18
|
+
end
|
19
|
+
|
20
|
+
def background_event?(event)
|
21
|
+
event.options[:background]
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_appearance?(event, appearance)
|
25
|
+
[HOURLY_APPEARANCE, DAILY_APPEARANCE]
|
26
|
+
.include?(event.options[:appearance]) &&
|
27
|
+
event.options[:appearance] == appearance.to_sym
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
|
5
|
+
module WithEvents
|
6
|
+
class Worker
|
7
|
+
include WithEvents::Validator
|
8
|
+
include Sidekiq::Worker
|
9
|
+
|
10
|
+
sidekiq_options retry: false
|
11
|
+
|
12
|
+
def perform(stream, event_name, appearance)
|
13
|
+
events(stream, event_name, appearance).each do |event|
|
14
|
+
event.options[:batch].call.each do |resource|
|
15
|
+
call(event, resource) if may_call?(event, resource)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
reraise_last_exception
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# rubocop:disable Lint/RescueWithoutErrorClass
|
25
|
+
def call(event, resource)
|
26
|
+
resource.public_send("#{event.name}!")
|
27
|
+
rescue => e
|
28
|
+
exceptions << e
|
29
|
+
end
|
30
|
+
|
31
|
+
def may_call?(event, resource)
|
32
|
+
resource.public_send("#{event.name}?")
|
33
|
+
rescue => e
|
34
|
+
exceptions << e
|
35
|
+
end
|
36
|
+
# rubocop:enable Lint/RescueWithoutErrorClass
|
37
|
+
|
38
|
+
def stream(stream)
|
39
|
+
@stream ||= Stream.find(stream.to_sym)
|
40
|
+
end
|
41
|
+
|
42
|
+
def events(stream, name, appearance)
|
43
|
+
return [] unless stream(stream)
|
44
|
+
|
45
|
+
@events ||= stream(stream).events.select do |event|
|
46
|
+
event.name == name.to_sym && valid_event?(event, appearance)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def exceptions
|
51
|
+
@exceptions ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def reraise_last_exception
|
55
|
+
raise exceptions.last if exceptions.size.positive?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: with_events
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.12
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vlad Gramuzov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.7
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.7
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: circuitry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: require_all
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.4.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.4.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sidekiq
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.5.3
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.5.3
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.51.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.51.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: A simple events system for Ruby apps.
|
126
|
+
email:
|
127
|
+
- vlad.gramuzov@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- MIT-LICENSE
|
133
|
+
- README.md
|
134
|
+
- Rakefile
|
135
|
+
- lib/tasks/with_events/with_events_tasks.rake
|
136
|
+
- lib/with_events.rb
|
137
|
+
- lib/with_events/aws/message.rb
|
138
|
+
- lib/with_events/aws/publisher.rb
|
139
|
+
- lib/with_events/aws/topic.rb
|
140
|
+
- lib/with_events/event.rb
|
141
|
+
- lib/with_events/invoker.rb
|
142
|
+
- lib/with_events/stream.rb
|
143
|
+
- lib/with_events/trigger.rb
|
144
|
+
- lib/with_events/validator.rb
|
145
|
+
- lib/with_events/version.rb
|
146
|
+
- lib/with_events/worker.rb
|
147
|
+
homepage: https://github.com/pandomic/with_events
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
metadata: {}
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
requirements: []
|
166
|
+
rubyforge_project:
|
167
|
+
rubygems_version: 2.6.14
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: A simple events system for Ruby apps.
|
171
|
+
test_files: []
|