predicated 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +183 -1
- data/lib/predicated.rb +1 -3
- data/lib/predicated/constrain.rb +2 -0
- data/lib/predicated/evaluate.rb +25 -6
- data/lib/predicated/from/callable_object.rb +7 -8
- data/lib/predicated/from/json.rb +59 -0
- data/lib/predicated/from/{ruby_string.rb → ruby_code_string.rb} +13 -5
- data/lib/predicated/from/{url_fragment.rb → url_part.rb} +17 -10
- data/lib/predicated/from/xml.rb +61 -0
- data/lib/predicated/predicate.rb +62 -43
- data/lib/predicated/print.rb +28 -16
- data/lib/predicated/selectable.rb +102 -0
- data/lib/predicated/simple_templated_predicate.rb +79 -0
- data/lib/predicated/string_utils.rb +20 -0
- data/lib/predicated/to/arel.rb +28 -22
- data/lib/predicated/to/json.rb +48 -0
- data/lib/predicated/to/sentence.rb +17 -14
- data/lib/predicated/to/solr.rb +15 -0
- data/lib/predicated/to/xml.rb +67 -0
- data/lib/predicated/version.rb +2 -2
- data/test/canonical_transform_cases.rb +67 -0
- data/test/constrain_test.rb +20 -11
- data/test/enumerable_test.rb +6 -34
- data/test/equality_test.rb +15 -4
- data/test/evaluate_test.rb +31 -26
- data/test/from/callable_object_canonical_test.rb +43 -0
- data/test/from/callable_object_test.rb +16 -40
- data/test/from/json_test.rb +83 -0
- data/test/from/ruby_code_string_canonical_test.rb +37 -0
- data/test/from/ruby_code_string_test.rb +103 -0
- data/test/from/{url_fragment_parser_test.rb → url_part_parser_test.rb} +20 -13
- data/test/from/url_part_test.rb +48 -0
- data/test/from/xml_test.rb +57 -0
- data/test/json_conversion_test.rb +33 -0
- data/test/print_test.rb +26 -25
- data/test/selectable_test.rb +123 -0
- data/test/simple_templated_predicate_test.rb +39 -0
- data/test/suite.rb +2 -4
- data/test/test_helper.rb +26 -4
- data/test/test_helper_with_wrong.rb +3 -2
- data/test/to/arel_test.rb +71 -31
- data/test/to/json_test.rb +74 -0
- data/test/to/sentence_test.rb +41 -34
- data/test/to/solr_test.rb +39 -0
- data/test/to/xml_test.rb +72 -0
- data/test/xml_conversion_test.rb +34 -0
- metadata +44 -16
- data/lib/predicated/selector.rb +0 -55
- data/test/from/ruby_string_test.rb +0 -135
- data/test/from/url_fragment_test.rb +0 -37
- data/test/selector_test.rb +0 -82
data/README.markdown
CHANGED
@@ -1,8 +1,190 @@
|
|
1
|
-
|
1
|
+
## Abstract ##
|
2
|
+
|
3
|
+
Predicated is a simple predicate model for Ruby. It provides useful predicate transformations and operations.
|
2
4
|
|
3
5
|
Tracker project:
|
4
6
|
[http://www.pivotaltracker.com/projects/95014](http://www.pivotaltracker.com/projects/95014)
|
5
7
|
|
8
|
+
## Transformations ##
|
9
|
+
|
10
|
+
- From:
|
11
|
+
- json
|
12
|
+
- xml
|
13
|
+
- url part, ex: "!(a=1&b=2|c=3)"
|
14
|
+
- callable objects (lambdas/procs, and therefore blocks) - ruby 1.8.x only
|
15
|
+
- ruby code - ruby 1.8.x only
|
16
|
+
- To:
|
17
|
+
- json
|
18
|
+
- xml
|
19
|
+
- sql where clause via [arel](http://github.com/rails/arel)
|
20
|
+
- [solr](http://lucene.apache.org/solr/) query string
|
21
|
+
- english sentence, ex: "'a' is not equal to 'b'"
|
22
|
+
|
23
|
+
## Usage ##
|
24
|
+
|
25
|
+
Note: The test suite acts as a comprehensive usage guide.
|
26
|
+
|
27
|
+
|
28
|
+
Evaluate a predicate:
|
29
|
+
|
30
|
+
require "predicated/evaluate"
|
31
|
+
include Predicated
|
32
|
+
|
33
|
+
Predicate { Eq(1, 2) }.evaluate == false
|
34
|
+
Predicate { Lt(1, 2) }.evaluate == true
|
35
|
+
Predicate { Or(Lt(1, 2),Eq(1, 2)) }.evaluate == true
|
36
|
+
|
37
|
+
x = 1
|
38
|
+
Predicate { Lt(x, 2) }.evaluate == true
|
39
|
+
|
40
|
+
|
41
|
+
Parse a predicate from part of a url and then turn it into a sql where clause:
|
42
|
+
|
43
|
+
require "predicated/from/url_part"
|
44
|
+
require "predicated/to/arel"
|
45
|
+
|
46
|
+
predicate = Predicated::Predicate.from_url_part("(color=red|color=green)&size=large")
|
47
|
+
|
48
|
+
predicate.inspect ==
|
49
|
+
"And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
50
|
+
|
51
|
+
predicate.to_arel(Table(:shirt)).to_sql ==
|
52
|
+
%{(("shirt"."color" = 'red' OR "shirt"."color" = 'green') AND "shirt"."size" = 'large')}
|
53
|
+
|
54
|
+
|
55
|
+
Parse a predicate from json and then turn it into a solr query string:
|
56
|
+
|
57
|
+
require "predicated/from/json"
|
58
|
+
require "predicated/to/solr"
|
59
|
+
|
60
|
+
predicate = Predicated::Predicate.from_json_str(%{
|
61
|
+
{"and":[{"or":[["color","==","red"],["color","==","green"]]},["size","==","large"]]}
|
62
|
+
})
|
63
|
+
|
64
|
+
predicate.inspect == "And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
65
|
+
|
66
|
+
predicate.to_solr == "((color:red OR color:green) AND size:large)"
|
67
|
+
|
68
|
+
|
69
|
+
From json:
|
70
|
+
|
71
|
+
require "predicated/from/json"
|
72
|
+
|
73
|
+
Predicated::Predicate.from_json_str(%{
|
74
|
+
{"and":[
|
75
|
+
{"or":[
|
76
|
+
["color","==","red"],
|
77
|
+
["color","==","green"]
|
78
|
+
]},
|
79
|
+
["size","==","large"]
|
80
|
+
]}
|
81
|
+
}).inspect == "And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
82
|
+
|
83
|
+
|
84
|
+
From xml:
|
85
|
+
|
86
|
+
require "predicated/from/xml"
|
87
|
+
|
88
|
+
Predicated::Predicate.from_xml(%{
|
89
|
+
<and>
|
90
|
+
<or>
|
91
|
+
<equal><left>color</left><right>red</right></equal>
|
92
|
+
<equal><left>color</left><right>green</right></equal>
|
93
|
+
</or>
|
94
|
+
<equal><left>size</left><right>large</right></equal>
|
95
|
+
</and>
|
96
|
+
}).inspect == "And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
97
|
+
|
98
|
+
|
99
|
+
From url part:
|
100
|
+
|
101
|
+
require "predicated/from/url_part"
|
102
|
+
|
103
|
+
Predicated::Predicate.from_url_part("(color=red|color=green)&size=large").inspect ==
|
104
|
+
"And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
105
|
+
|
106
|
+
|
107
|
+
From callable object:
|
108
|
+
|
109
|
+
require "predicated/from/callable_object"
|
110
|
+
|
111
|
+
Predicated::Predicate.from_callable_object{('color'=='red' || 'color'=='green') && 'size'=='large'}.inspect ==
|
112
|
+
"And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
113
|
+
|
114
|
+
|
115
|
+
From ruby code string:
|
116
|
+
|
117
|
+
require "predicated/from/ruby_code_string"
|
118
|
+
|
119
|
+
Predicated::Predicate.from_ruby_code_string("('color'=='red' || 'color'=='green') && 'size'=='large'").inspect ==
|
120
|
+
"And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))"
|
121
|
+
|
122
|
+
|
123
|
+
To json:
|
124
|
+
|
125
|
+
require "predicated/to/json"
|
126
|
+
include Predicated
|
127
|
+
|
128
|
+
Predicate{And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))}.to_json_str ==
|
129
|
+
%{
|
130
|
+
{"and":[
|
131
|
+
{"or":[
|
132
|
+
["color","==","red"],
|
133
|
+
["color","==","green"]
|
134
|
+
]},
|
135
|
+
["size","==","large"]
|
136
|
+
]}
|
137
|
+
}
|
138
|
+
|
139
|
+
|
140
|
+
To xml:
|
141
|
+
|
142
|
+
require "predicated/to/xml"
|
143
|
+
include Predicated
|
144
|
+
|
145
|
+
Predicate{And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))}.to_xml ==
|
146
|
+
%{
|
147
|
+
<and>
|
148
|
+
<or>
|
149
|
+
<equal><left>color</left><right>red</right></equal>
|
150
|
+
<equal><left>color</left><right>green</right></equal>
|
151
|
+
</or>
|
152
|
+
<equal><left>size</left><right>large</right></equal>
|
153
|
+
</and>
|
154
|
+
}
|
155
|
+
|
156
|
+
|
157
|
+
To arel (sql where clause):
|
158
|
+
|
159
|
+
require "predicated/to/arel"
|
160
|
+
include Predicated
|
161
|
+
|
162
|
+
Predicate{And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))}.to_arel(Table(:shirt)).to_sql ==
|
163
|
+
%{(("shirt"."color" = 'red' OR "shirt"."color" = 'green') AND "shirt"."size" = 'large')}
|
164
|
+
|
165
|
+
|
166
|
+
To solr query string:
|
167
|
+
|
168
|
+
require "predicated/to/solr"
|
169
|
+
include Predicated
|
170
|
+
|
171
|
+
Predicate{And(Or(Eq('color','red'),Eq('color','green')),Eq('size','large'))}.to_solr ==
|
172
|
+
"((color:red OR color:green) AND size:large)"
|
173
|
+
|
174
|
+
|
175
|
+
To sentence:
|
176
|
+
|
177
|
+
require "predicated/to/sentence"
|
178
|
+
include Predicated
|
179
|
+
|
180
|
+
Predicate{ And(Eq("a",1),Eq("b",2)) }.to_sentence ==
|
181
|
+
"'a' is equal to 1 and 'b' is equal to 2"
|
182
|
+
|
183
|
+
Predicate{ Gt("a",1) }.to_negative_sentence ==
|
184
|
+
"'a' is not greater than 1"
|
185
|
+
|
186
|
+
|
187
|
+
## Testing Notes ##
|
6
188
|
|
7
189
|
Right now this project makes use of Wrong for assertions. Wrong uses this project. It's kind of neat in an eat-your-own-dogfood sense, but it's possible that this will be problematic over time (particularly when changes in this project cause assertions to behave differently - if even temporarily).
|
8
190
|
|
data/lib/predicated.rb
CHANGED
data/lib/predicated/constrain.rb
CHANGED
data/lib/predicated/evaluate.rb
CHANGED
@@ -3,7 +3,7 @@ require "predicated/predicate"
|
|
3
3
|
module Predicated
|
4
4
|
|
5
5
|
|
6
|
-
class Operation
|
6
|
+
class Operation < Binary
|
7
7
|
attr_reader :method_sym
|
8
8
|
|
9
9
|
def initialize(left, method_sym, right)
|
@@ -12,7 +12,8 @@ module Predicated
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def evaluate
|
15
|
-
|
15
|
+
right_values = right.nil? ? [nil] : right #1.9 problem where nils and varargs don't play nicely
|
16
|
+
left.send(@method_sym, *right_values)
|
16
17
|
end
|
17
18
|
|
18
19
|
def ==(other)
|
@@ -36,11 +37,22 @@ module Predicated
|
|
36
37
|
super
|
37
38
|
end
|
38
39
|
|
39
|
-
def
|
40
|
-
"Call(#{
|
40
|
+
def to_s
|
41
|
+
"Call(#{left_to_s}.#{method_sym.to_s}#{right_to_s})"
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def left_to_s
|
46
|
+
part_to_s(left)
|
47
|
+
end
|
48
|
+
|
49
|
+
def right_to_s
|
50
|
+
values = right.is_a?(::Enumerable) ? right : [right]
|
51
|
+
values.empty? ? "" :
|
52
|
+
"(" + values.collect{|arg|part_to_s(arg)}.join(",") + ")"
|
41
53
|
end
|
42
54
|
end
|
43
|
-
|
55
|
+
Shorthand.module_eval(%{
|
44
56
|
def Call(left_object, method_sym, right_args=[])
|
45
57
|
::Predicated::Call.new(left_object, method_sym, right_args)
|
46
58
|
end
|
@@ -72,4 +84,11 @@ module Predicated
|
|
72
84
|
boolean_or_evaluate(left) || boolean_or_evaluate(right)
|
73
85
|
end
|
74
86
|
end
|
75
|
-
|
87
|
+
|
88
|
+
class Not
|
89
|
+
include Container
|
90
|
+
def evaluate
|
91
|
+
! boolean_or_evaluate(inner)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,13 +1,12 @@
|
|
1
1
|
require "predicated/predicate"
|
2
|
-
require "predicated/from/
|
2
|
+
require "predicated/from/ruby_code_string"
|
3
3
|
|
4
4
|
#Procs and lambdas are "callable objects"
|
5
|
-
|
6
5
|
module Predicated
|
7
6
|
|
8
|
-
require_gem_version("ParseTree", "3.0.5", "parse_tree")
|
7
|
+
require_gem_version("ParseTree", "3.0.5", "parse_tree") if RUBY_VERSION < "1.9"
|
9
8
|
|
10
|
-
|
9
|
+
class Predicate
|
11
10
|
|
12
11
|
#hrm
|
13
12
|
def self.from_callable_object(context_or_callable_object=nil, context=nil, &block)
|
@@ -22,7 +21,7 @@ module Predicated
|
|
22
21
|
|
23
22
|
context ||= callable_object.binding
|
24
23
|
|
25
|
-
|
24
|
+
from_ruby_code_string(TranslateToRubyString.convert(callable_object), context)
|
26
25
|
end
|
27
26
|
|
28
27
|
module TranslateToRubyString
|
@@ -32,8 +31,8 @@ module Predicated
|
|
32
31
|
temp_class.class_eval do
|
33
32
|
define_method :serializable, &callable_object
|
34
33
|
end
|
35
|
-
|
36
|
-
|
34
|
+
ruby_code_string = Ruby2Ruby.translate(temp_class, :serializable)
|
35
|
+
ruby_code_string.sub(/^def serializable\n /, "").sub(/\nend$/, "")
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
@@ -76,4 +75,4 @@ module Predicated
|
|
76
75
|
end
|
77
76
|
|
78
77
|
end
|
79
|
-
end
|
78
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "predicated/predicate"
|
2
|
+
|
3
|
+
module Predicated
|
4
|
+
|
5
|
+
require_gem_version("json", "1.1.9")
|
6
|
+
|
7
|
+
class Predicate
|
8
|
+
def self.from_json_str(json_str)
|
9
|
+
from_json_struct(JSON.parse(json_str))
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_json_struct(json_struct)
|
13
|
+
JsonStructToPredicate.convert(json_struct)
|
14
|
+
end
|
15
|
+
|
16
|
+
module JsonStructToPredicate
|
17
|
+
SIGN_TO_CLASS = {
|
18
|
+
"==" => Equal,
|
19
|
+
">" => GreaterThan,
|
20
|
+
"<" => LessThan,
|
21
|
+
">=" => GreaterThanOrEqualTo,
|
22
|
+
"<=" => LessThanOrEqualTo
|
23
|
+
}
|
24
|
+
|
25
|
+
def self.convert(json_struct)
|
26
|
+
if json_struct.is_a?(Array)
|
27
|
+
left, sign, right = json_struct
|
28
|
+
if operation_class=SIGN_TO_CLASS[sign]
|
29
|
+
operation_class.new(left, right)
|
30
|
+
else
|
31
|
+
raise DontKnowWhatToDoWithThisJson.new(json_struct)
|
32
|
+
end
|
33
|
+
elsif json_struct.is_a?(Hash)
|
34
|
+
if left_and_right=json_struct["and"]
|
35
|
+
left, right = left_and_right
|
36
|
+
And.new(convert(left), convert(right))
|
37
|
+
elsif left_and_right=json_struct["or"]
|
38
|
+
left, right = left_and_right
|
39
|
+
Or.new(convert(left), convert(right))
|
40
|
+
elsif inner=json_struct["not"]
|
41
|
+
Not.new(convert(inner))
|
42
|
+
else
|
43
|
+
raise DontKnowWhatToDoWithThisJson.new(json_struct)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
raise DontKnowWhatToDoWithThisJson.new(json_struct)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
class DontKnowWhatToDoWithThisJson < StandardError
|
53
|
+
def initialize(json_struct)
|
54
|
+
super("don't know what to do with #{JSON.generate(json_struct)}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -6,8 +6,8 @@ module Predicated
|
|
6
6
|
require_gem_version("ruby_parser", "2.0.4")
|
7
7
|
require_gem_version("ruby2ruby", "1.2.4")
|
8
8
|
|
9
|
-
|
10
|
-
def self.
|
9
|
+
class Predicate
|
10
|
+
def self.from_ruby_code_string(ruby_predicate_string, context=binding())
|
11
11
|
sexp = RubyParser.new.process(ruby_predicate_string.strip)
|
12
12
|
SexpToPredicate.new(context).convert(sexp)
|
13
13
|
end
|
@@ -31,13 +31,13 @@ module Predicated
|
|
31
31
|
#eval all the top lines and then treat the last one as a predicate
|
32
32
|
body_sexps = sexp.sexp_body.to_a
|
33
33
|
body_sexps.slice(0..-2).each do |upper_sexp|
|
34
|
-
|
34
|
+
eval_sexp(upper_sexp)
|
35
35
|
end
|
36
36
|
convert(body_sexps.last)
|
37
37
|
elsif first_element == :call
|
38
38
|
sym, left_sexp, method_sym, right_sexp = sexp
|
39
|
-
left =
|
40
|
-
right =
|
39
|
+
left = eval_sexp(left_sexp)
|
40
|
+
right = eval_sexp(right_sexp)
|
41
41
|
|
42
42
|
if operation_class=SIGN_TO_PREDICATE_CLASS[method_sym]
|
43
43
|
operation_class.new(left, right)
|
@@ -50,12 +50,20 @@ module Predicated
|
|
50
50
|
elsif first_element == :or
|
51
51
|
sym, left, right = sexp
|
52
52
|
Or.new(convert(left), convert(right))
|
53
|
+
elsif first_element == :not
|
54
|
+
sym, inner = sexp
|
55
|
+
Not.new(convert(inner))
|
53
56
|
else
|
54
57
|
raise DontKnowWhatToDoWithThisSexpError.new(sexp)
|
55
58
|
end
|
56
59
|
end
|
60
|
+
|
61
|
+
def eval_sexp(sexp)
|
62
|
+
eval(Ruby2Ruby.new.process(sexp), @context)
|
63
|
+
end
|
57
64
|
end
|
58
65
|
|
66
|
+
|
59
67
|
class DontKnowWhatToDoWithThisSexpError < StandardError
|
60
68
|
def initialize(sexp)
|
61
69
|
super("don't know what to do with #{sexp.inspect}")
|
@@ -4,21 +4,21 @@ module Predicated
|
|
4
4
|
|
5
5
|
require_gem_version("treetop", "1.4.8")
|
6
6
|
|
7
|
-
|
8
|
-
def self.
|
9
|
-
|
7
|
+
class Predicate
|
8
|
+
def self.from_url_part(url_part)
|
9
|
+
TreetopUrlPartParser.new.parse(url_part).to_predicate
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
module
|
13
|
+
module TreetopUrlPart
|
14
14
|
Treetop.load_from_string(%{
|
15
15
|
|
16
|
-
grammar
|
16
|
+
grammar TreetopUrlPart
|
17
17
|
|
18
|
-
include Predicated::
|
18
|
+
include Predicated::TreetopUrlPart
|
19
19
|
|
20
20
|
rule or
|
21
|
-
|
21
|
+
( and "|" or <OrNode>) / and
|
22
22
|
end
|
23
23
|
|
24
24
|
rule and
|
@@ -30,9 +30,14 @@ rule operation
|
|
30
30
|
end
|
31
31
|
|
32
32
|
rule parens
|
33
|
-
"(" or ")" <ParensNode>
|
33
|
+
not? "(" or ")" <ParensNode>
|
34
34
|
end
|
35
35
|
|
36
|
+
rule not
|
37
|
+
'!'
|
38
|
+
end
|
39
|
+
|
40
|
+
|
36
41
|
|
37
42
|
rule leaf
|
38
43
|
operation / parens
|
@@ -86,10 +91,12 @@ end
|
|
86
91
|
end
|
87
92
|
|
88
93
|
class ParensNode < Treetop::Runtime::SyntaxNode
|
89
|
-
def inner; elements[
|
94
|
+
def inner; elements[2] end
|
95
|
+
def not?; elements[0].text_value=="!" end
|
90
96
|
|
91
97
|
def to_predicate
|
92
|
-
inner.to_predicate
|
98
|
+
inner_predicate = inner.to_predicate
|
99
|
+
not? ? Not.new(inner_predicate) : inner_predicate
|
93
100
|
end
|
94
101
|
end
|
95
102
|
|