triggerable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +89 -0
  8. data/Rakefile +6 -0
  9. data/lib/triggerable.rb +78 -0
  10. data/lib/triggerable/actions.rb +27 -0
  11. data/lib/triggerable/conditions/condition.rb +42 -0
  12. data/lib/triggerable/conditions/field/exists.rb +12 -0
  13. data/lib/triggerable/conditions/field/field_condition.rb +29 -0
  14. data/lib/triggerable/conditions/field/in.rb +17 -0
  15. data/lib/triggerable/conditions/field/or_equal_to.rb +15 -0
  16. data/lib/triggerable/conditions/lambda_condition.rb +12 -0
  17. data/lib/triggerable/conditions/method_condition.rb +11 -0
  18. data/lib/triggerable/conditions/predicate/and.rb +7 -0
  19. data/lib/triggerable/conditions/predicate/or.rb +7 -0
  20. data/lib/triggerable/conditions/predicate/predicate_condition.rb +46 -0
  21. data/lib/triggerable/conditions/schedule/after.rb +28 -0
  22. data/lib/triggerable/conditions/schedule/before.rb +35 -0
  23. data/lib/triggerable/conditions/schedule/schedule_condition.rb +33 -0
  24. data/lib/triggerable/engine.rb +28 -0
  25. data/lib/triggerable/rules/automation.rb +13 -0
  26. data/lib/triggerable/rules/rule.rb +11 -0
  27. data/lib/triggerable/rules/trigger.rb +14 -0
  28. data/lib/triggerable/version.rb +3 -0
  29. data/spec/conditions_spec.rb +200 -0
  30. data/spec/integration/actions_spec.rb +37 -0
  31. data/spec/integration/automations_spec.rb +353 -0
  32. data/spec/integration/conditions_spec.rb +144 -0
  33. data/spec/integration/short_syntax_spec.rb +92 -0
  34. data/spec/models.rb +23 -0
  35. data/spec/schema.rb +27 -0
  36. data/spec/scopes_spec.rb +78 -0
  37. data/spec/spec_helper.rb +22 -0
  38. data/triggerable.gemspec +28 -0
  39. metadata +191 -0
