streamline 0.1.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/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/README.md +7 -0
- data/Rakefile +14 -0
- data/bin/console +11 -0
- data/lib/generators/streamline/stores/active_record_generator.rb +38 -0
- data/lib/generators/streamline/stores/templates/active_record/create_events.rb.tt +14 -0
- data/lib/generators/streamline/stores/templates/active_record/event.rb.tt +12 -0
- data/lib/streamline.rb +15 -0
- data/lib/streamline/configuration.rb +19 -0
- data/lib/streamline/handler.rb +13 -0
- data/lib/streamline/jobs.rb +3 -0
- data/lib/streamline/jobs/base_job.rb +7 -0
- data/lib/streamline/jobs/handle_event_job.rb +62 -0
- data/lib/streamline/jobs/track_event_job.rb +15 -0
- data/lib/streamline/registry.rb +32 -0
- data/lib/streamline/stores.rb +16 -0
- data/lib/streamline/stores/active_record_store.rb +34 -0
- data/lib/streamline/stores/base_store.rb +31 -0
- data/lib/streamline/target.rb +7 -0
- data/lib/streamline/tracker.rb +29 -0
- data/lib/streamline/util.rb +20 -0
- data/lib/streamline/version.rb +3 -0
- data/spec/lib/streamline/handler_spec.rb +16 -0
- data/spec/lib/streamline/jobs/handle_event_job_spec.rb +108 -0
- data/spec/lib/streamline/jobs/track_event_job_spec.rb +28 -0
- data/spec/lib/streamline/registry_spec.rb +25 -0
- data/spec/lib/streamline/stores/active_record_store_spec.rb +7 -0
- data/spec/lib/streamline/stores/base_store_spec.rb +14 -0
- data/spec/lib/streamline/target_spec.rb +14 -0
- data/spec/lib/streamline/tracker_spec.rb +12 -0
- data/spec/lib/streamline_spec.rb +21 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/event_context.rb +12 -0
- data/spec/support/handler_context.rb +5 -0
- data/spec/support/handlers.rb +7 -0
- data/spec/support/models.rb +27 -0
- data/spec/support/store_examples.rb +60 -0
- data/streamline.gemspec +31 -0
- metadata +295 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c06209f08a5c47cbef7e4e07932f563c3ff1e968
|
4
|
+
data.tar.gz: b3106d614435e118dad49658fb08007e7af1effa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f8f11ca2e4ba9cb215a3687e0dd5dbaf88aad2b7698a90f289998f13786e25aa3a215817da8d73fc4db13535cfd75afe367c435958740e48756a1e9a7649742b
|
7
|
+
data.tar.gz: fae0281c6a9f6a360884704829ee10bcefadb2ab2f3d0c2aeb352de579849b66cff1c2d89d415320643f29f794daa8d40d9f2dc6f43a87b9fe420e05bda570ff
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Airplane
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/airplane)
|
4
|
+
[](https://codeclimate.com/github/atipugin/airplane)
|
5
|
+
[](https://travis-ci.org/atipugin/airplane)
|
6
|
+
|
7
|
+
TODO: Write some meaningful description ;)
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
9
|
+
task.fail_on_error = false
|
10
|
+
task.patterns = %w({lib,spec}/**/*.rb)
|
11
|
+
task.requires << 'rubocop-rspec'
|
12
|
+
end
|
13
|
+
|
14
|
+
task default: %w(spec rubocop)
|
data/bin/console
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Generators
|
3
|
+
module Stores
|
4
|
+
class ActiveRecordGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
source_root File.expand_path('../templates/active_record', __FILE__)
|
8
|
+
|
9
|
+
def self.next_migration_number(*)
|
10
|
+
Time.now.utc.strftime('%Y%m%d%H%M%S')
|
11
|
+
end
|
12
|
+
|
13
|
+
def copy_migrations
|
14
|
+
migration_template 'create_events.rb',
|
15
|
+
'db/migrate/create_streamline_events.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy_models
|
19
|
+
template 'event.rb', 'app/models/streamline/event.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def jsonb?
|
23
|
+
return false unless postgresql?
|
24
|
+
|
25
|
+
ActiveRecord::Base
|
26
|
+
.connection
|
27
|
+
.execute('SELECT version();')[0]['version']
|
28
|
+
.start_with?('PostgreSQL 9.4')
|
29
|
+
end
|
30
|
+
|
31
|
+
def postgresql?
|
32
|
+
config = ActiveRecord::Base.configurations[Rails.env]
|
33
|
+
config && config['adapter'] == 'postgresql'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :streamline_events, id: false do |t|
|
4
|
+
t.uuid :id, primary_key: true
|
5
|
+
t.string :name, null: false
|
6
|
+
t.<%= postgresql? ? (jsonb? ? 'jsonb' : 'json') : 'text' %> :properties
|
7
|
+
t.datetime :occurred_at, null: false
|
8
|
+
t.references :target, polymorphic: true
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :streamline_events, :id
|
12
|
+
add_index :streamline_events, [:target_type, :target_id]
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Streamline
|
2
|
+
class Event < ActiveRecord::Base
|
3
|
+
self.table_name = 'streamline_events'<% unless postgresql? %>
|
4
|
+
|
5
|
+
serialize :properties<% end %>
|
6
|
+
|
7
|
+
belongs_to :target, polymorphic: true
|
8
|
+
|
9
|
+
validates :name, presence: true
|
10
|
+
validates :occurred_at, presence: true
|
11
|
+
end
|
12
|
+
end
|
data/lib/streamline.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_job'
|
2
|
+
require 'yaml'
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
4
|
+
|
5
|
+
require 'streamline/configuration'
|
6
|
+
require 'streamline/stores'
|
7
|
+
require 'streamline/jobs'
|
8
|
+
require 'streamline/handler'
|
9
|
+
require 'streamline/registry'
|
10
|
+
require 'streamline/tracker'
|
11
|
+
require 'streamline/util'
|
12
|
+
require 'streamline/target'
|
13
|
+
|
14
|
+
module Streamline
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Streamline
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :store
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@store = :active_record
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def configure
|
11
|
+
yield(configuration) if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def configuration
|
15
|
+
@configuration ||= Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
module_function :configure, :configuration
|
19
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Jobs
|
3
|
+
class HandleEventJob < BaseJob
|
4
|
+
def perform(params_dump)
|
5
|
+
params = YAML.load(params_dump)
|
6
|
+
handler = params[:handler]
|
7
|
+
event = Streamline.store.find_event(params[:event_id])
|
8
|
+
|
9
|
+
return unless conditions_satisfied?(handler, event)
|
10
|
+
|
11
|
+
handler[:klass].new.run(event)
|
12
|
+
enqueue_repeat(handler, params)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def conditions_satisfied?(handler, event)
|
18
|
+
subsequent_events =
|
19
|
+
apply_constraints(
|
20
|
+
handler,
|
21
|
+
event,
|
22
|
+
Streamline.store.find_subsequent_events(event)
|
23
|
+
)
|
24
|
+
|
25
|
+
if_satisfied?(handler, subsequent_events) &&
|
26
|
+
unless_satisfied?(handler, subsequent_events)
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_constraints(handler, event, subsequent_events)
|
30
|
+
return subsequent_events unless handler[:constraints]
|
31
|
+
|
32
|
+
constraints = Array(handler[:constraints]).map(&:to_s)
|
33
|
+
subsequent_events.select do |item|
|
34
|
+
constraints.all? do |constraint|
|
35
|
+
item['properties'][constraint] == event['properties'][constraint]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def if_satisfied?(handler, subsequent_events)
|
41
|
+
return true unless handler[:if]
|
42
|
+
subsequent_events.map { |e| e['name'] }.include?(handler[:if].to_s)
|
43
|
+
end
|
44
|
+
|
45
|
+
def unless_satisfied?(handler, subsequent_events)
|
46
|
+
return true unless handler[:unless]
|
47
|
+
!subsequent_events.map { |e| e['name'] }.include?(handler[:unless].to_s)
|
48
|
+
end
|
49
|
+
|
50
|
+
def enqueue_repeat(handler, params)
|
51
|
+
repeat = params[:repeat].to_i.next
|
52
|
+
|
53
|
+
return if repeat >= handler[:repeats]
|
54
|
+
|
55
|
+
self
|
56
|
+
.class
|
57
|
+
.set(wait: handler[:delay])
|
58
|
+
.perform_later(YAML.dump(params.merge(repeat: repeat)))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Jobs
|
3
|
+
class TrackEventJob < BaseJob
|
4
|
+
def perform(params_dump)
|
5
|
+
params = YAML.load(params_dump)
|
6
|
+
event_id = Streamline.store.save_event(params)
|
7
|
+
Streamline.registry[params[:name]].each do |handler|
|
8
|
+
HandleEventJob
|
9
|
+
.set(wait: handler[:delay])
|
10
|
+
.perform_later(YAML.dump(event_id: event_id, handler: handler))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Streamline
|
2
|
+
class Registry
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegators :@items, :count
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@items = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(klass, event_name, options = {})
|
12
|
+
return if self[event_name].map { |h| h[:klass] }.include?(klass)
|
13
|
+
self[event_name] << default_options.merge(options).merge(klass: klass)
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](event_name)
|
17
|
+
@items[event_name.to_s] ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def default_options
|
23
|
+
{ delay: 0, repeats: 1 }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def registry
|
28
|
+
@registry ||= Registry.new
|
29
|
+
end
|
30
|
+
|
31
|
+
module_function :registry
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'streamline/stores/base_store'
|
2
|
+
require 'streamline/stores/active_record_store' if defined?(ActiveRecord)
|
3
|
+
|
4
|
+
module Streamline
|
5
|
+
def store
|
6
|
+
@store ||= begin
|
7
|
+
klass = case configuration.store
|
8
|
+
when :active_record then Stores::ActiveRecordStore
|
9
|
+
end
|
10
|
+
|
11
|
+
klass.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module_function :store
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Stores
|
3
|
+
class ActiveRecordStore < BaseStore
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :model, :count
|
7
|
+
|
8
|
+
def save_event(attributes)
|
9
|
+
event = model.new(attributes)
|
10
|
+
event.id = generate_event_id
|
11
|
+
event.save!
|
12
|
+
|
13
|
+
event.id
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_event(id)
|
17
|
+
prepare_event(model.find(id).attributes)
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_subsequent_events(event)
|
21
|
+
model
|
22
|
+
.where('occurred_at > ?', event['occurred_at'])
|
23
|
+
.order('occurred_at ASC')
|
24
|
+
.map { |e| prepare_event(e.attributes) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def model
|
30
|
+
Streamline::Event
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Stores
|
3
|
+
class BaseStore
|
4
|
+
def save_event(*)
|
5
|
+
not_implemented!
|
6
|
+
end
|
7
|
+
|
8
|
+
def find_event(*)
|
9
|
+
not_implemented!
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_subsequent_events(*)
|
13
|
+
not_implemented!
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def not_implemented!
|
19
|
+
fail NotImplementedError, 'You need to implement this method first'
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_event_id
|
23
|
+
SecureRandom.uuid
|
24
|
+
end
|
25
|
+
|
26
|
+
def prepare_event(hsh)
|
27
|
+
hsh.deep_stringify_keys
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Streamline
|
2
|
+
class Tracker
|
3
|
+
def track_event(target, name, properties = {})
|
4
|
+
params =
|
5
|
+
{ name: name, properties: properties }
|
6
|
+
.merge(options)
|
7
|
+
.merge(extract_target_params(target))
|
8
|
+
|
9
|
+
Jobs::TrackEventJob.perform_later(YAML.dump(params))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def options
|
15
|
+
{ occurred_at: Time.zone.now }
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract_target_params(target)
|
19
|
+
{ target_type: Util.extract_object_type(target),
|
20
|
+
target_id: Util.extract_object_id(target) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def tracker
|
25
|
+
@tracker ||= Tracker.new
|
26
|
+
end
|
27
|
+
|
28
|
+
module_function :tracker
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Util
|
3
|
+
def extract_object_type(object)
|
4
|
+
case object
|
5
|
+
when Class then object.name
|
6
|
+
when NilClass then nil
|
7
|
+
else object.class.name
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def extract_object_id(object)
|
12
|
+
case object
|
13
|
+
when Class, NilClass then nil
|
14
|
+
else object.respond_to?(:id) ? object.id : object.hash
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module_function :extract_object_type, :extract_object_id
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Streamline
|
2
|
+
RSpec.describe Handler do
|
3
|
+
include_context 'handler'
|
4
|
+
|
5
|
+
it 'adds .handle method' do
|
6
|
+
expect(handler_class).to respond_to(:handle)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '.handle' do
|
10
|
+
it 'adds handler to the registry' do
|
11
|
+
expect(Streamline.registry).to receive(:add)
|
12
|
+
handler_class.handle(handler_event_name, handler_options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Jobs
|
3
|
+
RSpec.describe HandleEventJob do
|
4
|
+
include_context 'event'
|
5
|
+
include_context 'handler'
|
6
|
+
|
7
|
+
let(:handler) { Streamline.registry[event_name].sample }
|
8
|
+
let(:subsequent_events) { Streamline.store.find_subsequent_events(event) }
|
9
|
+
let(:params) { { event_id: event_id, handler: handler } }
|
10
|
+
|
11
|
+
before do
|
12
|
+
handler_class.handle event_name, handler_options
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#conditions_satisfied?' do
|
16
|
+
it 'returns true' do
|
17
|
+
expect(subject.send(:conditions_satisfied?, handler, event))
|
18
|
+
.to be(true)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#if_satisfied?' do
|
23
|
+
let(:handler_options) { { if: FFaker::Lorem.word } }
|
24
|
+
|
25
|
+
it 'returns false' do
|
26
|
+
expect(subject.send(:if_satisfied?, handler, subsequent_events))
|
27
|
+
.to be(false)
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when expected event is occurred' do
|
31
|
+
before do
|
32
|
+
Streamline.store.save_event(
|
33
|
+
event_attributes.merge(
|
34
|
+
name: handler_options[:if],
|
35
|
+
occurred_at: 1.minute.from_now
|
36
|
+
)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns true' do
|
41
|
+
expect(subject.send(:if_satisfied?, handler, subsequent_events))
|
42
|
+
.to be(true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#unless_satisfied?' do
|
48
|
+
let(:handler_options) { { unless: FFaker::Lorem.word } }
|
49
|
+
|
50
|
+
it 'returns true' do
|
51
|
+
expect(subject.send(:unless_satisfied?, handler, subsequent_events))
|
52
|
+
.to be(true)
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when unexpected event is occurred' do
|
56
|
+
before do
|
57
|
+
Streamline.store.save_event(
|
58
|
+
event_attributes.merge(
|
59
|
+
name: handler_options[:unless],
|
60
|
+
occurred_at: 1.minute.from_now
|
61
|
+
)
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'returns false' do
|
66
|
+
expect(subject.send(:unless_satisfied?, handler, subsequent_events))
|
67
|
+
.to be(false)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#apply_constraints' do
|
73
|
+
let(:event_properties) { { user_id: rand(1..10) } }
|
74
|
+
let(:handler_options) { { constraints: :user_id } }
|
75
|
+
|
76
|
+
before do
|
77
|
+
Streamline.store.save_event(
|
78
|
+
event_attributes.merge(occurred_at: 1.minute.from_now)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns suitable events' do
|
83
|
+
expect(
|
84
|
+
subject
|
85
|
+
.send(:apply_constraints, handler, event, subsequent_events)
|
86
|
+
.sample['properties']
|
87
|
+
).to include('user_id' => event_properties[:user_id])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#enqueue_repeat' do
|
92
|
+
it 'does not enqueue itself again' do
|
93
|
+
expect { subject.send(:enqueue_repeat, handler, params) }
|
94
|
+
.not_to enqueue_a(described_class)
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'when amount of handler repeats is greater than one' do
|
98
|
+
let(:handler_options) { { repeats: 2 } }
|
99
|
+
|
100
|
+
it 'enqueues itself again' do
|
101
|
+
expect { subject.send(:enqueue_repeat, handler, params) }
|
102
|
+
.to enqueue_a(described_class)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Jobs
|
3
|
+
RSpec.describe TrackEventJob do
|
4
|
+
include_context 'event'
|
5
|
+
include_context 'handler'
|
6
|
+
|
7
|
+
let(:params_dump) { YAML.dump(event_attributes) }
|
8
|
+
|
9
|
+
describe '#perform' do
|
10
|
+
it 'invokes store to save event' do
|
11
|
+
expect(Streamline.store).to receive(:save_event)
|
12
|
+
subject.perform(params_dump)
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when event has registered handlers' do
|
16
|
+
before do
|
17
|
+
handler_class.handle event_name
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'enqueues event handling' do
|
21
|
+
expect { subject.perform(params_dump) }
|
22
|
+
.to enqueue_a(Streamline::Jobs::HandleEventJob)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Streamline
|
2
|
+
RSpec.describe Registry do
|
3
|
+
include_context 'handler'
|
4
|
+
|
5
|
+
describe '#add' do
|
6
|
+
it 'adds an item' do
|
7
|
+
expect do
|
8
|
+
subject.add(handler_class, handler_event_name, handler_options)
|
9
|
+
end.to change(subject, :count).by(1)
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'when item is already added' do
|
13
|
+
before do
|
14
|
+
subject.add(handler_class, handler_event_name, handler_options)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'does not add it twice' do
|
18
|
+
expect do
|
19
|
+
subject.add(handler_class, handler_event_name, handler_options)
|
20
|
+
end.not_to change(subject, :count)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Streamline
|
2
|
+
module Stores
|
3
|
+
RSpec.describe BaseStore do
|
4
|
+
%w(save_event find_event find_subsequent_events).each do |method_name|
|
5
|
+
describe method_name do
|
6
|
+
it 'fails' do
|
7
|
+
expect { subject.send(method_name) }
|
8
|
+
.to raise_error(NotImplementedError)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Streamline
|
2
|
+
RSpec.describe Target do
|
3
|
+
include described_class
|
4
|
+
|
5
|
+
include_context 'event'
|
6
|
+
|
7
|
+
describe '#track_event' do
|
8
|
+
it 'invokes Tracker' do
|
9
|
+
expect(Streamline.tracker).to receive(:track_event)
|
10
|
+
track_event(event_name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Streamline
|
2
|
+
RSpec.describe Tracker do
|
3
|
+
include_context 'event'
|
4
|
+
|
5
|
+
describe '#track_event' do
|
6
|
+
it 'enqueues event tracking job' do
|
7
|
+
expect { subject.track_event(event_target, event_name) }
|
8
|
+
.to enqueue_a(Streamline::Jobs::TrackEventJob)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
RSpec.describe Streamline do
|
2
|
+
describe '.configure' do
|
3
|
+
let(:new_value) { FFaker::Lorem.word }
|
4
|
+
let!(:old_value) { described_class.configuration.store }
|
5
|
+
|
6
|
+
it 'allows to change config params' do
|
7
|
+
expect { described_class.configure { |c| c.store = new_value } }
|
8
|
+
.to change(described_class.configuration, :store).to(new_value)
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:each) do
|
12
|
+
described_class.configure { |c| c.store = old_value }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.tracker' do
|
17
|
+
it 'returns instance of Tracker' do
|
18
|
+
expect(described_class.tracker).to be_a(Streamline::Tracker)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'ffaker'
|
2
|
+
require 'rspec/active_job'
|
3
|
+
require 'simplecov'
|
4
|
+
require 'database_cleaner'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
require 'activeuuid'
|
8
|
+
|
9
|
+
SimpleCov.start
|
10
|
+
|
11
|
+
require 'streamline'
|
12
|
+
|
13
|
+
Dir[File.expand_path('../support/**/*.rb', __FILE__)].each { |f| require f }
|
14
|
+
|
15
|
+
ActiveJob::Base.queue_adapter = :test
|
16
|
+
Time.zone = 'UTC'
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.include RSpec::ActiveJob
|
20
|
+
|
21
|
+
config.before(:suite) do
|
22
|
+
DatabaseCleaner.strategy = :transaction
|
23
|
+
DatabaseCleaner.clean_with(:truncation)
|
24
|
+
end
|
25
|
+
|
26
|
+
config.around(:each) do |example|
|
27
|
+
DatabaseCleaner.cleaning do
|
28
|
+
example.run
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
RSpec.shared_context 'event' do
|
2
|
+
let(:event_name) { FFaker::Lorem.word }
|
3
|
+
let(:event_properties) { {} }
|
4
|
+
let(:event_target) { OpenStruct.new(id: rand(1..9)) }
|
5
|
+
let(:event_attributes) do
|
6
|
+
{ name: event_name,
|
7
|
+
properties: event_properties,
|
8
|
+
occurred_at: Time.zone.now }
|
9
|
+
end
|
10
|
+
let(:event_id) { Streamline.store.save_event(event_attributes) }
|
11
|
+
let(:event) { Streamline.store.find_event(event_id) }
|
12
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define do
|
4
|
+
create_table :streamline_events, id: false do |t|
|
5
|
+
t.uuid :id, primary_key: true
|
6
|
+
t.string :name, null: false
|
7
|
+
t.text :properties
|
8
|
+
t.datetime :occurred_at, null: false
|
9
|
+
t.references :target, polymorphic: true
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index :streamline_events, :id
|
13
|
+
add_index :streamline_events, [:target_type, :target_id]
|
14
|
+
end
|
15
|
+
|
16
|
+
module Streamline
|
17
|
+
class Event < ActiveRecord::Base
|
18
|
+
self.table_name = 'streamline_events'
|
19
|
+
|
20
|
+
serialize :properties
|
21
|
+
|
22
|
+
belongs_to :target, polymorphic: true
|
23
|
+
|
24
|
+
validates :name, presence: true
|
25
|
+
validates :occurred_at, presence: true
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
RSpec.shared_examples_for 'a store' do
|
2
|
+
include_context 'event'
|
3
|
+
|
4
|
+
describe '#save_event' do
|
5
|
+
it 'saves event to the store' do
|
6
|
+
expect { subject.save_event(event_attributes) }
|
7
|
+
.to change(subject, :count).by(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'returns id of saved event' do
|
11
|
+
expect(subject.save_event(event_attributes)).to be_a(String)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#find_event' do
|
16
|
+
it 'returns saved event as a hash' do
|
17
|
+
expect(subject.find_event(event_id)).to be_a(Hash)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns valid name' do
|
21
|
+
expect(subject.find_event(event_id)['name']).to eq(event_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns valid properties' do
|
25
|
+
expect(subject.find_event(event_id)['properties'])
|
26
|
+
.to eq(event_attributes[:properties])
|
27
|
+
end
|
28
|
+
|
29
|
+
# `be_within` matcher is used here because of issue with travis ci
|
30
|
+
it 'returns valid time of occurrance' do
|
31
|
+
expect(subject.find_event(event_id)['occurred_at'])
|
32
|
+
.to be_within(1.second).of(event_attributes[:occurred_at])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns hash with string keys' do
|
36
|
+
expect(subject.find_event(event_id).keys.sample).to be_a(String)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#find_subsequent_events' do
|
41
|
+
before do
|
42
|
+
Streamline.store.save_event(
|
43
|
+
event_attributes.merge(occurred_at: 1.minute.from_now)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns an array' do
|
48
|
+
expect(subject.find_subsequent_events(event)).to be_a(Array)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'returns items as hashes' do
|
52
|
+
expect(subject.find_subsequent_events(event).sample).to be_a(Hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns events in ascending order' do
|
56
|
+
events = subject.find_subsequent_events(event)
|
57
|
+
expect(events.first['occurred_at']).to be <= events.last['occurred_at']
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/streamline.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'streamline/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'streamline'
|
8
|
+
spec.version = Streamline::VERSION
|
9
|
+
spec.summary = '...'
|
10
|
+
spec.authors = ['Alexander Tipugin']
|
11
|
+
|
12
|
+
spec.files = `git ls-files -z`.split("\x0")
|
13
|
+
spec.test_files = spec.files.grep(%r{^spec\/})
|
14
|
+
spec.require_paths = %w(lib)
|
15
|
+
|
16
|
+
spec.add_dependency 'activejob'
|
17
|
+
spec.add_dependency 'activesupport'
|
18
|
+
|
19
|
+
spec.add_development_dependency 'activerecord'
|
20
|
+
spec.add_development_dependency 'activeuuid'
|
21
|
+
spec.add_development_dependency 'database_cleaner'
|
22
|
+
spec.add_development_dependency 'ffaker'
|
23
|
+
spec.add_development_dependency 'pry'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'rspec-activejob'
|
27
|
+
spec.add_development_dependency 'rubocop'
|
28
|
+
spec.add_development_dependency 'rubocop-rspec'
|
29
|
+
spec.add_development_dependency 'simplecov'
|
30
|
+
spec.add_development_dependency 'sqlite3'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: streamline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexander Tipugin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activeuuid
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
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: ffaker
|
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: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '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'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
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
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rspec-activejob
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rubocop-rspec
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: simplecov
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: sqlite3
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
description:
|
210
|
+
email:
|
211
|
+
executables: []
|
212
|
+
extensions: []
|
213
|
+
extra_rdoc_files: []
|
214
|
+
files:
|
215
|
+
- ".gitignore"
|
216
|
+
- ".rspec"
|
217
|
+
- ".rubocop.yml"
|
218
|
+
- ".travis.yml"
|
219
|
+
- Gemfile
|
220
|
+
- README.md
|
221
|
+
- Rakefile
|
222
|
+
- bin/console
|
223
|
+
- lib/generators/streamline/stores/active_record_generator.rb
|
224
|
+
- lib/generators/streamline/stores/templates/active_record/create_events.rb.tt
|
225
|
+
- lib/generators/streamline/stores/templates/active_record/event.rb.tt
|
226
|
+
- lib/streamline.rb
|
227
|
+
- lib/streamline/configuration.rb
|
228
|
+
- lib/streamline/handler.rb
|
229
|
+
- lib/streamline/jobs.rb
|
230
|
+
- lib/streamline/jobs/base_job.rb
|
231
|
+
- lib/streamline/jobs/handle_event_job.rb
|
232
|
+
- lib/streamline/jobs/track_event_job.rb
|
233
|
+
- lib/streamline/registry.rb
|
234
|
+
- lib/streamline/stores.rb
|
235
|
+
- lib/streamline/stores/active_record_store.rb
|
236
|
+
- lib/streamline/stores/base_store.rb
|
237
|
+
- lib/streamline/target.rb
|
238
|
+
- lib/streamline/tracker.rb
|
239
|
+
- lib/streamline/util.rb
|
240
|
+
- lib/streamline/version.rb
|
241
|
+
- spec/lib/streamline/handler_spec.rb
|
242
|
+
- spec/lib/streamline/jobs/handle_event_job_spec.rb
|
243
|
+
- spec/lib/streamline/jobs/track_event_job_spec.rb
|
244
|
+
- spec/lib/streamline/registry_spec.rb
|
245
|
+
- spec/lib/streamline/stores/active_record_store_spec.rb
|
246
|
+
- spec/lib/streamline/stores/base_store_spec.rb
|
247
|
+
- spec/lib/streamline/target_spec.rb
|
248
|
+
- spec/lib/streamline/tracker_spec.rb
|
249
|
+
- spec/lib/streamline_spec.rb
|
250
|
+
- spec/spec_helper.rb
|
251
|
+
- spec/support/event_context.rb
|
252
|
+
- spec/support/handler_context.rb
|
253
|
+
- spec/support/handlers.rb
|
254
|
+
- spec/support/models.rb
|
255
|
+
- spec/support/store_examples.rb
|
256
|
+
- streamline.gemspec
|
257
|
+
homepage:
|
258
|
+
licenses: []
|
259
|
+
metadata: {}
|
260
|
+
post_install_message:
|
261
|
+
rdoc_options: []
|
262
|
+
require_paths:
|
263
|
+
- lib
|
264
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
265
|
+
requirements:
|
266
|
+
- - ">="
|
267
|
+
- !ruby/object:Gem::Version
|
268
|
+
version: '0'
|
269
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
270
|
+
requirements:
|
271
|
+
- - ">="
|
272
|
+
- !ruby/object:Gem::Version
|
273
|
+
version: '0'
|
274
|
+
requirements: []
|
275
|
+
rubyforge_project:
|
276
|
+
rubygems_version: 2.4.5.1
|
277
|
+
signing_key:
|
278
|
+
specification_version: 4
|
279
|
+
summary: "..."
|
280
|
+
test_files:
|
281
|
+
- spec/lib/streamline/handler_spec.rb
|
282
|
+
- spec/lib/streamline/jobs/handle_event_job_spec.rb
|
283
|
+
- spec/lib/streamline/jobs/track_event_job_spec.rb
|
284
|
+
- spec/lib/streamline/registry_spec.rb
|
285
|
+
- spec/lib/streamline/stores/active_record_store_spec.rb
|
286
|
+
- spec/lib/streamline/stores/base_store_spec.rb
|
287
|
+
- spec/lib/streamline/target_spec.rb
|
288
|
+
- spec/lib/streamline/tracker_spec.rb
|
289
|
+
- spec/lib/streamline_spec.rb
|
290
|
+
- spec/spec_helper.rb
|
291
|
+
- spec/support/event_context.rb
|
292
|
+
- spec/support/handler_context.rb
|
293
|
+
- spec/support/handlers.rb
|
294
|
+
- spec/support/models.rb
|
295
|
+
- spec/support/store_examples.rb
|