triggerable 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 (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