predicated 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/README.markdown +183 -1
  2. data/lib/predicated.rb +1 -3
  3. data/lib/predicated/constrain.rb +2 -0
  4. data/lib/predicated/evaluate.rb +25 -6
  5. data/lib/predicated/from/callable_object.rb +7 -8
  6. data/lib/predicated/from/json.rb +59 -0
  7. data/lib/predicated/from/{ruby_string.rb → ruby_code_string.rb} +13 -5
  8. data/lib/predicated/from/{url_fragment.rb → url_part.rb} +17 -10
  9. data/lib/predicated/from/xml.rb +61 -0
  10. data/lib/predicated/predicate.rb +62 -43
  11. data/lib/predicated/print.rb +28 -16
  12. data/lib/predicated/selectable.rb +102 -0
  13. data/lib/predicated/simple_templated_predicate.rb +79 -0
  14. data/lib/predicated/string_utils.rb +20 -0
  15. data/lib/predicated/to/arel.rb +28 -22
  16. data/lib/predicated/to/json.rb +48 -0
  17. data/lib/predicated/to/sentence.rb +17 -14
  18. data/lib/predicated/to/solr.rb +15 -0
  19. data/lib/predicated/to/xml.rb +67 -0
  20. data/lib/predicated/version.rb +2 -2
  21. data/test/canonical_transform_cases.rb +67 -0
  22. data/test/constrain_test.rb +20 -11
  23. data/test/enumerable_test.rb +6 -34
  24. data/test/equality_test.rb +15 -4
  25. data/test/evaluate_test.rb +31 -26
  26. data/test/from/callable_object_canonical_test.rb +43 -0
  27. data/test/from/callable_object_test.rb +16 -40
  28. data/test/from/json_test.rb +83 -0
  29. data/test/from/ruby_code_string_canonical_test.rb +37 -0
  30. data/test/from/ruby_code_string_test.rb +103 -0
  31. data/test/from/{url_fragment_parser_test.rb → url_part_parser_test.rb} +20 -13
  32. data/test/from/url_part_test.rb +48 -0
  33. data/test/from/xml_test.rb +57 -0
  34. data/test/json_conversion_test.rb +33 -0
  35. data/test/print_test.rb +26 -25
  36. data/test/selectable_test.rb +123 -0
  37. data/test/simple_templated_predicate_test.rb +39 -0
  38. data/test/suite.rb +2 -4
  39. data/test/test_helper.rb +26 -4
  40. data/test/test_helper_with_wrong.rb +3 -2
  41. data/test/to/arel_test.rb +71 -31
  42. data/test/to/json_test.rb +74 -0
  43. data/test/to/sentence_test.rb +41 -34
  44. data/test/to/solr_test.rb +39 -0
  45. data/test/to/xml_test.rb +72 -0
  46. data/test/xml_conversion_test.rb +34 -0
  47. metadata +44 -16
  48. data/lib/predicated/selector.rb +0 -55
  49. data/test/from/ruby_string_test.rb +0 -135
  50. data/test/from/url_fragment_test.rb +0 -37
  51. data/test/selector_test.rb +0 -82
@@ -1,8 +1,190 @@
1
- Predicated is a simple predicate model for Ruby.
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
 
@@ -1,6 +1,4 @@
1
1
  $LOAD_PATH.unshift(File.dirname(__FILE__))
2
2
 
3
3
  require "predicated/gem_check"
4
- require "predicated/predicate"
5
- require "predicated/evaluate"
6
- require "predicated/constrain"
4
+ require "predicated/predicate"
@@ -1,3 +1,5 @@
1
+ require "predicated/selectable"
2
+
1
3
  module Predicated
2
4
  class Constraints
3
5
  def initialize
@@ -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
- left.send(@method_sym, *right)
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 inspect
40
- "Call(#{self.send(:part_inspect,left)}.#{method_sym.to_s}(#{self.send(:part_inspect, right)}))"
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
- Predicate.module_eval(%{
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
- end
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/ruby_string"
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
- module Predicate
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
- from_ruby_string(TranslateToRubyString.convert(callable_object), context)
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
- ruby_string = Ruby2Ruby.translate(temp_class, :serializable)
36
- ruby_string.sub(/^def serializable\n /, "").sub(/\nend$/, "")
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
- module Predicate
10
- def self.from_ruby_string(ruby_predicate_string, context=binding())
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
- eval(Ruby2Ruby.new.process(upper_sexp), @context)
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 = eval(Ruby2Ruby.new.process(left_sexp), @context)
40
- right = eval(Ruby2Ruby.new.process(right_sexp), @context)
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
- module Predicate
8
- def self.from_url_fragment(url_fragment_string)
9
- TreetopUrlFragmentParser.new.parse(url_fragment_string).to_predicate
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 TreetopUrlFragment
13
+ module TreetopUrlPart
14
14
  Treetop.load_from_string(%{
15
15
 
16
- grammar TreetopUrlFragment
16
+ grammar TreetopUrlPart
17
17
 
18
- include Predicated::TreetopUrlFragment
18
+ include Predicated::TreetopUrlPart
19
19
 
20
20
  rule or
21
- ( and "|" or <OrNode>) / and
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[1] end
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