predicate 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -0
- data/LICENSE.md +17 -19
- data/README.md +433 -0
- data/bin/g +2 -0
- data/lib/predicate.rb +15 -2
- data/lib/predicate/dsl.rb +138 -0
- data/lib/predicate/factory.rb +130 -33
- data/lib/predicate/grammar.rb +11 -2
- data/lib/predicate/grammar.sexp.yml +29 -0
- data/lib/predicate/nodes/${op_name}.rb.jeny +12 -0
- data/lib/predicate/nodes/binary_func.rb +20 -0
- data/lib/predicate/nodes/contradiction.rb +2 -7
- data/lib/predicate/nodes/dyadic_comp.rb +1 -3
- data/lib/predicate/nodes/empty.rb +14 -0
- data/lib/predicate/nodes/eq.rb +1 -3
- data/lib/predicate/nodes/expr.rb +1 -3
- data/lib/predicate/nodes/has_size.rb +14 -0
- data/lib/predicate/nodes/identifier.rb +1 -3
- data/lib/predicate/nodes/in.rb +2 -6
- data/lib/predicate/nodes/intersect.rb +3 -23
- data/lib/predicate/nodes/literal.rb +1 -3
- data/lib/predicate/nodes/match.rb +1 -21
- data/lib/predicate/nodes/nadic_bool.rb +1 -3
- data/lib/predicate/nodes/native.rb +1 -3
- data/lib/predicate/nodes/not.rb +1 -3
- data/lib/predicate/nodes/opaque.rb +1 -3
- data/lib/predicate/nodes/qualified_identifier.rb +1 -3
- data/lib/predicate/nodes/set_op.rb +26 -0
- data/lib/predicate/nodes/subset.rb +11 -0
- data/lib/predicate/nodes/superset.rb +11 -0
- data/lib/predicate/nodes/tautology.rb +2 -7
- data/lib/predicate/nodes/unary_func.rb +16 -0
- data/lib/predicate/nodes/var.rb +46 -0
- data/lib/predicate/processors/qualifier.rb +4 -0
- data/lib/predicate/processors/renamer.rb +4 -0
- data/lib/predicate/processors/to_s.rb +28 -0
- data/lib/predicate/processors/unqualifier.rb +4 -0
- data/lib/predicate/sequel/to_sequel.rb +3 -0
- data/lib/predicate/sugar.rb +47 -0
- data/lib/predicate/version.rb +1 -1
- data/spec/dsl/test_dsl.rb +204 -0
- data/spec/dsl/test_evaluate.rb +65 -0
- data/spec/dsl/test_respond_to_missing.rb +35 -0
- data/spec/dsl/test_to_skake_case.rb +38 -0
- data/spec/factory/shared/a_comparison_factory_method.rb +1 -0
- data/spec/factory/test_${op_name}.rb.jeny +12 -0
- data/spec/factory/test_empty.rb +11 -0
- data/spec/factory/test_has_size.rb +11 -0
- data/spec/factory/test_match.rb +1 -0
- data/spec/factory/test_set_ops.rb +18 -0
- data/spec/factory/test_var.rb +22 -0
- data/spec/factory/test_vars.rb +27 -0
- data/spec/nodes/${op_name}.jeny/test_evaluate.rb.jeny +19 -0
- data/spec/nodes/empty/test_evaluate.rb +42 -0
- data/spec/nodes/has_size/test_evaluate.rb +44 -0
- data/spec/predicate/test_and_split.rb +18 -0
- data/spec/predicate/test_attr_split.rb +18 -0
- data/spec/predicate/test_constant_variables.rb +24 -2
- data/spec/predicate/test_constants.rb +24 -0
- data/spec/predicate/test_evaluate.rb +205 -3
- data/spec/predicate/test_to_s.rb +37 -0
- data/spec/sequel/test_to_sequel.rb +16 -0
- data/spec/shared/a_predicate.rb +30 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/test_predicate.rb +68 -33
- data/spec/test_readme.rb +80 -0
- data/spec/test_sugar.rb +48 -0
- data/tasks/test.rake +3 -3
- metadata +40 -12
- data/spec/factory/test_between.rb +0 -12
- data/spec/factory/test_intersect.rb +0 -12
@@ -57,6 +57,14 @@ class Predicate
|
|
57
57
|
"#{apply(sexpr.identifier)} INTERSECTS #{to_literal(sexpr.values)}"
|
58
58
|
end
|
59
59
|
|
60
|
+
def on_subset(sexpr)
|
61
|
+
"#{apply(sexpr.identifier)} IS SUBSET OF #{to_literal(sexpr.values)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_superset(sexpr)
|
65
|
+
"#{apply(sexpr.identifier)} IS SUPERSET OF #{to_literal(sexpr.values)}"
|
66
|
+
end
|
67
|
+
|
60
68
|
def on_literal(sexpr)
|
61
69
|
to_literal(sexpr.last)
|
62
70
|
end
|
@@ -69,16 +77,36 @@ class Predicate
|
|
69
77
|
"#{apply(sexpr.left)} =~ #{apply(sexpr.right)}"
|
70
78
|
end
|
71
79
|
|
80
|
+
def on_empty(sexpr)
|
81
|
+
"EMPTY(#{commalist(sexpr.body)})"
|
82
|
+
end
|
83
|
+
|
84
|
+
def on_size(sexpr)
|
85
|
+
"SIZE(#{commalist(sexpr.body)})"
|
86
|
+
end
|
87
|
+
|
88
|
+
#jeny(predicate) def on_${op_name}(sexpr)
|
89
|
+
#jeny(predicate) "${OP_NAME}(#{commalist(sexpr.body)})"
|
90
|
+
#jeny(predicate) end
|
91
|
+
|
72
92
|
def on_native(sexpr)
|
73
93
|
sexpr.last.inspect
|
74
94
|
end
|
75
95
|
|
96
|
+
def on_var(sexpr)
|
97
|
+
"#{sexpr.semantics}(#{sexpr.formaldef})"
|
98
|
+
end
|
99
|
+
|
76
100
|
def on_missing(sexpr)
|
77
101
|
raise "Unimplemented: #{sexpr.first}"
|
78
102
|
end
|
79
103
|
|
80
104
|
protected
|
81
105
|
|
106
|
+
def commalist(of)
|
107
|
+
of.map{|o| apply(o) }.join(',')
|
108
|
+
end
|
109
|
+
|
82
110
|
def to_literal(x)
|
83
111
|
case x
|
84
112
|
when Placeholder then "$#{x.object_id}"
|
@@ -112,8 +112,11 @@ class Predicate
|
|
112
112
|
def on_unsupported(sexpr)
|
113
113
|
raise NotSupportedError, "Unsupported predicate #{sexpr}"
|
114
114
|
end
|
115
|
+
alias :on_var :on_unsupported
|
115
116
|
alias :on_native :on_unsupported
|
116
117
|
alias :on_intersect :on_unsupported
|
118
|
+
alias :on_subset :on_unsupported
|
119
|
+
alias :on_superset :on_unsupported
|
117
120
|
end
|
118
121
|
include Methods
|
119
122
|
end # class ToSequel
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Predicate
|
2
|
+
module Sugar
|
3
|
+
|
4
|
+
[ :eq, :neq, :lt, :lte, :gt, :gte ].each do |m|
|
5
|
+
define_method(m) do |left, right=nil|
|
6
|
+
return comp(m, left) if TupleLike===left && right.nil?
|
7
|
+
super(left, right)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def between(middle, lower_bound, upper_bound)
|
12
|
+
_factor_predicate [:and, [:gte, sexpr(middle), sexpr(lower_bound)],
|
13
|
+
[:lte, sexpr(middle), sexpr(upper_bound)]]
|
14
|
+
end
|
15
|
+
|
16
|
+
def match(left, right, options = nil)
|
17
|
+
super(left, right, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def min_size(left, right)
|
21
|
+
unless right.is_a?(Integer)
|
22
|
+
raise ArgumentError, "Integer expected, got #{right}"
|
23
|
+
end
|
24
|
+
if RUBY_VERSION >= "2.6"
|
25
|
+
has_size(left, Range.new(right,nil))
|
26
|
+
else
|
27
|
+
has_size(left, Range.new(right,(2**32-1)))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def max_size(left, right)
|
32
|
+
unless right.is_a?(Integer)
|
33
|
+
raise ArgumentError, "Integer expected, got #{right}"
|
34
|
+
end
|
35
|
+
has_size(left, 0..right)
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_null(operand)
|
39
|
+
eq(operand, nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
#jeny(sugar) def ${op_name}(*args)
|
43
|
+
#jeny(sugar) TODO
|
44
|
+
#jeny(sugar) end
|
45
|
+
|
46
|
+
end # module Sugar
|
47
|
+
end # class Predicate
|
data/lib/predicate/version.rb
CHANGED
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Predicate
|
4
|
+
describe Dsl do
|
5
|
+
subject{
|
6
|
+
Predicate::Dsl.new(:x).instance_eval(&bl)
|
7
|
+
}
|
8
|
+
|
9
|
+
context 'when used on a comparison op' do
|
10
|
+
context 'curried' do
|
11
|
+
let(:bl){
|
12
|
+
Proc.new{ eq(6) }
|
13
|
+
}
|
14
|
+
|
15
|
+
it { expect(subject).to eq(Predicate.eq(:x, 6))}
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'non curried' do
|
19
|
+
let(:bl){
|
20
|
+
Proc.new{ eq(:y, 6) }
|
21
|
+
}
|
22
|
+
|
23
|
+
it { expect(subject).to eq(Predicate.eq(:y, 6))}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when used on between' do
|
28
|
+
context 'curried' do
|
29
|
+
let(:bl){
|
30
|
+
Proc.new{ between(2, 6) }
|
31
|
+
}
|
32
|
+
|
33
|
+
it { expect(subject).to eq(Predicate.gte(:x, 2) & Predicate.lte(:x, 6))}
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'non curried' do
|
37
|
+
let(:bl){
|
38
|
+
Proc.new{ between(:y, 2, 6) }
|
39
|
+
}
|
40
|
+
|
41
|
+
it { expect(subject).to eq(Predicate.gte(:y, 2) & Predicate.lte(:y, 6))}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when used on a sugar op' do
|
46
|
+
context 'curried' do
|
47
|
+
let(:bl){
|
48
|
+
Proc.new{ min_size(6) }
|
49
|
+
}
|
50
|
+
|
51
|
+
it { expect(subject).to eq(Predicate.has_size(:x, Range.new(6, nil)))}
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'curried' do
|
55
|
+
let(:bl){
|
56
|
+
Proc.new{ min_size(:y, 6) }
|
57
|
+
}
|
58
|
+
|
59
|
+
it { expect(subject).to eq(Predicate.has_size(:y, Range.new(6, nil)))}
|
60
|
+
end
|
61
|
+
end if RUBY_VERSION >= "2.6"
|
62
|
+
|
63
|
+
context 'when used on match' do
|
64
|
+
context 'curryied' do
|
65
|
+
let(:bl){
|
66
|
+
Proc.new{ match(/a-z/) }
|
67
|
+
}
|
68
|
+
|
69
|
+
it { expect(subject).to eq(Predicate.match(:x, /a-z/, {}))}
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'curryied with options' do
|
73
|
+
let(:bl){
|
74
|
+
Proc.new{ match(/a-z/, {case_sensitive: false}) }
|
75
|
+
}
|
76
|
+
|
77
|
+
it { expect(subject).to eq(Predicate.match(:x, /a-z/, {case_sensitive: false}))}
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'non curried' do
|
81
|
+
let(:bl){
|
82
|
+
Proc.new{ match(:y, /a-z/) }
|
83
|
+
}
|
84
|
+
|
85
|
+
it { expect(subject).to eq(Predicate.match(:y, /a-z/, {}))}
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'non curried with options' do
|
89
|
+
let(:bl){
|
90
|
+
Proc.new{ match(:y, /a-z/, {case_sensitive: false}) }
|
91
|
+
}
|
92
|
+
|
93
|
+
it { expect(subject).to eq(Predicate.match(:y, /a-z/, {case_sensitive: false}))}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'when used on size' do
|
98
|
+
context 'curried' do
|
99
|
+
let(:bl){
|
100
|
+
Proc.new{ size(1..10) }
|
101
|
+
}
|
102
|
+
|
103
|
+
it { expect(subject).to eq(Predicate.has_size(:x, 1..10))}
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'curried with Integer' do
|
107
|
+
let(:bl){
|
108
|
+
Proc.new{ size(10) }
|
109
|
+
}
|
110
|
+
|
111
|
+
it { expect(subject).to eq(Predicate.has_size(:x, 10))}
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'not curried' do
|
115
|
+
let(:bl){
|
116
|
+
Proc.new{ size(:y, 1..10) }
|
117
|
+
}
|
118
|
+
|
119
|
+
it { expect(subject).to eq(Predicate.has_size(:y, 1..10))}
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'not curried with Integer' do
|
123
|
+
let(:bl){
|
124
|
+
Proc.new{ size(:y, 10) }
|
125
|
+
}
|
126
|
+
|
127
|
+
it { expect(subject).to eq(Predicate.has_size(:y, 10))}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'when used with some camelCase' do
|
132
|
+
context 'curried' do
|
133
|
+
let(:bl){
|
134
|
+
Proc.new{ hasSize(1..10) }
|
135
|
+
}
|
136
|
+
|
137
|
+
it { expect(subject).to eq(Predicate.has_size(:x, 1..10))}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'when used with full text versions and their negation' do
|
142
|
+
context 'less_than' do
|
143
|
+
let(:bl){
|
144
|
+
Proc.new{ less_than(:x, 1) }
|
145
|
+
}
|
146
|
+
|
147
|
+
it { expect(subject).to eq(Predicate.lt(:x, 1))}
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'lessThan' do
|
151
|
+
let(:bl){
|
152
|
+
Proc.new{ lessThan(:x, 1) }
|
153
|
+
}
|
154
|
+
|
155
|
+
it { expect(subject).to eq(Predicate.lt(:x, 1))}
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'notLessThan' do
|
159
|
+
let(:bl){
|
160
|
+
Proc.new{ notLessThan(:x, 1) }
|
161
|
+
}
|
162
|
+
|
163
|
+
it { expect(subject).to eq(!Predicate.lt(:x, 1))}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'when used with a negated form' do
|
168
|
+
context 'curried' do
|
169
|
+
let(:bl){
|
170
|
+
Proc.new{ notSize(1..10) }
|
171
|
+
}
|
172
|
+
|
173
|
+
it { expect(subject).to eq(!Predicate.has_size(:x, 1..10))}
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'when used on null' do
|
178
|
+
context 'curried' do
|
179
|
+
let(:bl){
|
180
|
+
Proc.new{ null() }
|
181
|
+
}
|
182
|
+
|
183
|
+
it { expect(subject).to eq(Predicate.eq(:x, nil))}
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'negated' do
|
187
|
+
let(:bl){
|
188
|
+
Proc.new{ notNull() }
|
189
|
+
}
|
190
|
+
|
191
|
+
it { expect(subject).to eq(Predicate.neq(:x, nil))}
|
192
|
+
end
|
193
|
+
|
194
|
+
context 'not curried' do
|
195
|
+
let(:bl){
|
196
|
+
Proc.new{ null(:y) }
|
197
|
+
}
|
198
|
+
|
199
|
+
it { expect(subject).to eq(Predicate.eq(:y, nil))}
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Predicate in a curried form' do
|
4
|
+
|
5
|
+
context 'with a variable name' do
|
6
|
+
it 'supports tautology, contradiction' do
|
7
|
+
p = Predicate.currying(:x){ tautology }
|
8
|
+
expect(p).to eql(Predicate.tautology)
|
9
|
+
|
10
|
+
p = Predicate.currying(:x){ contradiction }
|
11
|
+
expect(p).to eql(Predicate.contradiction)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'support comparison operators' do
|
15
|
+
p = Predicate.currying(:x){
|
16
|
+
gt(0) & lt(12)
|
17
|
+
}
|
18
|
+
expect(p.evaluate(x: 0)).to be_falsy
|
19
|
+
expect(p.evaluate(x: 1)).to be_truthy
|
20
|
+
expect(p.evaluate(x: 11)).to be_truthy
|
21
|
+
expect(p.evaluate(x: 12)).to be_falsy
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'supports between shortcut' do
|
25
|
+
p = Predicate.currying(:x){
|
26
|
+
between(1, 11)
|
27
|
+
}
|
28
|
+
expect(p.evaluate(x: 0)).to be_falsy
|
29
|
+
expect(p.evaluate(x: 1)).to be_truthy
|
30
|
+
expect(p.evaluate(x: 11)).to be_truthy
|
31
|
+
expect(p.evaluate(x: 12)).to be_falsy
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'supports match' do
|
35
|
+
p = Predicate.currying(:x){
|
36
|
+
match(/a/)
|
37
|
+
}
|
38
|
+
expect(p.evaluate(x: "zzz")).to be_falsy
|
39
|
+
expect(p.evaluate(x: "abc")).to be_truthy
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'without a variable name' do
|
44
|
+
it 'applies to the object passed' do
|
45
|
+
p = Predicate.currying{
|
46
|
+
gt(0) & lt(12)
|
47
|
+
}
|
48
|
+
expect(p.evaluate(0)).to be_falsy
|
49
|
+
expect(p.evaluate(1)).to be_truthy
|
50
|
+
expect(p.evaluate(11)).to be_truthy
|
51
|
+
expect(p.evaluate(12)).to be_falsy
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'on shortcuts' do
|
56
|
+
it 'applies to the object passed' do
|
57
|
+
p = Predicate.currying{
|
58
|
+
min_size(5)
|
59
|
+
}
|
60
|
+
expect(p.evaluate("1")).to be_falsy
|
61
|
+
expect(p.evaluate("013456789")).to be_truthy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Predicate
|
4
|
+
describe Dsl do
|
5
|
+
subject{
|
6
|
+
Predicate::Dsl.new(:x)
|
7
|
+
}
|
8
|
+
|
9
|
+
it 'respond to negated forms' do
|
10
|
+
[
|
11
|
+
:notSize,
|
12
|
+
:notEq
|
13
|
+
].each do |m|
|
14
|
+
expect(subject.send(:respond_to_missing?, m)).to eql(true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'respond to camelCased forms' do
|
19
|
+
[
|
20
|
+
:hasSize
|
21
|
+
].each do |m|
|
22
|
+
expect(subject.send(:respond_to_missing?, m)).to eql(true)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'respond false otherwise' do
|
27
|
+
[
|
28
|
+
:nosuchone,
|
29
|
+
:notsuchone
|
30
|
+
].each do |m|
|
31
|
+
expect(subject.send(:respond_to_missing?, m)).to eql(false)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Predicate
|
4
|
+
describe Dsl, "to_snake_case" do
|
5
|
+
|
6
|
+
def snake(str)
|
7
|
+
Dsl.new.__send__(:to_snake_case, str)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'works on snake case already' do
|
11
|
+
{
|
12
|
+
"snake" => "snake",
|
13
|
+
"snake_case" => "snake_case"
|
14
|
+
}.each_pair do |k,v|
|
15
|
+
expect(snake(k)).to eq(v)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'works on camelCase already' do
|
20
|
+
{
|
21
|
+
"camelCase" => "camel_case",
|
22
|
+
"theCamelCase" => "the_camel_case"
|
23
|
+
}.each_pair do |k,v|
|
24
|
+
expect(snake(k)).to eq(v)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'works on PascalCase already' do
|
29
|
+
{
|
30
|
+
"PascalCase" => "pascal_case",
|
31
|
+
"ThePascalCase" => "the_pascal_case"
|
32
|
+
}.each_pair do |k,v|
|
33
|
+
expect(snake(k)).to eq(v)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|