predicate 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) 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 +15 -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/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 +1 -3
  17. data/lib/predicate/nodes/expr.rb +1 -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 +2 -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 +2 -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 +4 -0
  39. data/lib/predicate/sequel/to_sequel.rb +3 -0
  40. data/lib/predicate/sugar.rb +47 -0
  41. data/lib/predicate/version.rb +1 -1
  42. data/spec/dsl/test_dsl.rb +204 -0
  43. data/spec/dsl/test_evaluate.rb +65 -0
  44. data/spec/dsl/test_respond_to_missing.rb +35 -0
  45. data/spec/dsl/test_to_skake_case.rb +38 -0
  46. data/spec/factory/shared/a_comparison_factory_method.rb +1 -0
  47. data/spec/factory/test_${op_name}.rb.jeny +12 -0
  48. data/spec/factory/test_empty.rb +11 -0
  49. data/spec/factory/test_has_size.rb +11 -0
  50. data/spec/factory/test_match.rb +1 -0
  51. data/spec/factory/test_set_ops.rb +18 -0
  52. data/spec/factory/test_var.rb +22 -0
  53. data/spec/factory/test_vars.rb +27 -0
  54. data/spec/nodes/${op_name}.jeny/test_evaluate.rb.jeny +19 -0
  55. data/spec/nodes/empty/test_evaluate.rb +42 -0
  56. data/spec/nodes/has_size/test_evaluate.rb +44 -0
  57. data/spec/predicate/test_and_split.rb +18 -0
  58. data/spec/predicate/test_attr_split.rb +18 -0
  59. data/spec/predicate/test_constant_variables.rb +24 -2
  60. data/spec/predicate/test_constants.rb +24 -0
  61. data/spec/predicate/test_evaluate.rb +205 -3
  62. data/spec/predicate/test_to_s.rb +37 -0
  63. data/spec/sequel/test_to_sequel.rb +16 -0
  64. data/spec/shared/a_predicate.rb +30 -0
  65. data/spec/spec_helper.rb +1 -0
  66. data/spec/test_predicate.rb +68 -33
  67. data/spec/test_readme.rb +80 -0
  68. data/spec/test_sugar.rb +48 -0
  69. data/tasks/test.rake +3 -3
  70. metadata +40 -12
  71. data/spec/factory/test_between.rb +0 -12
  72. 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)
@@ -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}'
@@ -3,6 +3,8 @@ rules:
3
3
  - tautology
4
4
  - contradiction
5
5
  - identifier
6
+ - qualified_identifier
7
+ - var
6
8
  - not
7
9
  - and
8
10
  - or
@@ -14,8 +16,13 @@ rules:
14
16
  - gte
15
17
  - in
16
18
  - intersect
19
+ - subset
20
+ - superset
17
21
  - match
18
22
  - native
23
+ - empty
24
+ - has_size
25
+ #jeny(predicate) - ${op_name}
19
26
  tautology:
20
27
  - [ true ]
21
28
  contradiction:
@@ -24,6 +31,8 @@ rules:
24
31
  - [ name ]
25
32
  qualified_identifier:
26
33
  - [ name, name ]
34
+ var:
35
+ - [ formaldef, semantics ]
27
36
  not:
28
37
  - [ predicate ]
29
38
  and:
@@ -46,8 +55,18 @@ rules:
46
55
  - [ varref, term ]
47
56
  intersect:
48
57
  - [ term, term ]
58
+ subset:
59
+ - [ term, term ]
60
+ superset:
61
+ - [ term, term ]
49
62
  match:
50
63
  - [ term, term, options ]
64
+ empty:
65
+ - [ term ]
66
+ has_size:
67
+ - [ term, term ]
68
+ #jeny(predicate) ${op_name}:
69
+ #jeny(predicate) - [ TODO ]
51
70
  term:
52
71
  - varref
53
72
  - literal
@@ -56,6 +75,7 @@ rules:
56
75
  varref:
57
76
  - qualified_identifier
58
77
  - identifier
78
+ - var
59
79
  native:
60
80
  - [ "::Proc" ]
61
81
  literal:
@@ -68,3 +88,12 @@ rules:
68
88
  - "::Hash"
69
89
  name:
70
90
  !ruby/regexp /^[a-zA-Z0-9_]+[?!]?$/
91
+ semantics:
92
+ !ruby/regexp /^(dig)$/
93
+ formaldef:
94
+ - string_formaldef
95
+ - array_formaldef
96
+ string_formaldef:
97
+ - "::String"
98
+ array_formaldef:
99
+ - "::Array"