predicator 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/predicator.svg)](http://badge.fury.io/rb/predicator)
|
2
2
|
[![Build Status](https://travis-ci.org/predicator/predicator.svg?branch=master)](https://travis-ci.org/predicator/predicator)
|
3
|
-
[![Dependency Status](https://img.shields.io/gemnasium/johnnyt/predicator.svg)](https://gemnasium.com/johnnyt/predicator)
|
4
3
|
[![Coverage Status](https://coveralls.io/repos/github/predicator/predicator/badge.svg?branch=master)](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
|