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
data/bin/g ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ jeny -d name:$1 s .
@@ -2,12 +2,16 @@ require 'sexpr'
2
2
  require_relative 'predicate/version'
3
3
  require_relative 'predicate/placeholder'
4
4
  require_relative 'predicate/factory'
5
+ require_relative 'predicate/sugar'
5
6
  require_relative 'predicate/grammar'
6
7
  require_relative 'predicate/processors'
8
+ require_relative 'predicate/dsl'
7
9
  class Predicate
8
10
 
9
- class NotSupportedError < StandardError; end
10
- class UnboundError < StandardError; end
11
+ class Error < StandardError; end
12
+ class NotSupportedError < Error; end
13
+ class UnboundError < Error; end
14
+ class TypeError < Error; end
11
15
 
12
16
  TupleLike = ->(t){ t.is_a?(Hash) }
13
17
 
@@ -21,6 +25,7 @@ class Predicate
21
25
 
22
26
  class << self
23
27
  include Factory
28
+ include Sugar
24
29
 
25
30
  def coerce(arg)
26
31
  case arg
@@ -36,6 +41,14 @@ class Predicate
36
41
  end
37
42
  alias :parse :coerce
38
43
 
44
+ def dsl(var = var(".", :dig), &bl)
45
+ Predicate::Dsl.new(var, false).instance_eval(&bl)
46
+ end
47
+
48
+ def currying(var = var(".", :dig), &bl)
49
+ Predicate::Dsl.new(var, true).instance_eval(&bl)
50
+ end
51
+
39
52
  private
40
53
 
41
54
  def _factor_predicate(arg)
@@ -88,6 +101,10 @@ class Predicate
88
101
  Predicate.new(expr.qualify(qualifier))
89
102
  end
90
103
 
104
+ def unqualify
105
+ Predicate.new(expr.unqualify)
106
+ end
107
+
91
108
  def rename(renaming)
92
109
  Predicate.new(expr.rename(renaming))
93
110
  end
@@ -138,4 +155,11 @@ class Predicate
138
155
  expr.to_s(scope)
139
156
  end
140
157
 
158
+ # If possible, converts this predicate back to a `{ attr: value, ... }`
159
+ # hash. Raises an IllegalArgumentError if the predicate cannot be
160
+ # represented that way.
161
+ def to_hash
162
+ expr.to_hash
163
+ end
164
+
141
165
  end # class Predicate
@@ -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
@@ -1,69 +1,182 @@
1
1
  class Predicate
2
2
  module Factory
3
3
 
4
+ public # Boolean
5
+
6
+ # Factors a Predicate that captures True
4
7
  def tautology
5
8
  _factor_predicate([:tautology, true])
6
9
  end
7
10
 
11
+ # Factors a Predicate that captures False
8
12
  def contradiction
9
13
  _factor_predicate([:contradiction, false])
10
14
  end
11
15
 
16
+ public # Literals
17
+
18
+ # Factors a Literal node for some ruby value.
19
+ def literal(literal)
20
+ _factor_predicate([:literal, literal])
21
+ end
22
+
23
+ public # Vars & identifiers
24
+
25
+ # Factors a var node, using a given extractor semantics
26
+ def var(formaldef, semantics = :dig)
27
+ _factor_predicate([:var, formaldef, semantics])
28
+ end
29
+
30
+ # Factors a couple of variables at once. The semantics can
31
+ # be passed as a Symbol as last argument and defaults to :dig
32
+ def vars(*args)
33
+ args << :dig unless args.last.is_a?(Symbol)
34
+ args[0...-1].map{|v| var(v, args.last) }
35
+ end
36
+
37
+ # Factors a Predicate for a free variable whose
38
+ # name is provided. If the variable is a Boolean
39
+ # variable, this is a valid Predicate, otherwise
40
+ # it must be used in a higher-level expression.
12
41
  def identifier(name)
13
42
  _factor_predicate([:identifier, name])
14
43
  end
15
44
 
45
+ # Factors a Predicate for a qualified free variable.
46
+ # Same remark as in `identifier`.
16
47
  def qualified_identifier(qualifier, name)
