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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +5 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +3 -0
  7. data/README.md +7 -0
  8. data/Rakefile +14 -0
  9. data/bin/console +11 -0
  10. data/lib/generators/streamline/stores/active_record_generator.rb +38 -0
  11. data/lib/generators/streamline/stores/templates/active_record/create_events.rb.tt +14 -0
  12. data/lib/generators/streamline/stores/templates/active_record/event.rb.tt +12 -0
  13. data/lib/streamline.rb +15 -0
  14. data/lib/streamline/configuration.rb +19 -0
  15. data/lib/streamline/handler.rb +13 -0
  16. data/lib/streamline/jobs.rb +3 -0
  17. data/lib/streamline/jobs/base_job.rb +7 -0
  18. data/lib/streamline/jobs/handle_event_job.rb +62 -0
  19. data/lib/streamline/jobs/track_event_job.rb +15 -0
  20. data/lib/streamline/registry.rb +32 -0
  21. data/lib/streamline/stores.rb +16 -0
  22. data/lib/streamline/stores/active_record_store.rb +34 -0
  23. data/lib/streamline/stores/base_store.rb +31 -0
  24. data/lib/streamline/target.rb +7 -0
  25. data/lib/streamline/tracker.rb +29 -0
  26. data/lib/streamline/util.rb +20 -0
  27. data/lib/streamline/version.rb +3 -0
  28. data/spec/lib/streamline/handler_spec.rb +16 -0
  29. data/spec/lib/streamline/jobs/handle_event_job_spec.rb +108 -0
  30. data/spec/lib/streamline/jobs/track_event_job_spec.rb +28 -0
  31. data/spec/lib/streamline/registry_spec.rb +25 -0
  32. data/spec/lib/streamline/stores/active_record_store_spec.rb +7 -0
  33. data/spec/lib/streamline/stores/base_store_spec.rb +14 -0
  34. data/spec/lib/streamline/target_spec.rb +14 -0
  35. data/spec/lib/streamline/tracker_spec.rb +12 -0
  36. data/spec/lib/streamline_spec.rb +21 -0
  37. data/spec/spec_helper.rb +31 -0
  38. data/spec/support/event_context.rb +12 -0
  39. data/spec/support/handler_context.rb +5 -0
  40. data/spec/support/handlers.rb +7 -0
  41. data/spec/support/models.rb +27 -0
  42. data/spec/support/store_examples.rb +60 -0
  43. data/streamline.gemspec +31 -0
  44. metadata +295 -0
@@ -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
@@ -0,0 +1,6 @@
1
+ .bundle/
2
+ coverage/
3
+ pkg/
4
+ vendor/bundle/
5
+
6
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ ---
2
+ require: rubocop-rspec
3
+
4
+ Style/Documentation:
5
+ Enabled: false
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.4
4
+ before_install:
5
+ - gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,7 @@
1
+ # Airplane
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/airplane.svg)](https://badge.fury.io/rb/airplane)
4
+ [![Code Climate](https://codeclimate.com/github/atipugin/airplane/badges/gpa.svg)](https://codeclimate.com/github/atipugin/airplane)
5
+ [![Build Status](https://travis-ci.org/atipugin/airplane.svg?branch=master)](https://travis-ci.org/atipugin/airplane)
6
+
7
+ TODO: Write some meaningful description ;)
@@ -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)
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'pry'
5
+
6
+ lib = File.expand_path('../../lib', __FILE__)
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+
9
+ require 'streamline'
10
+
11
+ Pry.start
@@ -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
@@ -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,13 @@
1
+ module Streamline
2
+ module Handler
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def handle(event_name, options = {})
9
+ Streamline.registry.add(self, event_name, options)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ require 'streamline/jobs/base_job'
2
+ require 'streamline/jobs/track_event_job'
3
+ require 'streamline/jobs/handle_event_job'
@@ -0,0 +1,7 @@
1
+ module Streamline
2
+ module Jobs
3
+ class BaseJob < ActiveJob::Base
4
+ queue_as :streamline
5
+ end
6
+ end
7
+ 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,7 @@
1
+ module Streamline
2
+ module Target
3
+ def track_event(name, properties = {})
4
+ Streamline.tracker.track_event(self, name, properties)
5
+ end
6
+ end
7
+ 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,3 @@
1
+ module Streamline
2
+ VERSION = '0.1.0'
3
+ 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,7 @@
1
+ module Streamline
2
+ module Stores
3
+ RSpec.describe ActiveRecordStore do
4
+ it_behaves_like 'a store'
5
+ end
6
+ end
7
+ 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
@@ -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,5 @@
1
+ RSpec.shared_context 'handler' do
2
+ let(:handler_class) { TestHandler }
3
+ let(:handler_event_name) { FFaker::Lorem.word }
4
+ let(:handler_options) { {} }
5
+ end
@@ -0,0 +1,7 @@
1
+ class TestHandler
2
+ include Streamline::Handler
3
+
4
+ def run(event)
5
+ # Does nothing ;)
6
+ end
7
+ 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
@@ -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