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