17
48
  _factor_predicate([:qualified_identifier, qualifier, name])
18
49
  end
19
50
 
51
+ # Builds and returns a placeholder that can be used
52
+ # everywhere a literal can be used. Placeholders can
53
+ # be bound later, using `Predicate#bind`.
20
54
  def placeholder
21
55
  Placeholder.new
22
56
  end
23
57
 
58
+ public # Boolean logic
59
+
60
+ # Builds a AND predicate using two sub predicates.
61
+ #
62
+ # Please favor `Predicate#&` instead.
24
63
  def and(left, right = nil)
25
64
  _factor_predicate([:and, sexpr(left), sexpr(right)])
26
65
  end
27
66
 
67
+ # Builds a OR predicate using two sub predicates.
68
+ #
69
+ # Please favor `Predicate#|` instead.
28
70
  def or(left, right = nil)
29
71
  _factor_predicate([:or, sexpr(left), sexpr(right)])
30
72
  end
31
73
 
74
+ # Negates an existing predicate.
75
+ #
76
+ # Please favor `Predicate#!` instead.
32
77
  def not(operand)
33
78
  _factor_predicate([:not, sexpr(operand)])
34
79
  end
35
80
 
81
+ public # Comparison operators
82
+
83
+ # :nodoc:
84
+ def comp(op, h)
85
+ from_hash(h, op)
86
+ end
87
+
88
+ # Factors =, !=, <, <=, >, >= predicates between
89
+ # a variable and either a literal or another variable.
90
+ [ :eq, :neq, :lt, :lte, :gt, :gte ].each do |m|
91
+ define_method(m) do |left, right|
92
+ _factor_predicate([m, sexpr(left), sexpr(right)])
93
+ end
94
+ end
95
+
96
+ # Set operators
97
+
98
+ # Factors a IN predicate between a variable and
99
+ # either a list of values of another variable.
36
100
  def in(left, right)
37
- left, right = sexpr(left), sexpr(right)
38
- if right.literal? && right.empty_value?
39
- contradiction
101
+ case right
102
+ when Range
103
+ return contradiction if right.size == 0
104
+ rl = gte(left, right.begin)
105
+ rr = right.exclude_end? ? lt(left, right.end) : lte(left, right.end)
106
+ self.and(rl, rr)
40
107
  else
41
- _factor_predicate([:in, left, right])
108
+ left, right = sexpr(left), sexpr(right)
109
+ if right.literal? && right.empty_value?
110
+ contradiction
111
+ else
112
+ _factor_predicate([:in, left, right])
113
+ end
42
114
  end
43
115
  end
44
116
  alias :among :in
45
117
 
46
- def intersect(identifier, values)
47
- identifier = sexpr(identifier) if identifier.is_a?(Symbol)
48
- _factor_predicate([:intersect, identifier, values])
118
+ # Factors an INTERSECT predicate between a
119
+ # variable and a list of values.
120
+ [:intersect, :subset, :superset].each do |name|
121
+ define_method(name) do |left, right|
122
+ identifier = sexpr(identifier) if identifier.is_a?(Symbol)
123
+ _factor_predicate([name, sexpr(left), sexpr(right)])
124
+ end
49
125
  end
50
126
 
51
- def comp(op, h)
52
- from_hash(h, op)
127
+ public # Other operators
128
+
129
+ # Factors a MATCH predicate between a variable
130
+ # and a literal or another variable.
131
+ #
132
+ # Matching options can be passes and are specific
133
+ # to the actual usage of the library.
134
+ def match(left, right, options)
135
+ s = [:match, sexpr(left), sexpr(right)]
136
+ s << options unless options.nil?
137
+ _factor_predicate(s)
53
138
  end
54
139
 
