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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: eccbfd503b44b892a1247b8dde94381fba3017ed
|
4
|
+
data.tar.gz: a159bb8a7559a264be77616c3d8bd83f144aa0ff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf594d01a28d8a826c38d13db346027233b5419147406d725d53e35e4e39bfb260689693ceec44e83000b83eb708a5d2b3343498d64c4d167b8dc06435dd4c9a
|
7
|
+
data.tar.gz: 6bef7a97e9236f09954cacdc30c76b4f3b9012864b2168115c0e2a482ea136630267c5d99f8bae96bd1bf34c75f8c1da9938acae1cb093790d067ffe75f4f51e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 DmitryTsepelev
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Triggerable
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/anjlab/triggerable)
|
4
|
+
|
5
|
+
Triggerable is a powerful engine for adding a conditional behaviour for ActiveRecord models. This logic can be defined in two ways - as triggers and automations. Triggers are called right after model creating, updating or saving, and automations are run on schedule (e.g. 2 hours after update).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'triggerable'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
```shell
|
18
|
+
bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Setup and defining trigger and automation:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class User < ActiveRecord::Base
|
27
|
+
trigger on: :after_create, if: { receives_sms: true }, do
|
28
|
+
user.send_welcome_sms
|
29
|
+
end
|
30
|
+
|
31
|
+
automation if: { created_at: { after: 24 }, confirmed: false } do
|
32
|
+
send_confirmation_email
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Combining different conditions and predicates:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
trigger on: :after_create, if: { field: { is: 1 } }, ...
|
41
|
+
# short form
|
42
|
+
trigger on: :after_create, if: { field: 1 }, ...
|
43
|
+
|
44
|
+
trigger on: :after_create, if: { field: { is_not: 1 } }, ...
|
45
|
+
|
46
|
+
trigger on: :after_create, if: { field: { in: [1, 2, 3] } }, ...
|
47
|
+
# short form
|
48
|
+
trigger on: :after_create, if: { field: [1, 2, 3] }, ...
|
49
|
+
|
50
|
+
trigger on: :after_create, if: { field: { greater_then: 1 } }, ...
|
51
|
+
trigger on: :after_create, if: { field: { less_then: 1 } }, ...
|
52
|
+
|
53
|
+
trigger on: :after_create, if: { field: { exists: true } }, ...
|
54
|
+
|
55
|
+
trigger on: :after_create, if: { and: [{ field1: '1' }, { field2: 1 }] }, ...
|
56
|
+
trigger on: :after_create, if: { or: [{ field1: '1' }, { field2: 1 }] }, ...
|
57
|
+
```
|
58
|
+
|
59
|
+
Triggerable does not run automations by itself, you should call `Engine.run_automations(interval)` using any scheduling script. Interval is a time difference between calling the method (e.g. `1.hour`). *You should avoid situations when your interval is less then the time your automations need to complete!*
|
60
|
+
|
61
|
+
If you have more complex condition or need to check associations (not supported in DSL now), you should use a lambda condition form:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
trigger on: :after_update, if: -> { orders.any? } do
|
65
|
+
# ...
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
If you need to share logic between triggers/automations bodies you can move it into separate class. It should be inherited from `Triggerable::Action` and implement a single method `run_for!(obj)` where obj is a triggered object. Then you can pass a name of your action class instead of do block.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class SendWelcomeSms < Triggerable::Action
|
73
|
+
def run_for! object
|
74
|
+
SmsGateway.send_to object.phone, welcome_text
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class User
|
79
|
+
trigger on: :after_create, if: { receives_sms: true }, do: :send_welcome_sms
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
|
85
|
+
1. Fork it
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/triggerable.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require "triggerable/version"
|
2
|
+
|
3
|
+
require "triggerable/engine"
|
4
|
+
|
5
|
+
require "triggerable/rules/rule"
|
6
|
+
require "triggerable/rules/trigger"
|
7
|
+
require "triggerable/rules/automation"
|
8
|
+
|
9
|
+
require "triggerable/conditions/condition"
|
10
|
+
require "triggerable/conditions/lambda_condition"
|
11
|
+
require "triggerable/conditions/method_condition"
|
12
|
+
|
13
|
+
require "triggerable/conditions/field/field_condition"
|
14
|
+
require "triggerable/conditions/field/exists"
|
15
|
+
require "triggerable/conditions/field/in"
|
16
|
+
require "triggerable/conditions/field/or_equal_to"
|
17
|
+
|
18
|
+
require "triggerable/conditions/predicate/predicate_condition"
|
19
|
+
require "triggerable/conditions/predicate/and"
|
20
|
+
require "triggerable/conditions/predicate/or"
|
21
|
+
|
22
|
+
require "triggerable/conditions/schedule/schedule_condition"
|
23
|
+
require "triggerable/conditions/schedule/before"
|
24
|
+
require "triggerable/conditions/schedule/after"
|
25
|
+
|
26
|
+
require "triggerable/actions"
|
27
|
+
|
28
|
+
module Triggerable
|
29
|
+
extend ActiveSupport::Concern
|
30
|
+
|
31
|
+
included do
|
32
|
+
CALLBACKS = [:before_create, :before_save, :before_update, :after_create, :after_save, :after_update]
|
33
|
+
CALLBACKS.each do |callback|
|
34
|
+
method_name = "run_#{callback}_triggers"
|
35
|
+
define_method(method_name) { run_triggers(callback) }
|
36
|
+
send(callback, method_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def run_triggers callback
|
42
|
+
Engine.triggers_for(self.class, callback).each{|t| t.execute!(self)}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def trigger options, &block
|
48
|
+
Engine.trigger(self, options, block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def automation options, &block
|
52
|
+
Engine.automation(self, options, block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
COMPARSIONS = [
|
58
|
+
{ name: 'Is', ancestor: Conditions::FieldCondition, args: { ruby_comparator: '==', db_comparator: 'eq' } },
|
59
|
+
{ name: 'GreaterThan', ancestor: Conditions::FieldCondition, args: { ruby_comparator: '>', db_comparator: 'gt' } },
|
60
|
+
{ name: 'LessThan', ancestor: Conditions::FieldCondition, args: { ruby_comparator: '<', db_comparator: 'lt' } },
|
61
|
+
{ name: 'IsNot', ancestor: Conditions::FieldCondition, args: { ruby_comparator: '!=', db_comparator: 'not_eq'} },
|
62
|
+
|
63
|
+
{ name: 'GreaterThanOrEqualTo', ancestor: Conditions::OrEqualTo, args: { db_comparator: 'gteq', additional_condition: 'Conditions::GreaterThan' } },
|
64
|
+
{ name: 'LessThanOrEqualTo', ancestor: Conditions::OrEqualTo, args: { db_comparator: 'lteq', additional_condition: 'Conditions::LessThan' } }
|
65
|
+
]
|
66
|
+
|
67
|
+
COMPARSIONS.each do |desc|
|
68
|
+
klass = Class.new(desc[:ancestor]) do
|
69
|
+
define_method :initialize do |field, value|
|
70
|
+
desc[:args].each_pair { |name, val| instance_variable_set("@#{name}".to_sym, val) }
|
71
|
+
super(field, value)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
Conditions.const_set(desc[:name], klass)
|
76
|
+
end
|
77
|
+
|
78
|
+
ActiveSupport.on_load(:active_record) { include Triggerable }
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Triggerable
|
2
|
+
class Action
|
3
|
+
def self.build source
|
4
|
+
if source.is_a?(Proc)
|
5
|
+
[LambdaAction.new(source)]
|
6
|
+
else
|
7
|
+
Array(source).map do |source|
|
8
|
+
descendant = descendants.find { |d| d == source.to_s.camelize.constantize }
|
9
|
+
descendant.new if descendant.present?
|
10
|
+
end.compact
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def run_for!(object); end
|
15
|
+
end
|
16
|
+
|
17
|
+
class LambdaAction < Action
|
18
|
+
def initialize block
|
19
|
+
@block = block
|
20
|
+
end
|
21
|
+
|
22
|
+
def run_for! object
|
23
|
+
proc = @block
|
24
|
+
object.instance_eval { instance_exec(&proc) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Conditions
|
2
|
+
class Condition
|
3
|
+
def self.build condition
|
4
|
+
return Condition.new if condition.blank?
|
5
|
+
return LambdaCondition.new(condition) if condition.is_a?(Proc)
|
6
|
+
return MethodCondition.new(condition) if condition.is_a?(Symbol)
|
7
|
+
|
8
|
+
key = condition.keys.first
|
9
|
+
value = condition[key]
|
10
|
+
|
11
|
+
if [:and, :or].include?(key)
|
12
|
+
predicate_condition(key, value)
|
13
|
+
else
|
14
|
+
field_condition(key, value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def true_for?(object); true; end
|
19
|
+
|
20
|
+
def scope; ''; end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.predicate_condition class_name, value
|
25
|
+
condition_class(class_name).new(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.field_condition field, value
|
29
|
+
if value.is_a?(Array)
|
30
|
+
Conditions::In.new(field, value)
|
31
|
+
elsif value.is_a?(Hash)
|
32
|
+
condition_class(value.keys.first).new(field, value.values.first)
|
33
|
+
else
|
34
|
+
Conditions::Is.new(field, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.condition_class sym
|
39
|
+
"Conditions::#{sym.to_s.camelize}".constantize
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Conditions
|
2
|
+
class FieldCondition < Condition
|
3
|
+
def initialize field, value
|
4
|
+
@field = field
|
5
|
+
@value = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def true_for? object
|
9
|
+
field_value(object).send(@ruby_comparator, @value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def scope table
|
13
|
+
table[@field].send(@db_comparator, @value)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def field_value object
|
18
|
+
object.send(@field)
|
19
|
+
end
|
20
|
+
|
21
|
+
def sanitized_value
|
22
|
+
if @value.is_a?(Array)
|
23
|
+
@value.map { |v| ActiveRecord::Base::sanitize(v) }
|
24
|
+
else
|
25
|
+
ActiveRecord::Base::sanitize(@value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Conditions
|
2
|
+
class In < FieldCondition
|
3
|
+
def initialize field, condition
|
4
|
+
super
|
5
|
+
@db_comparator = 'in'
|
6
|
+
end
|
7
|
+
|
8
|
+
def true_for? object
|
9
|
+
@value.include?(field_value(object))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def sanitized_value
|
14
|
+
"(#{super.join(',')})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Conditions
|
2
|
+
class OrEqualTo < FieldCondition
|
3
|
+
def initialize field, value
|
4
|
+
super
|
5
|
+
@condition = Or.new [
|
6
|
+
@additional_condition.constantize.new(field, value),
|
7
|
+
Is.new(field, value)
|
8
|
+
]
|
9
|
+
end
|
10
|
+
|
11
|
+
def true_for? object
|
12
|
+
@condition.true_for?(object)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|