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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -0
  3. data/LICENSE.md +17 -19
  4. data/README.md +435 -0
  5. data/bin/g +2 -0
  6. data/lib/predicate/dsl.rb +138 -0
  7. data/lib/predicate/factory.rb +142 -37
  8. data/lib/predicate/grammar.rb +11 -2
  9. data/lib/predicate/grammar.sexp.yml +29 -0
  10. data/lib/predicate/nodes/${op_name}.rb.jeny +12 -0
  11. data/lib/predicate/nodes/and.rb +9 -0
  12. data/lib/predicate/nodes/binary_func.rb +20 -0
  13. data/lib/predicate/nodes/contradiction.rb +2 -7
  14. data/lib/predicate/nodes/dyadic_comp.rb +1 -3
  15. data/lib/predicate/nodes/empty.rb +14 -0
  16. data/lib/predicate/nodes/eq.rb +11 -3
  17. data/lib/predicate/nodes/expr.rb +9 -3
  18. data/lib/predicate/nodes/has_size.rb +14 -0
  19. data/lib/predicate/nodes/identifier.rb +1 -3
  20. data/lib/predicate/nodes/in.rb +7 -6
  21. data/lib/predicate/nodes/intersect.rb +3 -23
  22. data/lib/predicate/nodes/literal.rb +1 -3
  23. data/lib/predicate/nodes/match.rb +1 -21
  24. data/lib/predicate/nodes/nadic_bool.rb +1 -3
  25. data/lib/predicate/nodes/native.rb +1 -3
  26. data/lib/predicate/nodes/not.rb +1 -3
  27. data/lib/predicate/nodes/opaque.rb +1 -3
  28. data/lib/predicate/nodes/qualified_identifier.rb +1 -3
  29. data/lib/predicate/nodes/set_op.rb +26 -0
  30. data/lib/predicate/nodes/subset.rb +11 -0
  31. data/lib/predicate/nodes/superset.rb +11 -0
  32. data/lib/predicate/nodes/tautology.rb +6 -7
  33. data/lib/predicate/nodes/unary_func.rb +16 -0
  34. data/lib/predicate/nodes/var.rb +46 -0
  35. data/lib/predicate/processors/qualifier.rb +4 -0
  36. data/lib/predicate/processors/renamer.rb +4 -0
  37. data/lib/predicate/processors/to_s.rb +28 -0
  38. data/lib/predicate/processors/unqualifier.rb +21 -0
  39. data/lib/predicate/processors.rb +1 -0
  40. data/lib/predicate/sequel/to_sequel.rb +4 -1
  41. data/lib/predicate/sugar.rb +47 -0
  42. data/lib/predicate/version.rb +2 -2
  43. data/lib/predicate.rb +26 -2
  44. data/spec/dsl/test_dsl.rb +204 -0
  45. data/spec/dsl/test_evaluate.rb +65 -0
  46. data/spec/dsl/test_respond_to_missing.rb +35 -0
  47. data/spec/dsl/test_to_skake_case.rb +38 -0
  48. data/spec/factory/shared/a_comparison_factory_method.rb +1 -0
  49. data/spec/factory/test_${op_name}.rb.jeny +12 -0
  50. data/spec/factory/test_comp.rb +28 -5
  51. data/spec/factory/test_empty.rb +11 -0
  52. data/spec/factory/test_has_size.rb +11 -0
  53. data/spec/factory/test_match.rb +1 -0
  54. data/spec/factory/test_set_ops.rb +18 -0
  55. data/spec/factory/test_var.rb +22 -0
  56. data/spec/factory/test_vars.rb +27 -0
  57. data/spec/nodes/${op_name}.jeny/test_evaluate.rb.jeny +19 -0
  58. data/spec/nodes/empty/test_evaluate.rb +42 -0
  59. data/spec/nodes/has_size/test_evaluate.rb +44 -0
  60. data/spec/predicate/test_and_split.rb +18 -0
  61. data/spec/predicate/test_attr_split.rb +18 -0
  62. data/spec/predicate/test_constant_variables.rb +24 -2
  63. data/spec/predicate/test_constants.rb +24 -0
  64. data/spec/predicate/test_evaluate.rb +205 -3
  65. data/spec/predicate/test_free_variables.rb +1 -1
  66. data/spec/predicate/test_to_hash.rb +40 -0
  67. data/spec/predicate/test_to_s.rb +37 -0
  68. data/spec/predicate/test_unqualify.rb +18 -0
  69. data/spec/sequel/test_to_sequel.rb +25 -0
  70. data/spec/shared/a_predicate.rb +30 -0
  71. data/spec/spec_helper.rb +1 -0
  72. data/spec/test_predicate.rb +78 -33
  73. data/spec/test_readme.rb +80 -0
  74. data/spec/test_sugar.rb +48 -0
  75. data/tasks/test.rake +3 -3
  76. metadata +45 -14
  77. data/spec/factory/test_between.rb +0 -12
  78. data/spec/factory/test_intersect.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '039636282e90c0f484d6747ca27286890152c280dc9a5c7ba1d3fd21aff575f2'
4
- data.tar.gz: 1ccfa7508aee58a1734ecdb8ae1f4bee5c3c1718d284b04051877bb7dc5d94e2
3
+ metadata.gz: 4d0149b8d39635ed0f27c39362d1c4aec0dc5f2ccc1290580ed67edf18414e5b
4
+ data.tar.gz: a623594513754521196f1a98f5d2f682650a3ec1b02b02b4dfa703600039faa1
5
5
  SHA512:
6
- metadata.gz: 6470c50519c4675a6caebebf58767d2ff85b0c43e82ea838a0b042a84400c906a00aa84ec8707c3dfcdb548a8622bc31a333da55d4c8260817eef4d1629dd594
7
- data.tar.gz: d12feeb814e4e6d5eae9e8fbd5eab71af196bc113ab714e8b2b98583221d79f94b95bb860b681b50999eb1a68b3002c944183a31819860c4d5b7f0f29f4829ba
6
+ metadata.gz: dfc8fdb19e1d61171a4c91e893bfc6b6b91d559d8402f3a47dbede8d09dcb2f702f697cbd9e8b6db688e896d1831f837ace44d8c78b8c0e110d767ac0f99db05
7
+ data.tar.gz: 21ffbda2c7831051ac8ffba6a5c84d029735ce953e3fc969ae252945b0417ea28bc05f865e0e4e29771b542ca2993ec3720389a7be012b96b378536fb0743602
data/Gemfile CHANGED
@@ -1,2 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
+
4
+ group :development do
5
+ gem "jeny", github: "enspirit/jeny"
6
+ end
data/LICENSE.md CHANGED
@@ -1,22 +1,20 @@
1
- # The MIT Licence
1
+ Copyright (c) 2017-2020 - Enspirit SPRL (Bernard Lambeau)
2
2
 
3
- Copyright (c) 2017 - Enspirit SPRL (Bernard Lambeau)
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
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
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
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
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
+ ![](https://travis-ci.com/enspirit/predicate.svg?branch=master)
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,2 @@
1
+ #!/bin/sh
2
+ jeny -d name:$1 s .
@@ -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