55
- [ :eq, :neq, :lt, :lte, :gt, :gte ].each do |m|
56
- define_method(m) do |left, right=nil|
57
- return comp(m, left) if TupleLike===left && right.nil?
58
- _factor_predicate([m, sexpr(left), sexpr(right)])
59
- end
140
+ # Factors an EMPTY predicate that responds true
141
+ # when its operand is something empty.
142
+ #
143
+ # Default evaluation uses ruby `empty?` method.
144
+ def empty(operand)
145
+ _factor_predicate([:empty, sexpr(operand)])
146
+ end
147
+
148
+ # Factors a SIZE predicate that responds true when
149
+ # its operand has a size meeting the right constraint
150
+ # (typically a Range literal)
151
+ def has_size(left, right)
152
+ _factor_predicate([:has_size, sexpr(left), sexpr(right)])
153
+ end
154
+
155
+ #jeny(predicate) # TODO
156
+ #jeny(predicate) def ${op_name}(*args)
157
+ #jeny(predicate) args = args.map{|arg| sexpr(arg) }
158
+ #jeny(predicate) _factor_predicate([:${op_name}] + args)
159
+ #jeny(predicate) end
160
+
161
+ public # Low-level
162
+
163
+ # Factors a predicate for a ruby Proc that returns
164
+ # truth-value for a single argument.
165
+ def native(arg)
166
+ _factor_predicate([:native, arg])
60
167
  end
61
168
 
62
- def between(middle, lower_bound, upper_bound)
63
- _factor_predicate [:and, [:gte, sexpr(middle), sexpr(lower_bound)],
64
- [:lte, sexpr(middle), sexpr(upper_bound)]]
169
+ # Converts `arg` to an opaque predicate, whose semantics
170
+ # depends on the actual usage of the library.
171
+ def opaque(arg)
172
+ _factor_predicate([:opaque, arg])
65
173
  end
66
174
 
175
+ public # Semi protected
176
+
177
+ # Builds a AND predicate between all key/value pairs
178
+ # of the provided Hash, using the comparison operator
179
+ # specified.
67
180
  def from_hash(h, op = :eq)
68
181
  if h.empty?
69
182
  tautology
@@ -80,22 +193,6 @@ class Predicate
80
193
  end
81
194
  end
82
195
 
83
- def literal(literal)
84
- _factor_predicate([:literal, literal])
85
- end
86
-
87
- def opaque(arg)
88
- _factor_predicate([:opaque, arg])
89
- end
90
-
91
- def match(left, right, options = nil)
92
- _factor_predicate([:match, sexpr(left), sexpr(right)] + (options.nil? ? [] : [options]))
93
- end
94
-
95
- def native(arg)
96
- _factor_predicate([:native, arg])
97
- end
98
-
99
196
  protected
100
197
 
101
198
  def sexpr(expr)
@@ -10,13 +10,14 @@ class Predicate
10
10
  Expr
11
11
  end
12
12
 
13
- end
13
+ end # module Grammar
14
14
  end # class Predicate
15
15
  require_relative 'nodes/expr'
16
16
  require_relative 'nodes/dyadic_comp'
17
17
  require_relative 'nodes/nadic_bool'
18
18
  require_relative 'nodes/tautology'
19
19
  require_relative 'nodes/contradiction'
20
+ require_relative 'nodes/var'
20
21
  require_relative 'nodes/identifier'
21
22
  require_relative 'nodes/qualified_identifier'
22
23
  require_relative 'nodes/and'
@@ -29,8 +30,16 @@ require_relative 'nodes/gte'
29
30
  require_relative 'nodes/lt'
30
31
  require_relative 'nodes/lte'
31
32
  require_relative 'nodes/in'
33
+ require_relative 'nodes/set_op'
32
34
  require_relative 'nodes/intersect'
35
+ require_relative 'nodes/subset'
36
+ require_relative 'nodes/superset'
33
37
  require_relative 'nodes/literal'
34
- require_relative 'nodes/match'
35
38
  require_relative 'nodes/native'
36
39
  require_relative 'nodes/opaque'
40
+ require_relative 'nodes/unary_func'
41
+ require_relative 'nodes/binary_func'
42
+ require_relative 'nodes/match'
43
+ require_relative 'nodes/empty'
44
+ require_relative 'nodes/has_size'
45
+ #jeny(predicate) require_relative 'nodes/${op_name}'