@@ -0,0 +1,46 @@
1
+ module Conditions
2
+ class PredicateCondition < Condition
3
+ attr_accessor :conditions
4
+
5
+ def initialize conditions
6
+ @conditions = conditions.map do |condition|
7
+ unless condition.is_a?(Hash)
8
+ condition
9
+ else
10
+ field = condition.keys.first
11
+ statement = condition.values.first
12
+
13
+ Condition.build({field => statement})
14
+ end
15
+ end
16
+ end
17
+
18
+ def scope table
19
+ predicate_scope = nil
20
+
21
+ @conditions.each_with_index do |condition, index|
22
+ condition_scope = condition.scope(table)
23
+
24
+ predicate_scope = if index.zero?
25
+ condition_scope
26
+ else
27
+ predicate_scope.send(predicate_name, condition_scope)
28
+ end
29
+ end
30
+
31
+ predicate_scope
32
+ end
33
+
34
+ protected
35
+
36
+ def predicate_name
37
+ self.class.name.demodulize.downcase
38
+ end
39
+
40
+ def true_conditions object
41
+ @conditions.select do |c|
42
+ c.is_a?(Symbol) ? object.send(c) : c.true_for?(object)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ module Conditions
2
+ class After < ScheduleCondition
3
+ def from
4
+ case @math_condition
5
+ when :greater_than, :less_than
6
+ Time.now - @value
7
+ when nil
8
+ automation_time - @value - Engine.interval
9
+ end
10
+ end
11
+
12
+ def to
13
+ automation_time - @value
14
+ end
15
+
16
+ private
17
+ def condition
18
+ return super if @math_condition.blank?
19
+
20
+ case @math_condition
21
+ when :greater_than
22
+ LessThanOrEqualTo.new(@field, from)
23
+ when :less_than
24
+ GreaterThanOrEqualTo.new(@field, from)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Conditions
2
+ class Before < ScheduleCondition
3
+ def from
4
+ case @math_condition
5
+ when :greater_than
6
+ Time.now
7
+ when :less_than
8
+ Time.now + @value
9
+ when nil
10
+ automation_time + @value
11
+ end
12
+ end
13
+
14
+ def to
15
+ case @math_condition
16
+ when :greater_than
17
+ Time.now + @value
18
+ when nil
19
+ automation_time + @value + Engine.interval
20
+ end
21
+ end
22
+
23
+ private
24
+ def condition
25
+ return super if @math_condition.blank?
26
+
27
+ case @math_condition
28
+ when :greater_than
29
+ And.new([GreaterThanOrEqualTo.new(@field, from), LessThan.new(@field, to)])
30
+ when :less_than
31
+ GreaterThanOrEqualTo.new(@field, from)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ module Conditions
2
+ class ScheduleCondition < FieldCondition
3
+ def initialize field, value
4
+ super
5
+ if value.is_a?(Hash)
6
+ @math_condition = value.keys.first
7
+ @value = value.values.first
8
+ end
9
+ end
10
+
11
+ def true_for? object
12
+ condition.true_for?(object)
13
+ end
14
+
15
+ def scope table
16
+ condition.scope(table)
17
+ end
18
+
19
+ protected
20
+ # automation_time is Time.now rounded by Engine.interval
21
+ def automation_time
22
+ i = Engine.interval
23
+ Time.at((Time.now.to_i / i) * i).utc
24
+ end
25
+
26
+ def condition
27
+ And.new [
28
+ GreaterThanOrEqualTo.new(@field, from),
29
+ LessThan.new(@field, to)
30
+ ]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ class Engine
2
+ cattr_accessor :triggers, :automations, :interval
3
+
4
+ self.triggers = []
5
+ self.automations = []
6
+
7
+ def self.trigger model, options, block
8
+ self.triggers << Rules::Trigger.new(model, options, block)
9
+ end
10
+
11
+ def self.automation model, options, block
12
+ self.automations << Rules::Automation.new(model, options, block)
13
+ end
14
+
15
+ def self.triggers_for model, callback
16
+ triggers.select{|t| t.model == model && t.callback == callback }
17
+ end
18
+
19
+ def self.run_automations interval
20
+ self.interval = interval
21
+ automations.each(&:execute!)
22
+ end
23
+
24
+ def self.clear
25
+ self.triggers = []
26
+ self.automations = []
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module Rules
2
+ class Automation < Rule
3
+ def execute!
4
+ table = Arel::Table.new(model.table_name)
5
+ scope = @condition.scope(table)
6
+ query = table.where(scope).project(Arel.sql('id')).to_sql
7
+ ids = ParentModel.connection.execute(query).map { |r| r['id'] }
8
+ models = model.where(id: ids)
9
+
10
+ models.each {|o| actions.each {|a| a.run_for!(o)} }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Rules
2
+ class Rule
3
+ attr_accessor :model, :condition, :actions
4
+
5
+ def initialize model, options, block
6
+ @model = model
7
+ @condition = Conditions::Condition.build(options[:if])
8
+ @actions = Triggerable::Action.build(block || options[:do])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module Rules
2
+ class Trigger < Rule
3
+ attr_accessor :callback
4
+
5
+ def initialize model, options, block
6
+ super
7
+ @callback = options[:on]
8
+ end
9
+
10
+ def execute! object
11
+ actions.each {|a| a.run_for!(object)} if condition.true_for?(object)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Triggerable
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,200 @@
1
+ require 'spec_helper'
2
+
3
+ describe Conditions do
4
+ before(:each) do
5
+ class Sample; attr_accessor(:field); end
6
+ @obj = Sample.new
7
+ end
8
+
9
+ context 'is' do
10
+
11
+ def check_value value
12
+ Conditions::Is.new(:field, value).true_for?(@obj)
13
+ end
14
+
15
+ def scope value
16
+ Conditions::Is.new(:field, value).scope
17
+ end
18
+
19
+ it 'integer' do
20
+ @obj.field = 1
21
+ expect(check_value(1)).to be_truthy
22
+
23
+ @obj.field = 2
24
+ expect(check_value(1)).to be_falsy
25
+ end
26
+
27
+ it 'string' do
28
+ @obj.field = '1'
29
+ expect(check_value('1')).to be_truthy
30
+
31
+ @obj.field = '2'
32
+ expect(check_value('1')).to be_falsy
33
+ end
34
+ end
35
+
36
+ context 'is_not' do
37
+
38
+ def check_value value
39
+ Conditions::IsNot.new(:field, value).true_for?(@obj)
40
+ end
41
+
42
+ def scope value
43
+ Conditions::IsNot.new(:field, value).scope
44
+ end
45
+
46
+ it 'integer' do
47
+ @obj.field = 1
48
+ expect(check_value(2)).to be_truthy
49
+
50
+ @obj.field = 2
51
+ expect(check_value(2)).to be_falsy
52
+ end
53
+
54
+ it 'string' do
55
+ @obj.field = '1'
56
+ expect(check_value('2')).to be_truthy
57
+
58
+ @obj.field = '2'
59
+ expect(check_value('2')).to be_falsy
60
+ end
61
+ end
62
+
63
+ context 'greater_than' do
64
+ def check_value value
65
+ Conditions::GreaterThan.new(:field, value).true_for?(@obj)
66
+ end
67
+
68
+ def scope value
69
+ Conditions::GreaterThan.new(:field, value).scope
70
+ end
71
+
72
+ it 'integer' do
73
+ @obj.field = 1
74
+
75
+ expect(check_value(-1)).to be_truthy
76
+ expect(check_value(0)).to be_truthy
77
+ expect(check_value(1)).to be_falsy
78
+ expect(check_value(2)).to be_falsy
79
+ end
80
+
81
+ it 'float' do
82
+ @obj.field = 1.0
83
+
84
+ expect(check_value(0)).to be_truthy
85
+ expect(check_value(0.9)).to be_truthy
86
+ expect(check_value(1.1)).to be_falsy
87
+ expect(check_value(2)).to be_falsy
88
+ end
89
+ end
90
+
91
+ context 'less_than' do
92
+ def check_value value
93
+ Conditions::LessThan.new(:field, value).true_for?(@obj)
94
+ end
95
+
96
+ def scope value
97
+ Conditions::LessThan.new(:field, value).scope
98
+ end
99
+
100
+ it 'integer' do
101
+ @obj.field = 1
102
+
103
+ expect(check_value(-1)).to be_falsy
104
+ expect(check_value(0)).to be_falsy
105
+ expect(check_value(1)).to be_falsy
106
+ expect(check_value(2)).to be_truthy
107
+ end
108
+
109
+ it 'float' do
110
+ @obj.field = 1.0
111
+
112
+ expect(check_value(0)).to be_falsy
113
+ expect(check_value(0.9)).to be_falsy
114
+ expect(check_value(1.1)).to be_truthy
115
+ expect(check_value(2)).to be_truthy
116
+ end
117
+ end
118
+
119
+ context 'in' do
120
+ def check_value value
121
+ Conditions::In.new(:field, value).true_for?(@obj)
122
+ end
123
+
124
+ def scope value
125
+ Conditions::In.new(:field, value).scope
126
+ end
127
+
128
+ it 'integer' do
129
+ @obj.field = 1
130
+
131
+ expect(check_value([0])).to be_falsy
132
+ expect(check_value([1])).to be_truthy
133
+ expect(check_value([1, 2])).to be_truthy
134
+ end
135
+
136
+ it 'float' do
137
+ @obj.field = 1.0
138
+
139
+ expect(check_value([0.0])).to be_falsy
140
+ expect(check_value([1.0])).to be_truthy
141
+ expect(check_value([1.0, 2.0])).to be_truthy
142
+ end
143
+
144
+ it 'string' do
145
+ @obj.field = '1'
146
+
147
+ expect(check_value(['0'])).to be_falsy
148
+ expect(check_value(['1'])).to be_truthy
149
+ expect(check_value(['1', '2'])).to be_truthy
150
+ end
151
+ end
152
+
153
+ context 'predicates' do
154
+ class TrueCondition
155
+ def true_for?(object); true; end
156
+ end
157
+
158
+ class FalseCondition
159
+ def true_for?(object); false; end
160
+ end
161
+
162
+ context 'and' do
163
+ before(:each) { @and_condition = Conditions::And.new([]) }
164
+
165
+ it ('true + true') do
166
+ @and_condition.conditions = [TrueCondition.new, TrueCondition.new]
167
+ expect(@and_condition.true_for?(@obj)).to be_truthy
168
+ end
169
+
170
+ it ('true + false') do
171
+ @and_condition.conditions = [TrueCondition.new, FalseCondition.new]
172
+ expect(@and_condition.true_for?(@obj)).to be_falsy
173
+ end
174
+
175
+ it ('true + false') do
176
+ @and_condition.conditions = [FalseCondition.new, FalseCondition.new]
177
+ expect(@and_condition.true_for?(@obj)).to be_falsy
178
+ end
179
+ end
180
+
181
+ context 'or' do
182
+ before(:each) { @or_condition = Conditions::Or.new([]) }
183
+
184
+ it ('true + true') do
185
+ @or_condition.conditions = [TrueCondition.new, TrueCondition.new]
186
+ expect(@or_condition.true_for?(@obj)).to be_truthy
187
+ end
188
+
189
+ it ('true + false') do
190
+ @or_condition.conditions = [TrueCondition.new, FalseCondition.new]
191
+ expect(@or_condition.true_for?(@obj)).to be_truthy
192
+ end
193
+
194
+ it ('true + false') do
195
+ @or_condition.conditions = [FalseCondition.new, FalseCondition.new]
196
+ expect(@or_condition.true_for?(@obj)).to be_falsy
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Actions' do
4
+ before(:each) do
5
+ Engine.clear
6
+ TestTask.destroy_all
7
+ end
8
+
9
+ class CreateFollowUp < Triggerable::Action
10
+ def run_for! task
11
+ TestTask.create kind: 'follow up'
12
+ end
13
+ end
14
+
15
+ it 'custom action' do
16
+ TestTask.trigger on: :after_update, if: {status: 'solved'}, do: :create_follow_up
17
+
18
+ task = TestTask.create
19
+ expect(TestTask.count).to eq(1)
20
+
21
+ task.update_attributes status: 'solved'
22
+ expect(TestTask.count).to eq(2)
23
+ expect(TestTask.all.last.kind).to eq('follow up')
24
+ end
25
+
26
+ it 'custom action chain' do
27
+ TestTask.trigger on: :after_update, if: {status: 'solved'}, do: [:create_follow_up, :create_follow_up]
28
+
29
+ task = TestTask.create
30
+ expect(TestTask.count).to eq(1)
31
+
32
+ task.update_attributes status: 'solved'
33
+ expect(TestTask.count).to eq(3)
34
+ expect(TestTask.all[-2].kind).to eq('follow up')
35
+ expect(TestTask.all.last.kind).to eq('follow up')
36
+ end
37
+ end