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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +89 -0
- data/Rakefile +6 -0
- data/lib/triggerable.rb +78 -0
- data/lib/triggerable/actions.rb +27 -0
- data/lib/triggerable/conditions/condition.rb +42 -0
- data/lib/triggerable/conditions/field/exists.rb +12 -0
- data/lib/triggerable/conditions/field/field_condition.rb +29 -0
- data/lib/triggerable/conditions/field/in.rb +17 -0
- data/lib/triggerable/conditions/field/or_equal_to.rb +15 -0
- data/lib/triggerable/conditions/lambda_condition.rb +12 -0
- data/lib/triggerable/conditions/method_condition.rb +11 -0
- data/lib/triggerable/conditions/predicate/and.rb +7 -0
- data/lib/triggerable/conditions/predicate/or.rb +7 -0
- data/lib/triggerable/conditions/predicate/predicate_condition.rb +46 -0
- data/lib/triggerable/conditions/schedule/after.rb +28 -0
- data/lib/triggerable/conditions/schedule/before.rb +35 -0
- data/lib/triggerable/conditions/schedule/schedule_condition.rb +33 -0
- data/lib/triggerable/engine.rb +28 -0
- data/lib/triggerable/rules/automation.rb +13 -0
- data/lib/triggerable/rules/rule.rb +11 -0
- data/lib/triggerable/rules/trigger.rb +14 -0
- data/lib/triggerable/version.rb +3 -0
- data/spec/conditions_spec.rb +200 -0
- data/spec/integration/actions_spec.rb +37 -0
- data/spec/integration/automations_spec.rb +353 -0
- data/spec/integration/conditions_spec.rb +144 -0
- data/spec/integration/short_syntax_spec.rb +92 -0
- data/spec/models.rb +23 -0
- data/spec/schema.rb +27 -0
- data/spec/scopes_spec.rb +78 -0
- data/spec/spec_helper.rb +22 -0
- data/triggerable.gemspec +28 -0
- 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,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
|