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.
- checksums.yaml +4 -4
- data/Gemfile +4 -0
- data/LICENSE.md +17 -19
- data/README.md +433 -0
- data/bin/g +2 -0
- data/lib/predicate.rb +26 -2
- data/lib/predicate/dsl.rb +138 -0
- data/lib/predicate/factory.rb +130 -33
- 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 +3 -5
- data/lib/predicate/nodes/empty.rb +14 -0
- data/lib/predicate/nodes/eq.rb +13 -6
- 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 +9 -8
- 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 +2 -4
- 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.rb +1 -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/sequel/to_sequel.rb +4 -1
- data/lib/predicate/sugar.rb +47 -0
- data/lib/predicate/version.rb +1 -1
- 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_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/eq/test_and.rb +6 -0
- data/spec/nodes/has_size/test_evaluate.rb +44 -0
- data/spec/nodes/qualified_identifier/test_and_split.rb +1 -1
- data/spec/nodes/qualified_identifier/test_free_variables.rb +1 -1
- data/spec/predicate/test_and_split.rb +18 -0
- data/spec/predicate/test_attr_split.rb +18 -0
- data/spec/predicate/test_bool_and.rb +11 -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 +16 -0
- data/spec/shared/a_predicate.rb +30 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/test_predicate.rb +68 -33
- data/spec/test_readme.rb +80 -0
- data/spec/test_sugar.rb +48 -0
- data/tasks/test.rake +3 -3
- metadata +43 -13
- data/spec/factory/test_between.rb +0 -12
- data/spec/factory/test_intersect.rb +0 -12
data/bin/g
ADDED
data/lib/predicate.rb
CHANGED
@@ -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
|
10
|
-
class
|
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
|
data/lib/predicate/factory.rb
CHANGED
@@ -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
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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)
|
data/lib/predicate/grammar.rb
CHANGED
@@ -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}'
|