predicator 1.1.0 → 1.2.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 +4 -4
- data/.rubocop.yml +46 -0
- data/HISTORY.md +8 -0
- data/README.md +30 -8
- data/lib/predicator/ast.rb +71 -19
- data/lib/predicator/evaluator.rb +55 -14
- data/lib/predicator/lexer.rex +18 -0
- data/lib/predicator/lexer.rex.rb +27 -0
- data/lib/predicator/parser.rb +233 -97
- data/lib/predicator/parser.y +54 -17
- data/lib/predicator/version.rb +1 -1
- data/lib/predicator/visitors/dot.rb +21 -8
- data/lib/predicator/visitors/instructions.rb +72 -6
- data/lib/predicator/visitors/string.rb +16 -0
- data/lib/predicator/visitors/visitor.rb +71 -19
- data/predicator.gemspec +1 -0
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b778c45c3885f3fd113cc8b71b20980774d3a694
|
4
|
+
data.tar.gz: be582dd047eb0d20e7380b1d7c5edd83d2d589c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25f7894b6db4c564eb15d0c2ef2fb4fb785ca5a030abf930da4e8a4eb6b3d3b591e70298cb256373ca7e52e4b62290b40858e66f0c37924296ce8a2f628146bc
|
7
|
+
data.tar.gz: bbb3843e12ec3d86f7ec27279ecf7eae7a8962fb47f88b59cd9521095eb89f26dccee8cbcc1cf62190139ea88f9eaf8c73e2f6d614dfe14b6eb2d612219cd7af
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
DisabledByDefault: true
|
4
|
+
Exclude:
|
5
|
+
- "lib/predicator/parser.rb" #generated file
|
6
|
+
- "lib/predicator/lexer.rex.rb" #generated file
|
7
|
+
|
8
|
+
Bundler/DuplicatedGem:
|
9
|
+
Enabled: true
|
10
|
+
Lint:
|
11
|
+
Enabled: true
|
12
|
+
Performance:
|
13
|
+
Enabled: true
|
14
|
+
Security:
|
15
|
+
Enabled: true
|
16
|
+
|
17
|
+
# Single quotes being faster is hardly measurable and only affects parse time.
|
18
|
+
# Enforcing double quotes reduces the times where you need to change them
|
19
|
+
# when introducing an interpolation. Use single quotes only if their semantics
|
20
|
+
# are needed.
|
21
|
+
Style/StringLiterals:
|
22
|
+
EnforcedStyle: double_quotes
|
23
|
+
|
24
|
+
# Seattle style
|
25
|
+
Style/MethodDefParentheses:
|
26
|
+
EnforcedStyle: require_no_parentheses
|
27
|
+
|
28
|
+
# Allow regex argument in Seattle style
|
29
|
+
Lint/AmbiguousRegexpLiteral:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
# Allow block argument in Seattle style
|
33
|
+
Lint/AmbiguousOperator:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
# No long classes
|
37
|
+
Style/ClassLength:
|
38
|
+
Enabled: true
|
39
|
+
Exclude:
|
40
|
+
- "test/**/*.rb"
|
41
|
+
- "lib/predicator/visitors/instructions.rb"
|
42
|
+
- "lib/predicator/evaluator.rb"
|
43
|
+
|
44
|
+
Lint/HandleExceptions:
|
45
|
+
Exclude:
|
46
|
+
- lib/tasks/**/*.rake
|
data/HISTORY.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
### 1.2.0 / 2018-09-13
|
2
|
+
|
3
|
+
* Adds type casting to comparisons
|
4
|
+
* Adds date type and predicates
|
5
|
+
* Adds "from now" and "ago" relative dates
|
6
|
+
* Adds "present" and "blank" predicates
|
7
|
+
* Adds "starts with" and "ends with" string comparisons
|
8
|
+
|
1
9
|
### 1.1.0 / 2017-12-06
|
2
10
|
|
3
11
|
* Rescues errors when comparing invalid types
|
data/README.md
CHANGED
@@ -1,24 +1,47 @@
|
|
1
1
|
[](http://badge.fury.io/rb/predicator)
|
2
2
|
[](https://travis-ci.org/predicator/predicator)
|
3
|
-
[](https://gemnasium.com/johnnyt/predicator)
|
4
3
|
[](https://coveralls.io/github/predicator/predicator?branch=master)
|
5
4
|
|
6
5
|
# Predicator
|
7
6
|
|
8
|
-
Predicator is a predicate engine.
|
7
|
+
Predicator is a safe (does not eval code), admin or business user facing predicate engine. It turns a string predicate like `"score > 600 or (score > 580 and monthly_income > 9000)"` along with a supplied context into a `true` or `false`. This predicate can be stored as an attribute of a model (ex: an Offer model could store a predicate indicating if it is available to a customer).
|
9
8
|
|
10
9
|
## Usage
|
11
10
|
|
12
|
-
|
11
|
+
Simple usage:
|
13
12
|
|
14
13
|
```ruby
|
15
14
|
require "predicator"
|
16
15
|
|
17
|
-
Predicator.evaluate "
|
16
|
+
Predicator.evaluate "score > 600 or (score > 580 and income > 9000)", score: 590 # false
|
18
17
|
|
19
|
-
Predicator.evaluate "
|
18
|
+
Predicator.evaluate "score > 600 or (score > 580 and income > 9000)", score: 590, income: 9500 # true
|
19
|
+
```
|
20
|
+
|
21
|
+
Example usage with a model:
|
20
22
|
|
21
|
-
|
23
|
+
```ruby
|
24
|
+
class Customer
|
25
|
+
...
|
26
|
+
def to_hash
|
27
|
+
{
|
28
|
+
...
|
29
|
+
score: score,
|
30
|
+
income: income,
|
31
|
+
...
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Offer
|
37
|
+
attr_accessor :available_predicate
|
38
|
+
|
39
|
+
...
|
40
|
+
|
41
|
+
def available_to? customer
|
42
|
+
Predicator.evaluate available_predicate, customer.to_hash
|
43
|
+
end
|
44
|
+
end
|
22
45
|
```
|
23
46
|
|
24
47
|
## Installation
|
@@ -40,10 +63,9 @@ Or install it yourself as:
|
|
40
63
|
## Development
|
41
64
|
|
42
65
|
After checking out the repo, run `bin/setup` to install dependencies.
|
43
|
-
Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
66
|
+
Then, run `rake test` (or just `rake`) to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
44
67
|
|
45
68
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
46
|
-
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
47
69
|
|
48
70
|
## Contributing
|
49
71
|
|
data/lib/predicator/ast.rb
CHANGED
@@ -21,10 +21,6 @@ module Predicator
|
|
21
21
|
Visitors::Instructions.new.accept self
|
22
22
|
end
|
23
23
|
|
24
|
-
def to_predicate
|
25
|
-
Visitors::Predicate.new.accept self
|
26
|
-
end
|
27
|
-
|
28
24
|
def to_s
|
29
25
|
Visitors::String.new.accept self
|
30
26
|
end
|
@@ -51,7 +47,7 @@ module Predicator
|
|
51
47
|
def variable?; true; end
|
52
48
|
end
|
53
49
|
|
54
|
-
%w[ True False Integer String ].each do |t|
|
50
|
+
%w[ True False Integer String Date Duration Blank Present ].each do |t|
|
55
51
|
class_eval <<-eoruby, __FILE__, __LINE__ + 1
|
56
52
|
class #{t} < Literal;
|
57
53
|
def type; :#{t.upcase}; end
|
@@ -63,8 +59,12 @@ module Predicator
|
|
63
59
|
def children; [left] end
|
64
60
|
end
|
65
61
|
|
66
|
-
class
|
67
|
-
def type; :
|
62
|
+
class IntegerArray < Unary
|
63
|
+
def type; :INTARRAY; end
|
64
|
+
end
|
65
|
+
|
66
|
+
class StringArray < Unary
|
67
|
+
def type; :STRARRAY; end
|
68
68
|
end
|
69
69
|
|
70
70
|
class Not < Unary
|
@@ -75,6 +75,14 @@ module Predicator
|
|
75
75
|
def type; :GROUP; end
|
76
76
|
end
|
77
77
|
|
78
|
+
class DateFromNow < Unary
|
79
|
+
def type; :DATEFROMNOW; end
|
80
|
+
end
|
81
|
+
|
82
|
+
class DateAgo < Unary
|
83
|
+
def type; :DATEAGO; end
|
84
|
+
end
|
85
|
+
|
78
86
|
class Binary < Node
|
79
87
|
attr_accessor :right
|
80
88
|
|
@@ -86,24 +94,64 @@ module Predicator
|
|
86
94
|
def children; [left, right] end
|
87
95
|
end
|
88
96
|
|
89
|
-
class
|
90
|
-
def type; :
|
97
|
+
class IntegerEqual < Binary
|
98
|
+
def type; :INTEQ; end
|
99
|
+
end
|
100
|
+
|
101
|
+
class StringEqual < Binary
|
102
|
+
def type; :STREQ; end
|
103
|
+
end
|
104
|
+
|
105
|
+
class DateEqual < Binary
|
106
|
+
def type; :DATEQ; end
|
107
|
+
end
|
108
|
+
|
109
|
+
class IntegerGreaterThan < Binary
|
110
|
+
def type; :INTGT; end
|
111
|
+
end
|
112
|
+
|
113
|
+
class StringGreaterThan < Binary
|
114
|
+
def type; :STRGT; end
|
115
|
+
end
|
116
|
+
|
117
|
+
class DateGreaterThan < Binary
|
118
|
+
def type; :DATGT; end
|
119
|
+
end
|
120
|
+
|
121
|
+
class IntegerLessThan < Binary
|
122
|
+
def type; :INTLT; end
|
123
|
+
end
|
124
|
+
|
125
|
+
class StringLessThan < Binary
|
126
|
+
def type; :STRLT; end
|
91
127
|
end
|
92
128
|
|
93
|
-
class
|
94
|
-
def type; :
|
129
|
+
class DateLessThan < Binary
|
130
|
+
def type; :DATLT; end
|
95
131
|
end
|
96
132
|
|
97
|
-
class
|
98
|
-
def type; :
|
133
|
+
class IntegerIn < Binary
|
134
|
+
def type; :INTIN; end
|
99
135
|
end
|
100
136
|
|
101
|
-
class
|
102
|
-
def type; :
|
137
|
+
class StringIn < Binary
|
138
|
+
def type; :STRIN; end
|
103
139
|
end
|
104
140
|
|
105
|
-
class
|
106
|
-
def type; :
|
141
|
+
class IntegerNotIn < Binary
|
142
|
+
def type; :INTNOTIN; end
|
143
|
+
end
|
144
|
+
|
145
|
+
class StringNotIn < Binary
|
146
|
+
def type; :STRNOTIN; end
|
147
|
+
end
|
148
|
+
|
149
|
+
class StringStartsWith < Binary
|
150
|
+
def type; :STRSTARTSWITH; end
|
151
|
+
end
|
152
|
+
|
153
|
+
class StringEndsWith < Binary
|
154
|
+
def type; :STRENDSWITH; end
|
107
155
|
end
|
108
156
|
|
109
157
|
class And < Binary
|
@@ -126,8 +174,12 @@ module Predicator
|
|
126
174
|
def children; [left, middle, right] end
|
127
175
|
end
|
128
176
|
|
129
|
-
class
|
130
|
-
def type; :
|
177
|
+
class IntegerBetween < Ternary
|
178
|
+
def type; :INTBETWEEN; end
|
179
|
+
end
|
180
|
+
|
181
|
+
class DateBetween < Ternary
|
182
|
+
def type; :DATBETWEEN; end
|
131
183
|
end
|
132
184
|
|
133
185
|
class BooleanVariable < Unary
|
data/lib/predicator/evaluator.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "date"
|
2
|
+
|
1
3
|
module Predicator
|
2
4
|
class Evaluator
|
3
5
|
attr_reader :instructions, :stack, :context
|
@@ -24,18 +26,19 @@ module Predicator
|
|
24
26
|
|
25
27
|
def process instruction
|
26
28
|
case instruction.first
|
27
|
-
when "not"
|
28
|
-
|
29
|
-
when "
|
30
|
-
|
31
|
-
when "
|
32
|
-
|
33
|
-
when "
|
34
|
-
|
35
|
-
when "
|
36
|
-
stack.push
|
37
|
-
when "
|
38
|
-
|
29
|
+
when "not" then stack.push !stack.pop
|
30
|
+
when "jfalse" then jump_if_false instruction.last
|
31
|
+
when "jtrue" then jump_if_true instruction.last
|
32
|
+
when "lit", "array" then stack.push instruction.last
|
33
|
+
when "load" then stack.push context[instruction.last]
|
34
|
+
when "to_bool" then stack.push !!stack.pop
|
35
|
+
when "to_int" then stack.push to_int(stack.pop)
|
36
|
+
when "to_str" then stack.push to_str(stack.pop)
|
37
|
+
when "to_date" then stack.push to_date(stack.pop)
|
38
|
+
when "date_ago" then stack.push date_ago(stack.pop)
|
39
|
+
when "date_from_now" then stack.push date_from_now(stack.pop)
|
40
|
+
when "blank" then stack.push blank?(stack.pop)
|
41
|
+
when "present" then stack.push !blank?(stack.pop)
|
39
42
|
when "compare"
|
40
43
|
if instruction.last == "BETWEEN"
|
41
44
|
compare_BETWEEN
|
@@ -45,6 +48,36 @@ module Predicator
|
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
51
|
+
def to_int val
|
52
|
+
if val.nil? || (val.is_a?(String) && val.empty?)
|
53
|
+
nil
|
54
|
+
else
|
55
|
+
val.to_i
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_str val
|
60
|
+
val.nil? ? nil : val.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_date val
|
64
|
+
val.nil? ? nil : Date.parse(val)
|
65
|
+
end
|
66
|
+
|
67
|
+
def date_ago seconds
|
68
|
+
past_time = Time.now - seconds
|
69
|
+
to_date past_time.strftime "%Y-%m-%d"
|
70
|
+
end
|
71
|
+
|
72
|
+
def date_from_now seconds
|
73
|
+
future_time = Time.now + seconds
|
74
|
+
to_date future_time.strftime "%Y-%m-%d"
|
75
|
+
end
|
76
|
+
|
77
|
+
def blank? val
|
78
|
+
val.respond_to?(:empty?) ? !!val.empty? : !val
|
79
|
+
end
|
80
|
+
|
48
81
|
def jump_if_false offset
|
49
82
|
if stack[-1] == false
|
50
83
|
adjusted_offset = offset - 1
|
@@ -71,7 +104,7 @@ module Predicator
|
|
71
104
|
else
|
72
105
|
stack.push send("compare_#{comparison}", left, right)
|
73
106
|
end
|
74
|
-
rescue
|
107
|
+
rescue StandardError
|
75
108
|
stack.push false
|
76
109
|
end
|
77
110
|
|
@@ -95,6 +128,14 @@ module Predicator
|
|
95
128
|
!right.include? left
|
96
129
|
end
|
97
130
|
|
131
|
+
def compare_STARTSWITH left, right
|
132
|
+
left.start_with? right
|
133
|
+
end
|
134
|
+
|
135
|
+
def compare_ENDSWITH left, right
|
136
|
+
left.end_with? right
|
137
|
+
end
|
138
|
+
|
98
139
|
def compare_BETWEEN
|
99
140
|
max = stack.pop
|
100
141
|
min = stack.pop
|
@@ -105,7 +146,7 @@ module Predicator
|
|
105
146
|
result = val.between? min, max
|
106
147
|
stack.push result
|
107
148
|
end
|
108
|
-
rescue
|
149
|
+
rescue StandardError
|
109
150
|
stack.push false
|
110
151
|
end
|
111
152
|
end
|
data/lib/predicator/lexer.rex
CHANGED
@@ -21,6 +21,15 @@ macro
|
|
21
21
|
EQ /=/
|
22
22
|
GT />/
|
23
23
|
LT /</
|
24
|
+
ENDSWITH /ends with/
|
25
|
+
STARTSWITH /starts with/
|
26
|
+
BEGINSWITH /begins with/
|
27
|
+
BLANK /is blank/
|
28
|
+
PRESENT /is present/
|
29
|
+
AGO /ago/
|
30
|
+
FROMNOW /from now/
|
31
|
+
DATE /\d{4}[-|\/]\d{2}[-|\/]\d{2}/i
|
32
|
+
DURATION /\d+d/
|
24
33
|
INTEGER /[+-]?\d(_?\d)*\b/
|
25
34
|
STRING /(["'])(?:\\?.)*?\1/
|
26
35
|
IDENTIFIER /[a-z][A-Za-z0-9_]*\b/
|
@@ -43,6 +52,15 @@ rule
|
|
43
52
|
/#{EQ}/ { [:EQ, text] }
|
44
53
|
/#{GT}/ { [:GT, text] }
|
45
54
|
/#{LT}/ { [:LT, text] }
|
55
|
+
/#{AGO}/ { [:AGO, text] }
|
56
|
+
/#{FROMNOW}/ { [:FROMNOW, text] }
|
57
|
+
/#{ENDSWITH}/ { [:ENDSWITH, text] }
|
58
|
+
/#{STARTSWITH}/ { [:STARTSWITH, text] }
|
59
|
+
/#{BEGINSWITH}/ { [:STARTSWITH, text] }
|
60
|
+
/#{BLANK}/ { [:BLANK, text] }
|
61
|
+
/#{PRESENT}/ { [:PRESENT, text] }
|
62
|
+
/#{DATE}/ { [:DATE, text] }
|
63
|
+
/#{DURATION}/ { [:DURATION, text] }
|
46
64
|
/#{INTEGER}/ { [:INTEGER, text] }
|
47
65
|
/#{STRING}/ { [:STRING, text[1...-1]] }
|
48
66
|
/#{IDENTIFIER}/ { [:IDENTIFIER, text] }
|
data/lib/predicator/lexer.rex.rb
CHANGED
@@ -26,6 +26,15 @@ class Predicator::Lexer
|
|
26
26
|
EQ = /=/
|
27
27
|
GT = />/
|
28
28
|
LT = /</
|
29
|
+
ENDSWITH = /ends with/
|
30
|
+
STARTSWITH = /starts with/
|
31
|
+
BEGINSWITH = /begins with/
|
32
|
+
BLANK = /is blank/
|
33
|
+
PRESENT = /is present/
|
34
|
+
AGO = /ago/
|
35
|
+
FROMNOW = /from now/
|
36
|
+
DATE = /\d{4}[-|\/]\d{2}[-|\/]\d{2}/i
|
37
|
+
DURATION = /\d+d/
|
29
38
|
INTEGER = /[+-]?\d(_?\d)*\b/
|
30
39
|
STRING = /(["'])(?:\\?.)*?\1/
|
31
40
|
IDENTIFIER = /[a-z][A-Za-z0-9_]*\b/
|
@@ -131,6 +140,24 @@ class Predicator::Lexer
|
|
131
140
|
action { [:GT, text] }
|
132
141
|
when text = ss.scan(/#{LT}/) then
|
133
142
|
action { [:LT, text] }
|
143
|
+
when text = ss.scan(/#{AGO}/) then
|
144
|
+
action { [:AGO, text] }
|
145
|
+
when text = ss.scan(/#{FROMNOW}/) then
|
146
|
+
action { [:FROMNOW, text] }
|
147
|
+
when text = ss.scan(/#{ENDSWITH}/) then
|
148
|
+
action { [:ENDSWITH, text] }
|
149
|
+
when text = ss.scan(/#{STARTSWITH}/) then
|
150
|
+
action { [:STARTSWITH, text] }
|
151
|
+
when text = ss.scan(/#{BEGINSWITH}/) then
|
152
|
+
action { [:STARTSWITH, text] }
|
153
|
+
when text = ss.scan(/#{BLANK}/) then
|
154
|
+
action { [:BLANK, text] }
|
155
|
+
when text = ss.scan(/#{PRESENT}/) then
|
156
|
+
action { [:PRESENT, text] }
|
157
|
+
when text = ss.scan(/#{DATE}/) then
|
158
|
+
action { [:DATE, text] }
|
159
|
+
when text = ss.scan(/#{DURATION}/) then
|
160
|
+
action { [:DURATION, text] }
|
134
161
|
when text = ss.scan(/#{INTEGER}/) then
|
135
162
|
action { [:INTEGER, text] }
|
136
163
|
when text = ss.scan(/#{STRING}/) then
|