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 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