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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5fb315c811b1933aee5a9e373019e46be91ea240
4
- data.tar.gz: f89434cbd64dcfb65ec62452839386175f530147
3
+ metadata.gz: b778c45c3885f3fd113cc8b71b20980774d3a694
4
+ data.tar.gz: be582dd047eb0d20e7380b1d7c5edd83d2d589c1
5
5
  SHA512:
6
- metadata.gz: 3e6698af9ad44e8a5c98bfdd7ee6d8d8b504129e37c9357c9845865334cfcedc5abfb577d198ca04de6ffc92d1dd5795f1950703c86e19ad625209f6cffe03a2
7
- data.tar.gz: fcbf8a75dc46c7040898fd598155b905907fc10ff3249e42c5e571ea47616d082458b6df02a4a2aa8046e2dda414a0bada7ed344b70894ee41c2f43c1cd47fcc
6
+ metadata.gz: 25f7894b6db4c564eb15d0c2ef2fb4fb785ca5a030abf930da4e8a4eb6b3d3b591e70298cb256373ca7e52e4b62290b40858e66f0c37924296ce8a2f628146bc
7
+ data.tar.gz: bbb3843e12ec3d86f7ec27279ecf7eae7a8962fb47f88b59cd9521095eb89f26dccee8cbcc1cf62190139ea88f9eaf8c73e2f6d614dfe14b6eb2d612219cd7af
@@ -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
- Example usage:
11
+ Simple usage:
13
12
 
14
13
  ```ruby
15
14
  require "predicator"
16
15
 
17
- Predicator.evaluate "age > 21" # false
16
+ Predicator.evaluate "score > 600 or (score > 580 and income > 9000)", score: 590 # false
18
17
 
19
- Predicator.evaluate "age > 21", age: 10 # false
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
- Predicator.evaluate "age > 21", age: 50 # true
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
 
@@ -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 Array < Unary
67
- def type; :ARRAY; end
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 Equal < Binary
90
- def type; :EQ; end
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 GreaterThan < Binary
94
- def type; :GT; end
129
+ class DateLessThan < Binary
130
+ def type; :DATLT; end
95
131
  end
96
132
 
97
- class LessThan < Binary
98
- def type; :LT; end
133
+ class IntegerIn < Binary
134
+ def type; :INTIN; end
99
135
  end
100
136
 
101
- class In < Binary
102
- def type; :IN; end
137
+ class StringIn < Binary
138
+ def type; :STRIN; end
103
139
  end
104
140
 
105
- class NotIn < Binary
106
- def type; :NOTIN; end
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 Between < Ternary
130
- def type; :BETWEEN; end
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
@@ -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
- stack.push !stack.pop
29
- when "jfalse"
30
- jump_if_false instruction.last
31
- when "jtrue"
32
- jump_if_true instruction.last
33
- when "lit", "array"
34
- stack.push instruction.last
35
- when "load"
36
- stack.push context[instruction.last]
37
- when "to_bool"
38
- stack.push !!stack.pop
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
@@ -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] }
@@ -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