predicate 2.3.2 → 2.6.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/Gemfile +4 -0
- data/LICENSE.md +17 -19
- data/README.md +435 -0
- data/bin/g +2 -0
- data/lib/predicate/dsl.rb +138 -0
- data/lib/predicate/factory.rb +142 -37
- 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/and.rb +9 -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 +11 -3
- data/lib/predicate/nodes/expr.rb +9 -3
- data/lib/predicate/nodes/has_size.rb +14 -0
- data/lib/predicate/nodes/identifier.rb +1 -3
- data/lib/predicate/nodes/in.rb +7 -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 +6 -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 +21 -0
- data/lib/predicate/processors.rb +1 -0
- data/lib/predicate/sequel/to_sequel.rb +4 -1
- data/lib/predicate/sugar.rb +47 -0
- data/lib/predicate/version.rb +2 -2
- data/lib/predicate.rb +26 -2
- 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_comp.rb +28 -5
- 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_free_variables.rb +1 -1
- data/spec/predicate/test_to_hash.rb +40 -0
- data/spec/predicate/test_to_s.rb +37 -0
- data/spec/predicate/test_unqualify.rb +18 -0
- data/spec/sequel/test_to_sequel.rb +25 -0
- data/spec/shared/a_predicate.rb +30 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/test_predicate.rb +78 -33
- data/spec/test_readme.rb +80 -0
- data/spec/test_sugar.rb +48 -0
- data/tasks/test.rake +3 -3
- metadata +45 -14
- data/spec/factory/test_between.rb +0 -12
- data/spec/factory/test_intersect.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d0149b8d39635ed0f27c39362d1c4aec0dc5f2ccc1290580ed67edf18414e5b
|
4
|
+
data.tar.gz: a623594513754521196f1a98f5d2f682650a3ec1b02b02b4dfa703600039faa1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfc8fdb19e1d61171a4c91e893bfc6b6b91d559d8402f3a47dbede8d09dcb2f702f697cbd9e8b6db688e896d1831f837ace44d8c78b8c0e110d767ac0f99db05
|
7
|
+
data.tar.gz: 21ffbda2c7831051ac8ffba6a5c84d029735ce953e3fc969ae252945b0417ea28bc05f865e0e4e29771b542ca2993ec3720389a7be012b96b378536fb0743602
|
data/Gemfile
CHANGED
data/LICENSE.md
CHANGED
@@ -1,22 +1,20 @@
|
|
1
|
-
|
1
|
+
Copyright (c) 2017-2020 - Enspirit SPRL (Bernard Lambeau)
|
2
2
|
|
3
|
-
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
4
10
|
|
5
|
-
|
6
|
-
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
11
|
-
the following conditions:
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,435 @@
|
|
1
|
+
# Predicate
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
Boolean (truth-value) expressions that can be evaluated, manipulated,
|
6
|
+
optimized, translated to code, etc.
|
7
|
+
|
8
|
+
## Example(s)
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# Let's build a simple predicate for 'x = 2 and not(y <= 3)'
|
12
|
+
p = Predicate.eq(:x, 2) & !Predicate.lte(:y, 3)
|
13
|
+
|
14
|
+
p.evaluate(:x => 2, :y => 6)
|
15
|
+
# => true
|
16
|
+
|
17
|
+
p.evaluate(:x => 2, :y => 3)
|
18
|
+
# => false
|
19
|
+
```
|
20
|
+
|
21
|
+
When building complex expressions, you can use the `dsl` method.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# This builds the same predicate
|
25
|
+
p = Predicate.dsl{
|
26
|
+
eq(:x, 2) & !lte(:y, 3)
|
27
|
+
}
|
28
|
+
```
|
29
|
+
|
30
|
+
The `dsl` block also have all predicates in camelCase, negated, and full text
|
31
|
+
variants:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
p = Predicate.dsl{
|
35
|
+
notEq(:x, "foo") & hasSize(:y, 1..10) & lessThan(:z, 3)
|
36
|
+
}
|
37
|
+
```
|
38
|
+
|
39
|
+
If you have complex expressions where many members apply to the same variable,
|
40
|
+
a `currying` dsl extension is provided. It allows using all `dsl` methods
|
41
|
+
while omitting their first argument.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# Instead of this
|
45
|
+
p = Predicate.gt(:x, 1) & Predicate.lt(:x, 10)
|
46
|
+
|
47
|
+
# or this
|
48
|
+
p = Predicate.dsl{
|
49
|
+
gt(:x, 1) & lt(:x, 10)
|
50
|
+
}
|
51
|
+
|
52
|
+
# do this
|
53
|
+
p = Predicate.currying(:x){
|
54
|
+
gt(1) & lt(10)
|
55
|
+
}
|
56
|
+
p.evaluate(:x => 6)
|
57
|
+
# => true
|
58
|
+
```
|
59
|
+
|
60
|
+
Predicate also works if you want to evaluate an expression on a single object
|
61
|
+
without having to introduce a variable like `:x`...
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
p = Predicate.currying{
|
65
|
+
gt(1) & lt(10)
|
66
|
+
}
|
67
|
+
p.evaluate(6)
|
68
|
+
# => true
|
69
|
+
```
|
70
|
+
|
71
|
+
... or, in contrast, if you want to evaluate boolean expressions over more
|
72
|
+
complex data structures that a flat Hash like `{:x => 6, ...}`
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
x, y = Predicate.vars("items.0.price", "items.1.price")
|
76
|
+
p = Predicate.eq(x, 6) & Predicate.lt(y, 10)
|
77
|
+
p.evaluate({
|
78
|
+
items: [
|
79
|
+
{ name: "Candy", price: 6 },
|
80
|
+
{ name: "Crush", price: 4 }
|
81
|
+
]
|
82
|
+
})
|
83
|
+
# => true
|
84
|
+
```
|
85
|
+
|
86
|
+
The following sections explain a) why we created this library, b) how to build
|
87
|
+
expressions, c) what operators are available, and d) how abstract variables
|
88
|
+
work and what features are supported when using them (because not all are).
|
89
|
+
|
90
|
+
## Rationale
|
91
|
+
|
92
|
+
This reusable library is used in various ruby gems developed and maintained
|
93
|
+
by Enspirit where boolean expressions are first-class citizen. It provides
|
94
|
+
a common API for expressing, evaluating, and manipulating them.
|
95
|
+
|
96
|
+
* [Bmg](https://github.com/enspirit/bmg)
|
97
|
+
* [Finitio](https://github.com/blambeau/finitio-rb)
|
98
|
+
* [Webspicy](https://github.com/enspirit/webspicy)
|
99
|
+
|
100
|
+
The library represents an expression as an AST internally. This allows for
|
101
|
+
subsequent manipulations & reasoning. Please check the `Predicate::Factory`
|
102
|
+
module for details.
|
103
|
+
|
104
|
+
Best-effort simplifications are also performed at construction and when
|
105
|
+
boolean logic is used (and, or, not). For instance, `eq(:x, 6) & eq(:x, 10)`
|
106
|
+
yields a `contradiction` predicate. There is currently no way to disable those
|
107
|
+
simplifications that were initially implemented for `Bmg`.
|
108
|
+
|
109
|
+
## Building expressions
|
110
|
+
|
111
|
+
The following list of operators is currently available.
|
112
|
+
|
113
|
+
### True and False
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Predicate.tautology # aka True
|
117
|
+
Predicate.contradiction # aka False
|
118
|
+
```
|
119
|
+
|
120
|
+
### Logical operators
|
121
|
+
|
122
|
+
For every valid Predicate instances `p` and `q`:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
p & q # Boolean conjunction
|
126
|
+
p | q # Boolean disjunction
|
127
|
+
!p # Boolean negation
|
128
|
+
```
|
129
|
+
|
130
|
+
### Comparison operators
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Predicate.eq(:x, 2) # x = 2
|
134
|
+
Predicate.eq(:x, :y) # x = y
|
135
|
+
Predicate.neq(:x, 2) # x != 2
|
136
|
+
Predicate.neq(:x, :y) # x != y
|
137
|
+
Predicate.lt(:x, 2) # x < 2
|
138
|
+
Predicate.lt(:x, :y) # x < y
|
139
|
+
Predicate.lte(:x, 2) # x <= 2
|
140
|
+
Predicate.lte(:x, :y) # x <= y
|
141
|
+
Predicate.gt(:x, 2) # x > 2
|
142
|
+
Predicate.gt(:x, :y) # x > y
|
143
|
+
Predicate.gte(:x, 2) # x >= 2
|
144
|
+
Predicate.gte(:x, :y) # x >= y
|
145
|
+
```
|
146
|
+
|
147
|
+
Shortcuts (translated immediately, no trace kept in AST) :
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Predicate.eq(x: 2, y: 6) # eq(:x, 2) & eq(:y, 6)
|
151
|
+
Predicate.eq(x: 2, y: :z) # eq(:x, 2) & eq(:y, :z)
|
152
|
+
# ... and so on for neq, lt, lte, gt, gte
|
153
|
+
|
154
|
+
Predicate.between(:x, l, h) # gte(:x, l) & lte(:x, h), for all l and h
|
155
|
+
Predicate.in(:x, 1..10) # gte(:x, 1) & lte(:x, 10)
|
156
|
+
Predicate.in(:x, 1...10) # gte(:x, 1) & lt(:x, 10)
|
157
|
+
#
|
158
|
+
|
159
|
+
Predicate.is_null(:x) # eq(:x, nil)
|
160
|
+
```
|
161
|
+
|
162
|
+
### Set-based operators
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Predicate.in(:x, [2, 4, 6]) # x ∈ {2, 4, 6}
|
166
|
+
Predicate.in(:x, :y) # x ∈ y
|
167
|
+
Predicate.intersect(:x, [2, 4, 6]) # x ∩ {2, 4, 6} ≠ ∅
|
168
|
+
Predicate.intersect(:x, :y) # x ∩ y ≠ ∅
|
169
|
+
Predicate.subset(:x, [2, 4, 6]) # x ⊆ {2, 4, 6}
|
170
|
+
Predicate.subset(:x, :y) # x ⊆ y
|
171
|
+
Predicate.superset(:x, [2, 4, 6]) # x ⊇ {2, 4, 6}
|
172
|
+
Predicate.superset(:x, :y) # x ⊇ y
|
173
|
+
```
|
174
|
+
|
175
|
+
### Other operators
|
176
|
+
|
177
|
+
The following operators have no clear mathematical semantics. Their semantics
|
178
|
+
depends on the underlying type system. Most are currently not supported outside
|
179
|
+
of ruby (e.g. SQL compilation). The documentation below applies to a Ruby usage.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
Predicate.match(:x, /abc/) # ruby's ===
|
183
|
+
Predicate.empty(:x) # ruby's empty?
|
184
|
+
Predicate.has_size(:x, 1..10) # ruby's size and ===
|
185
|
+
Predicate.has_size(:x, 10) # Same as has_size(:x, 10..10)
|
186
|
+
Predicate.has_size(:x, :y) # y must resolve to a Range or Integer
|
187
|
+
```
|
188
|
+
|
189
|
+
Shortcuts (translated immediately, no trace kept in AST) :
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
Predicate.min_size(:x, 10) # has_size(:x, 10..)
|
193
|
+
Predicate.max_size(:x, 10) # has_size(:x, 0..10)
|
194
|
+
```
|
195
|
+
|
196
|
+
### Native expressions
|
197
|
+
|
198
|
+
Ruby `Proc` can be used to capture complex predicates. Native predicates always
|
199
|
+
receive the top evaluation context as first argument.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
p = Predicate.native(->(t){
|
203
|
+
# t here is the {:x => 2, :y => 6} Hash below
|
204
|
+
Foo::Bar.call_to_ruby_code?(t)
|
205
|
+
})
|
206
|
+
p.evaluate(:x => 2, :y => 6)
|
207
|
+
```
|
208
|
+
|
209
|
+
Resulting predicates cannot be translated to, e.g. SQL, and typically prevent
|
210
|
+
optimizations and manipulations:
|
211
|
+
|
212
|
+
## Available operators
|
213
|
+
|
214
|
+
The following operators are available on predicates.
|
215
|
+
|
216
|
+
### Evaluate
|
217
|
+
|
218
|
+
`Predicate#evaluate` takes a Hash mapping each free variable to a value,
|
219
|
+
and returns the Boolean evaluation of the expression.
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
# Let's build a simple predicate for 'x = 2 and not(y <= 3)'
|
223
|
+
p = Predicate.eq(:x, 2) & !Predicate.lte(:y, 3)
|
224
|
+
|
225
|
+
p.evaluate(:x => 2, :y => 6)
|
226
|
+
# => true
|
227
|
+
```
|
228
|
+
|
229
|
+
### Rename
|
230
|
+
|
231
|
+
`Predicate#rename` allows renaming variables.
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
p = Predicate.eq(:x, 4) # x = 4
|
235
|
+
p = p.rename(:x => :z) # z = 4
|
236
|
+
```
|
237
|
+
|
238
|
+
### Bind
|
239
|
+
|
240
|
+
`Predicate#bind` allows late binding of placeholders to values.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
pl = Predicate.placeholder
|
244
|
+
p = Predicate.eq(:x, pl) # x = _
|
245
|
+
p = p.bind(pl, 5) # x = 5
|
246
|
+
p.evaluate(:x => 10)
|
247
|
+
# => false
|
248
|
+
```
|
249
|
+
|
250
|
+
### Quality & Unqualify
|
251
|
+
|
252
|
+
`Predicate#qualify` allows adding a qualifier to each variable, for
|
253
|
+
disambiguation when composing predicates from different contexts.
|
254
|
+
`Predicate#unqualify` does the opposite.
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
p = Predicate.eq(:x, 2) # x = 2
|
258
|
+
p.qualify(:t) # t.x = 2
|
259
|
+
p.unqualify # x = 2
|
260
|
+
```
|
261
|
+
|
262
|
+
Qualify accepts a Hash to use different qualifiers for variables.
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
p = Predicate.eq(x: 2, y: 4) # x = 2 & y = 4
|
266
|
+
p.qualify(:x => :t, :y => :s) # t.x = 2 & s.y = 4
|
267
|
+
```
|
268
|
+
|
269
|
+
### And split
|
270
|
+
|
271
|
+
`Predicate#and_split` split a predicate `p` as two predicates `p1` and `p2`
|
272
|
+
so that `p <=> p1 & p2` and `p2` makes no reference to any variable of the
|
273
|
+
given list.
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
p = Predicate.eq(x: 2, y: 4) # x = 2 & y = 4
|
277
|
+
p1, p2 = p.and_split([:x]) # p1 is x = 2 ; p2 is y = 4
|
278
|
+
```
|
279
|
+
|
280
|
+
Observe that `and_split` is always possible but may degenerate to an
|
281
|
+
uninteresting `p2`, typically when disjunctions are used. For instance,
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
p = Predicate.eq(x: 2) | Predicate.eq(y: 4) # x = 2 | y = 4
|
285
|
+
p1, p2 = p.and_split([:x]) # p1 is x = 2 | y = 4 ; p2 is true
|
286
|
+
```
|
287
|
+
|
288
|
+
### Attr split
|
289
|
+
|
290
|
+
`Predicate#attr_split` can be used to split a predicate `p` as n+1 predicates
|
291
|
+
`p1, p2, ..., pn, pz`, such that `p <=> p1 & p2 & ... & pn & pz`. Each
|
292
|
+
predicate `pi` makes references to variable `i` only, except `pz` which can
|
293
|
+
reference all of them.
|
294
|
+
|
295
|
+
The result is a Hash mapping each variable to its predicate. A `nil` key maps
|
296
|
+
to `pz`.
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
p = Predicate.eq(x: 2, y: 4) # x = 2 & y = 4
|
300
|
+
split = p.attr_split
|
301
|
+
# => {
|
302
|
+
# :x => Predicate.eq(:x, 2),
|
303
|
+
# :y => Predicate.eq(:y, 4)
|
304
|
+
# }
|
305
|
+
```
|
306
|
+
|
307
|
+
## Working with abstract variables
|
308
|
+
|
309
|
+
WARNING: this `var` feature is only compatible with `Predicate#evaluate`
|
310
|
+
and `Predicate#bind` so far. Other operators have not been tested and may fail
|
311
|
+
in unexpected ways or raise a NotImplementedError. Also, predicates using
|
312
|
+
abstract variables are not properly translated to e.g. SQL.
|
313
|
+
|
314
|
+
By default, Predicate expects variable identifiers to be represented by
|
315
|
+
ruby Symbols. `#evaluate` then takes a mapping between variables and values as
|
316
|
+
a Hash:
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
# :x and :y are variable identifiers
|
320
|
+
p = Predicate.eq(:x, 2) & !Predicate.lte(:y, 3)
|
321
|
+
|
322
|
+
# the Hash below is a mapping between variables and values
|
323
|
+
p.evaluate(:x => 2, :y => 6)
|
324
|
+
# => true
|
325
|
+
```
|
326
|
+
|
327
|
+
There are situations where you would like variables to be kept simple in
|
328
|
+
expressions while evaluating the latter on complex data structures.
|
329
|
+
|
330
|
+
`Predicate#var` can be used as an abstraction mechanism in such cases.
|
331
|
+
It takes a variable definition as first argument and a semantics as second.
|
332
|
+
The semantics defines how a value is extracted when the variable value must
|
333
|
+
be evaluated.
|
334
|
+
|
335
|
+
Supported protocols are `:dig`, `:send` and `:public_send`. Only `:dig`
|
336
|
+
must be considered safe while the two other ones used with great care.
|
337
|
+
|
338
|
+
* `:dig` relies on Ruby's `dig` protocol introduced in Ruby 2.3. It
|
339
|
+
will work out of the box with Hash, Array, Struct, OpenStruct and
|
340
|
+
more generally any object responding to `:dig`:
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
xyz = Predicate.var([:x, :y, :z], :dig)
|
344
|
+
p = Predicate.eq(xyz, 2)
|
345
|
+
p.evaluate({ :x => { :y => { :z => 2 } } })
|
346
|
+
# => true
|
347
|
+
```
|
348
|
+
|
349
|
+
When using `:dig` the variable definition can be passed as a String
|
350
|
+
that will be automatically decomposed for you. Variable names are
|
351
|
+
transformed to Symbols and integer literals to Integers. You must
|
352
|
+
use the explicit version above if you don't want those conversions.
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
# this
|
356
|
+
Predicate.var("x.0.y", :dig)
|
357
|
+
|
358
|
+
# is equivalent to
|
359
|
+
Predicate.var([:x, 0, :y], :dig)
|
360
|
+
```
|
361
|
+
|
362
|
+
* `:send` relies on Ruby's `__send__` method and is generally less
|
363
|
+
safe if variable definitions are not strictly controlled. But it
|
364
|
+
allows evaluating predicates over any data structure made of pure
|
365
|
+
ruby objects:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
class C
|
369
|
+
attr_reader :x
|
370
|
+
def initialize(x)
|
371
|
+
@x = x
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
xy = Predicate.var([:x, :y], :send)
|
376
|
+
p = Predicate.eq(xy, 2)
|
377
|
+
p.evaluate(C.new(OpenStruct.new(y: 2)))
|
378
|
+
# => true
|
379
|
+
```
|
380
|
+
|
381
|
+
The variable can similarly be passed as a dotted String that will be
|
382
|
+
decomposed as a sequence of Symbols.
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
xy = Predicate.var("x.y", :send)
|
386
|
+
p = Predicate.eq(xy, 2)
|
387
|
+
p.evaluate(C.new(OpenStruct.new(y: 2)))
|
388
|
+
# => true
|
389
|
+
```
|
390
|
+
|
391
|
+
* `:public_send` is similar to `:send` but slightly safer as it only
|
392
|
+
allows calling Ruby's public methods.
|
393
|
+
|
394
|
+
## Public API
|
395
|
+
|
396
|
+
This library follows semantics versioning 2.0. Its public API is:
|
397
|
+
|
398
|
+
* Class methods of the `Predicate` class, such as those covered in the
|
399
|
+
"Building expressions" section above.
|
400
|
+
|
401
|
+
* DSL methods contributed by `Predicate::Factory`, `Predicate::Sugar`,
|
402
|
+
and `Predicate::Dsl` modules ; including dynamic ones (negation,
|
403
|
+
camelCase, etc.)
|
404
|
+
|
405
|
+
* Instance methods of the `Predicate` class, such as those covered in the
|
406
|
+
"Available operators" section above.
|
407
|
+
|
408
|
+
* Instance and class methods contributed by plugins (e.g. `predicate/sequel`).
|
409
|
+
|
410
|
+
* Exception classes: `Predicate::NotSupportedError`,
|
411
|
+
`Predicate::UnboundError` and `Predicate::TypeError`.
|
412
|
+
|
413
|
+
The AST representation of predicate expressions is NOT part of the public API.
|
414
|
+
We bump the minor version of the library when it changes, though.
|
415
|
+
|
416
|
+
Everything else is condidered private and may change any time (i.e. on patch
|
417
|
+
releases).
|
418
|
+
|
419
|
+
## Contributing
|
420
|
+
|
421
|
+
Please use github issues and pull requests, and favor the latter if possible.
|
422
|
+
|
423
|
+
This repository uses the help of [jeny](https://github.com/enspirit/jeny) to
|
424
|
+
generate code snippets when adding new predicates. It supports `predicate`
|
425
|
+
and `sugar` snippets and add code to be completed in various places:
|
426
|
+
|
427
|
+
```
|
428
|
+
bundle exec jeny s predicate -d op_name:my_predicate -d arity:unary
|
429
|
+
bundle exec jeny s sugar -d op_name:my_shortcut
|
430
|
+
```
|
431
|
+
|
432
|
+
## Licence
|
433
|
+
|
434
|
+
This software is distributed by Enspirit SRL under a MIT Licence. Please
|
435
|
+
contact Bernard Lambeau (blambeau@gmail.com) with any question.
|
data/bin/g
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
class Predicate
|
2
|
+
class Dsl
|
3
|
+
|
4
|
+
def initialize(var = nil, allow_currying = true)
|
5
|
+
@var = var || ::Predicate.var(".", :dig)
|
6
|
+
@allow_currying = allow_currying
|
7
|
+
end
|
8
|
+
|
9
|
+
public # No injection
|
10
|
+
|
11
|
+
[
|
12
|
+
:tautology,
|
13
|
+
:contradiction,
|
14
|
+
:literal,
|
15
|
+
:var,
|
16
|
+
:vars,
|
17
|
+
:identifier,
|
18
|
+
:qualified_identifier,
|
19
|
+
:placeholder
|
20
|
+
].each do |name|
|
21
|
+
define_method(name) do |*args|
|
22
|
+
::Predicate.send(name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
public # All normal
|
27
|
+
|
28
|
+
[
|
29
|
+
:in,
|
30
|
+
:intersect,
|
31
|
+
:subset,
|
32
|
+
:superset,
|
33
|
+
#
|
34
|
+
:eq,
|
35
|
+
:neq,
|
36
|
+
:lt,
|
37
|
+
:lte,
|
38
|
+
:gt,
|
39
|
+
:gte,
|
40
|
+
#
|
41
|
+
:empty,
|
42
|
+
:has_size,
|
43
|
+
#jeny(predicate) :${op_name},
|
44
|
+
].each do |name|
|
45
|
+
define_method(name) do |*args|
|
46
|
+
args = apply_curry(name, args, Factory)
|
47
|
+
::Predicate.send(name, *args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
public # Operators with options as last arg
|
52
|
+
|
53
|
+
[
|
54
|
+
:match
|
55
|
+
].each do |name|
|
56
|
+
define_method(name) do |*args|
|
57
|
+
args << {} unless args.last.is_a?(::Hash)
|
58
|
+
args = apply_curry(name, args, ::Predicate::Factory)
|
59
|
+
::Predicate.send(name, *args)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
public # Sugar operators
|
64
|
+
|
65
|
+
[
|
66
|
+
:between,
|
67
|
+
:min_size,
|
68
|
+
:max_size,
|
69
|
+
:is_null,
|
70
|
+
#jeny(sugar) :${op_name},
|
71
|
+
].each do |name|
|
72
|
+
define_method(name) do |*args|
|
73
|
+
args = apply_curry(name, args, ::Predicate::Sugar)
|
74
|
+
::Predicate.send(name, *args)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
public # Extra names
|
79
|
+
|
80
|
+
{
|
81
|
+
:null => :is_null,
|
82
|
+
:size => :has_size,
|
83
|
+
:equal => :eq,
|
84
|
+
:less_than => :lt,
|
85
|
+
:less_than_or_equal => :lte,
|
86
|
+
:greater_than => :gt,
|
87
|
+
:greater_than_or_equal => :gte
|
88
|
+
}.each_pair do |k,v|
|
89
|
+
define_method(k) do |*args|
|
90
|
+
__send__(v, *args)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
public
|
95
|
+
|
96
|
+
def method_missing(n, *args, &bl)
|
97
|
+
snaked, to_negate = missing_method_pair(n)
|
98
|
+
if snaked == n.to_s && !to_negate
|
99
|
+
super
|
100
|
+
elsif self.respond_to?(snaked)
|
101
|
+
got = __send__(snaked.to_sym, *args, &bl)
|
102
|
+
to_negate ? !got : got
|
103
|
+
else
|
104
|
+
super
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def respond_to_missing?(n, include_private = false)
|
109
|
+
snaked, to_negate = missing_method_pair(n)
|
110
|
+
return super if snaked == n.to_s
|
111
|
+
self.respond_to?(snaked)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def missing_method_pair(n)
|
117
|
+
name, to_negate = n.to_s, false
|
118
|
+
if name.to_s[0..2] == "not"
|
119
|
+
name, to_negate = name[3..-1], true
|
120
|
+
end
|
121
|
+
[to_snake_case(name), to_negate]
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_snake_case(str)
|
125
|
+
str.gsub(/[A-Z]/){|x| "_#{x.downcase}" }.gsub(/^_/, "")
|
126
|
+
end
|
127
|
+
|
128
|
+
def apply_curry(name, args, on)
|
129
|
+
m = on.instance_method(name)
|
130
|
+
if @allow_currying and m.arity == 1+args.length
|
131
|
+
[@var] + args
|
132
|
+
else
|
133
|
+
args
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end # class Dsl
|
138
|
+
end # class Predicate
|