smart_tuple 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.html ADDED
@@ -0,0 +1,294 @@
1
+ <head>
2
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
3
+ <link href="dev/github.css" rel="stylesheet" type="text/css" />
4
+ </head>
5
+
6
+ <h1 id="smarttuple-a-simple-yet-smart-sql-conditions-builder">SmartTuple: A Simple Yet Smart SQL Conditions Builder</h1>
7
+
8
+ <h2 id="introduction">Introduction</h2>
9
+
10
+ <p>Sometimes we need to build SQL WHERE statements which are compound or conditional by nature. SmartTuple simplifies this task by letting us build statements of virtually unlimited complexity out of smaller ones.</p>
11
+
12
+ <p>SmartTuple is suitable for use with Ruby on Rails (ActiveRecord) and other Ruby frameworks and ORMs.</p>
13
+
14
+ <h2 id="setup">Setup</h2>
15
+
16
+ <pre><code>gem install smart_tuple
17
+ </code></pre>
18
+
19
+ <p>In your app's <code>config/environment.rb</code> do a:</p>
20
+
21
+ <pre><code>config.gem &quot;smart_tuple&quot;
22
+ </code></pre>
23
+
24
+ <h2 id="kickstart-demo">Kickstart Demo</h2>
25
+
26
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;)
27
+ tup &lt;&lt; {:brand =&gt; params[:brand]} if params[:brand].present?
28
+ tup &lt;&lt; [&quot;min_price &gt;= ?&quot;, params[:min_price]] if params[:min_price].present?
29
+ tup &lt;&lt; [&quot;max_price &lt;= ?&quot;, params[:max_price]] if params[:max_price].present?
30
+
31
+ @phones = Phone.find(:all, :conditions =&gt; tup.compile)
32
+ </code></pre>
33
+
34
+ <p>There's a number of ways you can use SmartTuple depending on the situation. They are covered in the tutorial below.</p>
35
+
36
+ <h2 id="tutorial">Tutorial</h2>
37
+
38
+ <p>Suppose we've got a mobile phone catalog with a search form. We are starting with a price filter of two values: <code>min_price</code> and <code>max_price</code>, both optional.</p>
39
+
40
+ <p>Filter logic:</p>
41
+
42
+ <ul>
43
+ <li>If the user hasn't input anything, the filter has no conditions (allows any record).</li>
44
+ <li>If the user has input <code>min_price</code>, it's used in filter condition.</li>
45
+ <li>If the user has input <code>max_price</code>, it's used in filter condition.</li>
46
+ <li>If the user has input <code>min_price</code> and <code>max_price</code>, they both are used in filter condition.</li>
47
+ </ul>
48
+
49
+ <p>Suppose the HTML form passed to a controller results in a <code>params</code> hash:</p>
50
+
51
+ <pre><code>params[:min_price] = 100 # Can be blank.
52
+ params[:max_price] = 300 # Can be blank.
53
+ </code></pre>
54
+
55
+ <p>Now let's write condition-building code:</p>
56
+
57
+ <pre><code># Start by creating a tuple whose statements are glued with &quot; AND &quot;.
58
+ tup = SmartTuple.new(&quot; AND &quot;)
59
+
60
+ # If min_price is not blank, append its statement.
61
+ if params[:min_price].present?
62
+ tup &lt;&lt; [&quot;min_price &gt;= ?&quot;, params[:min_price]]
63
+ end
64
+
65
+ # Same for max_price.
66
+ if params[:max_price].present?
67
+ tup &lt;&lt; [&quot;max_price &lt;= ?&quot;, params[:max_price]]
68
+ end
69
+
70
+ # Finally, fire up the query.
71
+ @phones = Phone.find(:all, {:conditions =&gt; tup.compile})
72
+ </code></pre>
73
+
74
+ <p>That's basically it. Now let's see how different <code>params</code> values affect the resulting <code>:conditions</code> value. Labelled <strong>p</strong> and <strong>c</strong> in this and following listings:</p>
75
+
76
+ <pre><code>p: {}
77
+ c: []
78
+
79
+ p: {:max_price=&gt;300}
80
+ c: [&quot;max_price &lt;= ?&quot;, 300]
81
+
82
+ p: {:min_price=&gt;100, :max_price=&gt;300}
83
+ c: [&quot;min_price &gt;= ? AND max_price &lt;= ?&quot;, 100, 300]
84
+ </code></pre>
85
+
86
+ <h3 id="plus-another-condition">Plus Another Condition</h3>
87
+
88
+ <p>Let's make things a bit more user-friendly. Let user filter phones by brand. We do it by adding another field, let's call it <code>brand</code>, bearing a straight string value (that's just a simple tutorial, remember?).</p>
89
+
90
+ <p>Our <code>params</code> now becomes something like:</p>
91
+
92
+ <pre><code>params[:brand] = &quot;Nokia&quot; # Can be blank.
93
+ params[:min_price] = 100 # Can be blank.
94
+ params[:max_price] = 300 # Can be blank.
95
+ </code></pre>
96
+
97
+ <p>Let's build a tuple:</p>
98
+
99
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;) +
100
+ ({:brand =&gt; params[:brand]} if params[:brand].present?) +
101
+ ([&quot;min_price &gt;= ?&quot;, params[:min_price]] if params[:min_price].present?) +
102
+ ([&quot;max_price &lt;= ?&quot;, params[:max_price]] if params[:max_price].present?)
103
+ </code></pre>
104
+
105
+ <p>The above code shows that we can construct ready-made tuples with a single expression, using <code>+</code> operator. Also, if a condition is an equality test, we can use Hash notation: <code>{:brand =&gt; params[:brand]}</code>.</p>
106
+
107
+ <p>A quick look at <code>params</code> and <code>:conditions</code>, again:</p>
108
+
109
+ <pre><code>p: {:brand=&gt;&quot;Nokia&quot;}
110
+ c: [&quot;brand = ?&quot;, &quot;Nokia&quot;]
111
+
112
+ p: {:brand=&gt;&quot;Nokia&quot;, :max_price=&gt;300}
113
+ c: [&quot;brand = ? AND max_price &lt;= ?&quot;, &quot;Nokia&quot;, 300]
114
+
115
+ p: {:brand=&gt;&quot;Nokia&quot;, :min_price=&gt;100, :max_price=&gt;300}
116
+ c: [&quot;brand = ? AND min_price &gt;= ? AND max_price &lt;= ?&quot;, &quot;Nokia&quot;, 100, 300]
117
+ </code></pre>
118
+
119
+ <h3 id="we-want-more">We Want More!</h3>
120
+
121
+ <p>Since we now see how easy it's to build compound conditions, we decide to further extend our search form. Now we want to:</p>
122
+
123
+ <ul>
124
+ <li>Let user specify more than 1 brand.</li>
125
+ <li>Let user specify a selection of colors.</li>
126
+ </ul>
127
+
128
+ <p>From <code>params</code> perspective that's something like:</p>
129
+
130
+ <pre><code>params[:brands] = [&quot;Nokia&quot;, &quot;Motorola&quot;] # Can be blank.
131
+ params[:min_price] = 100 # Can be blank.
132
+ params[:max_price] = 300 # Can be blank.
133
+ params[:colors] = [&quot;Black&quot;, &quot;Silver&quot;, &quot;Pink&quot;] # Can be blank.
134
+ </code></pre>
135
+
136
+ <p>Quite obvious is that supplied values for brands and colors should be OR'ed. We're now facing the task of creating a &quot;sub-tuple&quot;, e.g. to match brand, and then merging this sub-tuple into main tuple. Doing it straight is something like:</p>
137
+
138
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;)
139
+
140
+ if params[:brands].present?
141
+ subtup = SmartTuple.new(&quot; OR &quot;)
142
+ params[:brands].each {|brand| subtup &lt;&lt; [&quot;brand = ?&quot;, brand]}
143
+ tup &lt;&lt; subtup
144
+ end
145
+ </code></pre>
146
+
147
+ <p>Or, in a smarter way by utilizing <code>#add_each</code> method:</p>
148
+
149
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;)
150
+ tup &lt;&lt; SmartTuple.new(&quot; OR &quot;).add_each(params[:brands]) {|v| [&quot;brand = ?&quot;, v]} if params[:brands].present?
151
+ </code></pre>
152
+
153
+ <p>The final query:</p>
154
+
155
+ <pre><code>Phone.find(:all, {:conditions =&gt; [SmartTuple.new(&quot; AND &quot;),
156
+ (SmartTuple.new(&quot; OR &quot;).add_each(params[:brands]) {|v| [&quot;brand = ?&quot;, v]} if params[:brands].present?),
157
+ ([&quot;min_price &gt;= ?&quot;, params[:min_price]] if params[:min_price].present?),
158
+ ([&quot;max_price &lt;= ?&quot;, params[:max_price]] if params[:max_price].present?),
159
+ (SmartTuple.new(&quot; OR &quot;).add_each(params[:colors]) {|v| [&quot;color = ?&quot;, v]} if params[:colors].present?),
160
+ ].sum.compile})
161
+ </code></pre>
162
+
163
+ <blockquote>
164
+ <p>NOTE: In the above sample I've used <code>Array#sum</code> (available in ActiveSupport) instead of <code>+</code> to add statements to the tuple. I prefer to write it like this since it allows to comment and swap lines without breaking the syntax.</p>
165
+ </blockquote>
166
+
167
+ <p>Checking out <code>params</code> and <code>:conditions</code>:</p>
168
+
169
+ <pre><code>p: {:brands=&gt;[&quot;Nokia&quot;], :max_price=&gt;300}
170
+ c: [&quot;brand = ? AND max_price &lt;= ?&quot;, &quot;Nokia&quot;, 300]
171
+
172
+ p: {:brands=&gt;[&quot;Nokia&quot;, &quot;Motorola&quot;], :max_price=&gt;300}
173
+ c: [&quot;(brand = ? OR brand = ?) AND max_price &lt;= ?&quot;, &quot;Nokia&quot;, &quot;Motorola&quot;, 300]
174
+ ^-- ^-- note the auto brackets
175
+
176
+ p: {:brands=&gt;[&quot;Nokia&quot;, &quot;Motorola&quot;], :max_price=&gt;300, :colors=&gt;[&quot;Black&quot;]}
177
+ c: [&quot;(brand = ? OR brand = ?) AND max_price &lt;= ? AND color = ?&quot;, &quot;Nokia&quot;, &quot;Motorola&quot;, 300, &quot;Black&quot;]
178
+
179
+ p: {:brands=&gt;[&quot;Nokia&quot;, &quot;Motorola&quot;], :colors=&gt;[&quot;Black&quot;, &quot;Silver&quot;, &quot;Pink&quot;]}
180
+ c: [&quot;(brand = ? OR brand = ?) AND (color = ? OR color = ? OR color = ?)&quot;, &quot;Nokia&quot;, &quot;Motorola&quot;, &quot;Black&quot;, &quot;Silver&quot;, &quot;Pink&quot;]
181
+ </code></pre>
182
+
183
+ <p>That's the end of our tutorial. Hope now you've got an idea of what SmartTuple is.</p>
184
+
185
+ <h2 id="api-summary">API Summary</h2>
186
+
187
+ <p>Here's a brief cheatsheet, which outlines main SmartTuple features.</p>
188
+
189
+ <h3 id="appending-statements">Appending Statements</h3>
190
+
191
+ <pre><code># Array.
192
+ tup &lt;&lt; [&quot;brand = ?&quot;, &quot;Nokia&quot;]
193
+ tup &lt;&lt; [&quot;brand = ? AND color = ?&quot;, &quot;Nokia&quot;, &quot;Black&quot;]
194
+
195
+ # Hash.
196
+ tup &lt;&lt; {:brand =&gt; &quot;Nokia&quot;}
197
+ tup &lt;&lt; {:brand =&gt; &quot;Nokia&quot;, :color =&gt; &quot;Black&quot;}
198
+
199
+ # Another SmartTuple.
200
+ tup &lt;&lt; other_tuple
201
+
202
+ # String.
203
+ tup &lt;&lt; &quot;brand IS NULL&quot;
204
+ </code></pre>
205
+
206
+ <p>Appending empty or blank (where appropriate) statements has no effect on the receiver:</p>
207
+
208
+ <pre><code>tup &lt;&lt; nil
209
+ tup &lt;&lt; []
210
+ tup &lt;&lt; {}
211
+ tup &lt;&lt; an_empty_tuple
212
+ tup &lt;&lt; &quot;&quot;
213
+ tup &lt;&lt; &quot; &quot; # Will be treated as blank if ActiveSupport is on.
214
+ </code></pre>
215
+
216
+ <p>Another way to append something is to use <code>+</code>.</p>
217
+
218
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;) + {:brand =&gt; &quot;Nokia&quot;} + [&quot;max_price &lt;= ?&quot;, 300]
219
+ </code></pre>
220
+
221
+ <p>Appending one statement per each collection item is easy through <code>#add_each</code>:</p>
222
+
223
+ <pre><code>tup.add_each([&quot;Nokia&quot;, &quot;Motorola&quot;]) {|v| [&quot;brand = ?&quot;, v]}
224
+ </code></pre>
225
+
226
+ <p>The latter can be made conditional. Remember, appending <code>nil</code> has no effect on the receiving tuple, which gives us freedom to use conditions whenever we want to:</p>
227
+
228
+ <pre><code>tup.add_each([&quot;Nokia&quot;, &quot;Motorola&quot;]) do |v|
229
+ [&quot;brand = ?&quot;, v] if v =~ /^Moto/
230
+ end
231
+ </code></pre>
232
+
233
+ <h3 id="bracketing-the-statements-always-never-and-auto">Bracketing the Statements: Always, Never and Auto</h3>
234
+
235
+ <p><em>This chapter still has to be written.</em></p>
236
+
237
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;)
238
+ tup.brackets
239
+ =&gt; :auto
240
+
241
+ tup.brackets = true
242
+ tup.brackets = false
243
+ tup.brackets = :auto
244
+ </code></pre>
245
+
246
+ <h3 id="clearing">Clearing</h3>
247
+
248
+ <p>To put tuple into its initial state, do a:</p>
249
+
250
+ <pre><code>tup.clear
251
+ </code></pre>
252
+
253
+ <h3 id="compiling">Compiling</h3>
254
+
255
+ <p>Compiling is converting the tuple into something suitable for use as <code>:conditions</code> of an ActiveRecord call.</p>
256
+
257
+ <p>It's as straight as:</p>
258
+
259
+ <pre><code>tup.compile
260
+ tup.to_a # An alias, does the same.
261
+
262
+ # Go fetch!
263
+ Phone.find(:all, :conditions =&gt; tup.compile)
264
+ </code></pre>
265
+
266
+ <h3 id="contents-and-size">Contents and Size</h3>
267
+
268
+ <p>You can examine tuple's state with methods often found in other Ruby classes: <code>#empty?</code>, <code>#size</code>, and attribute accessors <code>#statements</code> and <code>#args</code>.</p>
269
+
270
+ <pre><code>tup = SmartTuple.new(&quot; AND &quot;)
271
+ tup.empty?
272
+ =&gt; true
273
+ tup.size
274
+ =&gt; 0
275
+
276
+ tup &lt;&lt; [&quot;brand = ?&quot;, &quot;Nokia&quot;]
277
+ tup.empty?
278
+ =&gt; false
279
+ tup.size
280
+ =&gt; 1
281
+
282
+ tup &lt;&lt; [&quot;max_price &gt;= ?&quot;, 300]
283
+ tup.size
284
+ =&gt; 2
285
+
286
+ tup.statements
287
+ =&gt; [&quot;brand = ?&quot;, &quot;max_price &gt;= ?&quot;]
288
+ tup.args
289
+ =&gt; [&quot;Nokia&quot;, 300]
290
+ </code></pre>
291
+
292
+ <h2 id="feedback">Feedback</h2>
293
+
294
+ <p>Send bug reports, suggestions and criticisms through <a href="http://github.com/dadooda/smart_tuple">project's page on GitHub</a>.</p>
data/README.md ADDED
@@ -0,0 +1,278 @@
1
+
2
+ SmartTuple: A Simple Yet Smart SQL Conditions Builder
3
+ =====================================================
4
+
5
+
6
+ Introduction
7
+ ------------
8
+
9
+ Sometimes we need to build SQL WHERE statements which are compound or conditional by nature. SmartTuple simplifies this task by letting us build statements of virtually unlimited complexity out of smaller ones.
10
+
11
+ SmartTuple is suitable for use with Ruby on Rails (ActiveRecord) and other Ruby frameworks and ORMs.
12
+
13
+
14
+ Setup
15
+ -----
16
+
17
+ gem install smart_tuple
18
+
19
+ In your app's `config/environment.rb` do a:
20
+
21
+ config.gem "smart_tuple"
22
+
23
+
24
+ Kickstart Demo
25
+ --------------
26
+
27
+ tup = SmartTuple.new(" AND ")
28
+ tup << {:brand => params[:brand]} if params[:brand].present?
29
+ tup << ["min_price >= ?", params[:min_price]] if params[:min_price].present?
30
+ tup << ["max_price <= ?", params[:max_price]] if params[:max_price].present?
31
+
32
+ @phones = Phone.find(:all, :conditions => tup.compile)
33
+
34
+ There's a number of ways you can use SmartTuple depending on the situation. They are covered in the tutorial below.
35
+
36
+
37
+ Tutorial
38
+ --------
39
+
40
+ Suppose we've got a mobile phone catalog with a search form. We are starting with a price filter of two values: `min_price` and `max_price`, both optional.
41
+
42
+ Filter logic:
43
+
44
+ * If the user hasn't input anything, the filter has no conditions (allows any record).
45
+ * If the user has input `min_price`, it's used in filter condition.
46
+ * If the user has input `max_price`, it's used in filter condition.
47
+ * If the user has input `min_price` and `max_price`, they both are used in filter condition.
48
+
49
+ Suppose the HTML form passed to a controller results in a `params` hash:
50
+
51
+ params[:min_price] = 100 # Can be blank.
52
+ params[:max_price] = 300 # Can be blank.
53
+
54
+ Now let's write condition-building code:
55
+
56
+ # Start by creating a tuple whose statements are glued with " AND ".
57
+ tup = SmartTuple.new(" AND ")
58
+
59
+ # If min_price is not blank, append its statement.
60
+ if params[:min_price].present?
61
+ tup << ["min_price >= ?", params[:min_price]]
62
+ end
63
+
64
+ # Same for max_price.
65
+ if params[:max_price].present?
66
+ tup << ["max_price <= ?", params[:max_price]]
67
+ end
68
+
69
+ # Finally, fire up the query.
70
+ @phones = Phone.find(:all, {:conditions => tup.compile})
71
+
72
+ That's basically it. Now let's see how different `params` values affect the resulting `:conditions` value. Labelled **p** and **c** in this and following listings:
73
+
74
+ p: {}
75
+ c: []
76
+
77
+ p: {:max_price=>300}
78
+ c: ["max_price <= ?", 300]
79
+
80
+ p: {:min_price=>100, :max_price=>300}
81
+ c: ["min_price >= ? AND max_price <= ?", 100, 300]
82
+
83
+ ### Plus Another Condition ###
84
+
85
+ Let's make things a bit more user-friendly. Let user filter phones by brand. We do it by adding another field, let's call it `brand`, bearing a straight string value (that's just a simple tutorial, remember?).
86
+
87
+ Our `params` now becomes something like:
88
+
89
+ params[:brand] = "Nokia" # Can be blank.
90
+ params[:min_price] = 100 # Can be blank.
91
+ params[:max_price] = 300 # Can be blank.
92
+
93
+ Let's build a tuple:
94
+
95
+ tup = SmartTuple.new(" AND ") +
96
+ ({:brand => params[:brand]} if params[:brand].present?) +
97
+ (["min_price >= ?", params[:min_price]] if params[:min_price].present?) +
98
+ (["max_price <= ?", params[:max_price]] if params[:max_price].present?)
99
+
100
+ The above code shows that we can construct ready-made tuples with a single expression, using `+` operator. Also, if a condition is an equality test, we can use Hash notation: `{:brand => params[:brand]}`.
101
+
102
+ A quick look at `params` and `:conditions`, again:
103
+
104
+ p: {:brand=>"Nokia"}
105
+ c: ["brand = ?", "Nokia"]
106
+
107
+ p: {:brand=>"Nokia", :max_price=>300}
108
+ c: ["brand = ? AND max_price <= ?", "Nokia", 300]
109
+
110
+ p: {:brand=>"Nokia", :min_price=>100, :max_price=>300}
111
+ c: ["brand = ? AND min_price >= ? AND max_price <= ?", "Nokia", 100, 300]
112
+
113
+ ### We Want More! ###
114
+
115
+ Since we now see how easy it's to build compound conditions, we decide to further extend our search form. Now we want to:
116
+
117
+ * Let user specify more than 1 brand.
118
+ * Let user specify a selection of colors.
119
+
120
+ From `params` perspective that's something like:
121
+
122
+ params[:brands] = ["Nokia", "Motorola"] # Can be blank.
123
+ params[:min_price] = 100 # Can be blank.
124
+ params[:max_price] = 300 # Can be blank.
125
+ params[:colors] = ["Black", "Silver", "Pink"] # Can be blank.
126
+
127
+ Quite obvious is that supplied values for brands and colors should be OR'ed. We're now facing the task of creating a "sub-tuple", e.g. to match brand, and then merging this sub-tuple into main tuple. Doing it straight is something like:
128
+
129
+ tup = SmartTuple.new(" AND ")
130
+
131
+ if params[:brands].present?
132
+ subtup = SmartTuple.new(" OR ")
133
+ params[:brands].each {|brand| subtup << ["brand = ?", brand]}
134
+ tup << subtup
135
+ end
136
+
137
+ Or, in a smarter way by utilizing `#add_each` method:
138
+
139
+ tup = SmartTuple.new(" AND ")
140
+ tup << SmartTuple.new(" OR ").add_each(params[:brands]) {|v| ["brand = ?", v]} if params[:brands].present?
141
+
142
+ The final query:
143
+
144
+ Phone.find(:all, {:conditions => [SmartTuple.new(" AND "),
145
+ (SmartTuple.new(" OR ").add_each(params[:brands]) {|v| ["brand = ?", v]} if params[:brands].present?),
146
+ (["min_price >= ?", params[:min_price]] if params[:min_price].present?),
147
+ (["max_price <= ?", params[:max_price]] if params[:max_price].present?),
148
+ (SmartTuple.new(" OR ").add_each(params[:colors]) {|v| ["color = ?", v]} if params[:colors].present?),
149
+ ].sum.compile})
150
+
151
+ > NOTE: In the above sample I've used `Array#sum` (available in ActiveSupport) instead of `+` to add statements to the tuple. I prefer to write it like this since it allows to comment and swap lines without breaking the syntax.
152
+
153
+ Checking out `params` and `:conditions`:
154
+
155
+ p: {:brands=>["Nokia"], :max_price=>300}
156
+ c: ["brand = ? AND max_price <= ?", "Nokia", 300]
157
+
158
+ p: {:brands=>["Nokia", "Motorola"], :max_price=>300}
159
+ c: ["(brand = ? OR brand = ?) AND max_price <= ?", "Nokia", "Motorola", 300]
160
+ ^-- ^-- note the auto brackets
161
+
162
+ p: {:brands=>["Nokia", "Motorola"], :max_price=>300, :colors=>["Black"]}
163
+ c: ["(brand = ? OR brand = ?) AND max_price <= ? AND color = ?", "Nokia", "Motorola", 300, "Black"]
164
+
165
+ p: {:brands=>["Nokia", "Motorola"], :colors=>["Black", "Silver", "Pink"]}
166
+ c: ["(brand = ? OR brand = ?) AND (color = ? OR color = ? OR color = ?)", "Nokia", "Motorola", "Black", "Silver", "Pink"]
167
+
168
+ That's the end of our tutorial. Hope now you've got an idea of what SmartTuple is.
169
+
170
+
171
+ API Summary
172
+ -----------
173
+
174
+ Here's a brief cheatsheet, which outlines main SmartTuple features.
175
+
176
+ ### Appending Statements ###
177
+
178
+ # Array.
179
+ tup << ["brand = ?", "Nokia"]
180
+ tup << ["brand = ? AND color = ?", "Nokia", "Black"]
181
+
182
+ # Hash.
183
+ tup << {:brand => "Nokia"}
184
+ tup << {:brand => "Nokia", :color => "Black"}
185
+
186
+ # Another SmartTuple.
187
+ tup << other_tuple
188
+
189
+ # String.
190
+ tup << "brand IS NULL"
191
+
192
+ Appending empty or blank (where appropriate) statements has no effect on the receiver:
193
+
194
+ tup << nil
195
+ tup << []
196
+ tup << {}
197
+ tup << an_empty_tuple
198
+ tup << ""
199
+ tup << " " # Will be treated as blank if ActiveSupport is on.
200
+
201
+ Another way to append something is to use `+`.
202
+
203
+ tup = SmartTuple.new(" AND ") + {:brand => "Nokia"} + ["max_price <= ?", 300]
204
+
205
+ Appending one statement per each collection item is easy through `#add_each`:
206
+
207
+ tup.add_each(["Nokia", "Motorola"]) {|v| ["brand = ?", v]}
208
+
209
+ The latter can be made conditional. Remember, appending `nil` has no effect on the receiving tuple, which gives us freedom to use conditions whenever we want to:
210
+
211
+ tup.add_each(["Nokia", "Motorola"]) do |v|
212
+ ["brand = ?", v] if v =~ /^Moto/
213
+ end
214
+
215
+
216
+ ### Bracketing the Statements: Always, Never and Auto ###
217
+
218
+ _This chapter still has to be written._
219
+
220
+ tup = SmartTuple.new(" AND ")
221
+ tup.brackets
222
+ => :auto
223
+
224
+ tup.brackets = true
225
+ tup.brackets = false
226
+ tup.brackets = :auto
227
+
228
+
229
+ ### Clearing ###
230
+
231
+ To put tuple into its initial state, do a:
232
+
233
+ tup.clear
234
+
235
+
236
+ ### Compiling ###
237
+
238
+ Compiling is converting the tuple into something suitable for use as `:conditions` of an ActiveRecord call.
239
+
240
+ It's as straight as:
241
+
242
+ tup.compile
243
+ tup.to_a # An alias, does the same.
244
+
245
+ # Go fetch!
246
+ Phone.find(:all, :conditions => tup.compile)
247
+
248
+
249
+ ### Contents and Size ###
250
+
251
+ You can examine tuple's state with methods often found in other Ruby classes: `#empty?`, `#size`, and attribute accessors `#statements` and `#args`.
252
+
253
+ tup = SmartTuple.new(" AND ")
254
+ tup.empty?
255
+ => true
256
+ tup.size
257
+ => 0
258
+
259
+ tup << ["brand = ?", "Nokia"]
260
+ tup.empty?
261
+ => false
262
+ tup.size
263
+ => 1
264
+
265
+ tup << ["max_price >= ?", 300]
266
+ tup.size
267
+ => 2
268
+
269
+ tup.statements
270
+ => ["brand = ?", "max_price >= ?"]
271
+ tup.args
272
+ => ["Nokia", 300]
273
+
274
+
275
+ Feedback
276
+ --------
277
+
278
+ Send bug reports, suggestions and criticisms through [project's page on GitHub](http://github.com/dadooda/smart_tuple).
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), "lib/**/*.rb")].each do |fn|
2
+ require fn
3
+ end
@@ -0,0 +1,186 @@
1
+ class SmartTuple
2
+ attr_reader :args
3
+ attr_reader :brackets
4
+ attr_accessor :glue
5
+ attr_reader :statements
6
+
7
+ # new(" AND ")
8
+ # new(" OR ")
9
+ # new(", ") # E.g. for a SET or UPDATE statement.
10
+ def initialize(glue, attrs = {})
11
+ @glue = glue
12
+ clear
13
+ attrs.each {|k, v| send("#{k}=", v)}
14
+ end
15
+
16
+ # We need it to control #dup behaviour.
17
+ def initialize_copy(src)
18
+ @statements = src.statements.dup
19
+ @args = src.args.dup
20
+ end
21
+
22
+ # NOTE: Alphabetical order below.
23
+
24
+ # Add a sub-statement, return new object. See <tt>#&lt;&lt;</tt>.
25
+ # SmartTuple.new(" AND ") + {:brand => "Nokia"} + ["max_price <= ?", 300]
26
+ def +(sub)
27
+ # Since #<< supports chaining, it boils down to this.
28
+ dup << sub
29
+ end
30
+
31
+ # Append a sub-statement.
32
+ # # Array.
33
+ # tup << ["brand = ?", "Nokia"]
34
+ # tup << ["brand = ? AND color = ?", "Nokia", "Black"]
35
+ #
36
+ # # Hash.
37
+ # tup << {:brand => "Nokia"}
38
+ # tup << {:brand => "Nokia", :color => "Black"}
39
+ #
40
+ # # Another SmartTuple.
41
+ # tup << other_tuple
42
+ #
43
+ # # String. Generally NOT recommended.
44
+ # tup << "brand IS NULL"
45
+ #
46
+ # Appending empty or blank (where appropriate) statements has no effect on the receiver:
47
+ # tup << nil
48
+ # tup << []
49
+ # tup << {}
50
+ # tup << another_empty_tuple
51
+ # tup << ""
52
+ # tup << " " # Will be treated as blank if ActiveSupport is on.
53
+ def <<(sub)
54
+ ##p "self.class", self.class
55
+
56
+ # NOTE: Autobracketing help is placing [value] instead of (value) into @statements. #compile will take it into account.
57
+
58
+ # Chop off everything empty first time.
59
+ if sub.nil? or (sub.empty? rescue false) or (sub.blank? rescue false)
60
+ ##puts "-- empty"
61
+ elsif sub.is_a? String or (sub.acts_like? :string rescue false)
62
+ ##puts "-- is a string"
63
+ @statements << sub.to_s
64
+ elsif sub.is_a? Array
65
+ # NOTE: If sub == [], the execution won't get here.
66
+ # So, we've got at least one element. Therefore stmt will be scalar, and args -- DEFINITELY an array.
67
+ stmt, args = sub[0], sub[1..-1]
68
+ ##p "stmt", stmt
69
+ ##p "args", args
70
+ if not (stmt.nil? or (stmt.empty? rescue false) or (stmt.blank? rescue false))
71
+ ##puts "-- stmt nempty"
72
+ # Help do autobracketing later. Here we can ONLY judge by number of passed arguments.
73
+ @statements << (args.size > 1 ? [stmt] : stmt)
74
+ @args += args
75
+ end
76
+ elsif sub.is_a? Hash
77
+ sub.each do |k, v|
78
+ @statements << "#{k} = ?"
79
+ @args << v
80
+ end
81
+ elsif sub.is_a? self.class
82
+ # NOTE: If sub is empty, the execution won't get here.
83
+
84
+ # Autobrackets here are smarter, than in Array processing case.
85
+ stmt = sub.compile[0]
86
+ @statements << ((sub.size > 1 or sub.args.size > 1) ? [stmt] : stmt)
87
+ @args += sub.args
88
+ else
89
+ raise ArgumentError, "Invalid sub-statement #{sub.inspect}"
90
+ end
91
+
92
+ # Return self, it's IMPORTANT to make chaining possible.
93
+ self
94
+ end
95
+
96
+ # Iterate over collection and add block's result per each record.
97
+ # add_each(brands) do |v|
98
+ # ["brand = ?", v]
99
+ # end
100
+ #
101
+ # Can be conditional:
102
+ # tup.add_each(["Nokia", "Motorola"]) do |v|
103
+ # ["brand = ?", v] if v =~ /^Moto/
104
+ # end
105
+ def add_each(collection, &block)
106
+ raise ArgumentError, "Code block expected" if not block
107
+ ##p "collection", collection
108
+ collection.each do |v|
109
+ self << yield(v)
110
+ end
111
+
112
+ # This is IMPORTANT.
113
+ self
114
+ end
115
+
116
+ # Set bracketing mode.
117
+ # brackets = true # Put brackets around each sub-statement.
118
+ # brackets = false # Don't put brackets.
119
+ # brackets = :auto # Automatically put brackets around compound sub-statements.
120
+ def brackets=(value)
121
+ raise ArgumentError, "Unknown value #{value.inspect}" if not [true, false, :auto].include? value
122
+ @brackets = value
123
+ end
124
+
125
+ # Set self into default state.
126
+ def clear
127
+ @statements = []
128
+ @args = []
129
+ @brackets = :auto
130
+
131
+ # Array does it like this. We do either.
132
+ self
133
+ end
134
+
135
+ # Compile self into an array.
136
+ def compile
137
+ return [] if empty?
138
+
139
+ ##p "@statements", @statements
140
+ ##p "@args", @args
141
+
142
+ # Build "bracketed" statements.
143
+ bsta = @statements.map do |s|
144
+ auto_brackets, scalar_s = s.is_a?(Array) ? [true, s[0]] : [false, s]
145
+
146
+ # Logic:
147
+ # brackets | auto | result
148
+ # ----------|-------|-------
149
+ # true | * | true
150
+ # false | * | false
151
+ # :auto | true | true
152
+ # :auto | false | false
153
+
154
+ brackets = if @statements.size < 2
155
+ # If there are no neighboring statements, there WILL BE NO brackets in any case.
156
+ false
157
+ elsif @brackets == true or @brackets == false
158
+ @brackets
159
+ elsif @brackets == :auto
160
+ auto_brackets
161
+ else
162
+ raise "Unknown @brackets value #{@brackets.inspect}, SE"
163
+ end
164
+
165
+ if brackets
166
+ ["(", scalar_s, ")"].join
167
+ else
168
+ scalar_s
169
+ end
170
+ end
171
+
172
+ [bsta.join(glue)] + @args
173
+ end
174
+ alias_method :to_a, :compile
175
+
176
+ def empty?
177
+ @statements.empty?
178
+ end
179
+
180
+ # Get number of sub-statements.
181
+ def size
182
+ @statements.size
183
+ end
184
+
185
+ # NOTE: Decided not to make #count as an alias to #size. For other classes #count normally is a bit smarter, supports block, etc.
186
+ end
@@ -0,0 +1,46 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{smart_tuple}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Alex Fortuna"]
12
+ s.date = %q{2010-07-18}
13
+ s.description = %q{A Simple Yet Smart SQL Conditions Builder}
14
+ s.email = %q{alex.r@askit.org}
15
+ s.extra_rdoc_files = [
16
+ "README.html",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "README.html",
21
+ "README.md",
22
+ "VERSION.yml",
23
+ "init.rb",
24
+ "lib/smart_tuple.rb",
25
+ "smart_tuple.gemspec"
26
+ ]
27
+ s.homepage = %q{http://github.com/dadooda/smart_tuple}
28
+ s.rdoc_options = ["--charset=UTF-8"]
29
+ s.require_paths = ["lib"]
30
+ s.rubygems_version = %q{1.3.5}
31
+ s.summary = %q{A Simple Yet Smart SQL Conditions Builder}
32
+ s.test_files = [
33
+ "spec/spec_helper.rb",
34
+ "spec/smart_tuple_spec.rb"
35
+ ]
36
+
37
+ if s.respond_to? :specification_version then
38
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
39
+ s.specification_version = 3
40
+
41
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
42
+ else
43
+ end
44
+ else
45
+ end
46
+ end
@@ -0,0 +1,317 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe (klass = SmartTuple) do
4
+ r = nil
5
+ before :each do
6
+ r = klass.new(" AND ")
7
+ end
8
+
9
+ it "is initially empty" do
10
+ r.empty?.should == true
11
+ r.compile.should == []
12
+ r.size.should == 0
13
+ end
14
+
15
+ describe "bracketing logic" do
16
+ it "never puts brackets around a single statement" do
17
+ [true, false, :auto].each do |mode|
18
+ r = klass.new(" AND ", :brackets => mode)
19
+ r << ["age < ?", 25]
20
+ r.compile.should == ["age < ?", 25]
21
+ end
22
+ end
23
+
24
+ describe "[SC1]" do
25
+ before :each do
26
+ r = klass.new(" AND ")
27
+ r << ["is_male = ?", true]
28
+ r << ["age >= ? AND age <= ?", 18, 35]
29
+ end
30
+
31
+ it "works if brackets = true" do
32
+ r.brackets = true
33
+ r.compile.should == ["(is_male = ?) AND (age >= ? AND age <= ?)", true, 18, 35]
34
+ end
35
+
36
+ it "works if brackets = false" do
37
+ r.brackets = false
38
+ r.compile.should == ["is_male = ? AND age >= ? AND age <= ?", true, 18, 35]
39
+ end
40
+
41
+ it "works if brackets = :auto" do
42
+ r.brackets = :auto
43
+ r.compile.should == ["is_male = ? AND (age >= ? AND age <= ?)", true, 18, 35]
44
+ end
45
+ end
46
+
47
+ describe "[SC1.1]" do
48
+ # NOTE: This SC has brackets DELIBERATELY undetected.
49
+ before :each do
50
+ r = klass.new(" AND ")
51
+ r << ["is_male = 1"]
52
+ r << ["age >= 18 AND age <= 35"]
53
+ end
54
+
55
+ it "works if brackets = true" do
56
+ r.brackets = true
57
+ r.compile.should == ["(is_male = 1) AND (age >= 18 AND age <= 35)"]
58
+ end
59
+
60
+ it "works if brackets = false" do
61
+ r.brackets = false
62
+ r.compile.should == ["is_male = 1 AND age >= 18 AND age <= 35"]
63
+ end
64
+
65
+ it "works if brackets = :auto" do
66
+ r.brackets = :auto
67
+ r.compile.should == ["is_male = 1 AND age >= 18 AND age <= 35"]
68
+ end
69
+ end
70
+
71
+ describe "[SC2]" do
72
+ before :each do
73
+ r = klass.new(" AND ")
74
+ r << ["is_male = ?", true]
75
+ r << klass.new(" AND ") + ["age >= ?", 18] + ["age <= ?", 35] # stmt: 1+1, args: 1+1
76
+ end
77
+
78
+ it "works if brackets = true" do
79
+ r.brackets = true
80
+ r.compile.should == ["(is_male = ?) AND (age >= ? AND age <= ?)", true, 18, 35]
81
+ end
82
+
83
+ it "works if brackets = false" do
84
+ r.brackets = false
85
+ r.compile.should == ["is_male = ? AND age >= ? AND age <= ?", true, 18, 35]
86
+ end
87
+
88
+ it "works if brackets = :auto" do
89
+ r.brackets = :auto
90
+ r.compile.should == ["is_male = ? AND (age >= ? AND age <= ?)", true, 18, 35]
91
+ end
92
+ end
93
+
94
+ describe "[SC2.1]" do
95
+ before :each do
96
+ r = klass.new(" AND ")
97
+ r << ["is_male = ?", true]
98
+ r << klass.new(" AND ") + ["age >= ? AND age <= ?", 18, 35] # stmt: 1, args: 2
99
+ end
100
+
101
+ it "works if brackets = true" do
102
+ r.brackets = true
103
+ r.compile.should == ["(is_male = ?) AND (age >= ? AND age <= ?)", true, 18, 35]
104
+ end
105
+
106
+ it "works if brackets = false" do
107
+ r.brackets = false
108
+ r.compile.should == ["is_male = ? AND age >= ? AND age <= ?", true, 18, 35]
109
+ end
110
+
111
+ it "works if brackets = :auto" do
112
+ r.brackets = :auto
113
+ r.compile.should == ["is_male = ? AND (age >= ? AND age <= ?)", true, 18, 35]
114
+ end
115
+ end
116
+
117
+ describe "[SC2.2]" do
118
+ before :each do
119
+ r = klass.new(" AND ")
120
+ r << ["is_male = ?", true]
121
+ r << klass.new(" AND ") + "age >= 18" + "age <= 35" # stmt: 1+1, args: 0
122
+ end
123
+
124
+ it "works if brackets = true" do
125
+ r.brackets = true
126
+ r.compile.should == ["(is_male = ?) AND (age >= 18 AND age <= 35)", true]
127
+ end
128
+
129
+ it "works if brackets = false" do
130
+ r.brackets = false
131
+ r.compile.should == ["is_male = ? AND age >= 18 AND age <= 35", true]
132
+ end
133
+
134
+ it "works if brackets = :auto" do
135
+ r.brackets = :auto
136
+ r.compile.should == ["is_male = ? AND (age >= 18 AND age <= 35)", true]
137
+ end
138
+ end
139
+
140
+ describe "[SC2.3]" do
141
+ # NOTE: This SC has brackets DELIBERATELY undetected.
142
+ before :each do
143
+ r = klass.new(" AND ")
144
+ r << ["is_male = ?", true]
145
+ r << klass.new(" AND ") + ["age >= 18 AND age <= 35"] # stmt: 1, args: 0
146
+ end
147
+
148
+ it "works if brackets = true" do
149
+ r.brackets = true
150
+ r.compile.should == ["(is_male = ?) AND (age >= 18 AND age <= 35)", true]
151
+ end
152
+
153
+ it "works if brackets = false" do
154
+ r.brackets = false
155
+ r.compile.should == ["is_male = ? AND age >= 18 AND age <= 35", true]
156
+ end
157
+
158
+ it "works if brackets = :auto" do
159
+ r.brackets = :auto
160
+ r.compile.should == ["is_male = ? AND age >= 18 AND age <= 35", true]
161
+ end
162
+ end
163
+ end # bracketing logic
164
+
165
+ #--------------------------------------- Method tests
166
+
167
+ # NOTE: Alphabetical order, except for #initialize.
168
+
169
+ describe "#initialize" do
170
+ it "requires an argument" do
171
+ Proc.new do
172
+ klass.new
173
+ end.should raise_error ArgumentError
174
+
175
+ Proc.new do
176
+ klass.new(" AND ")
177
+ end.should_not raise_error
178
+ end
179
+ end # #initialize
180
+
181
+ describe "#+" do
182
+ it "returns a copy" do
183
+ [nil, "created_at IS NULL", ["is_pirate = ?", true]].each do |arg|
184
+ r = klass.new(" AND ")
185
+ #(r + arg).object_id.should_not == r.object_id
186
+ (r + arg).should_not eql r
187
+ end
188
+ end
189
+ end # #+
190
+
191
+ describe "#<<" do
192
+ it "ignores nil/empty/blank objects" do
193
+ objs = []
194
+ objs << nil
195
+ objs << ""
196
+ objs << " " if "".respond_to? :blank?
197
+ objs << []
198
+ objs << {}
199
+
200
+ objs.each do |obj|
201
+ r = klass.new(" AND ")
202
+ r << "field IS NULL"
203
+ compile_before = r.compile
204
+ r << obj
205
+ r.compile.should == compile_before
206
+ end
207
+ end
208
+
209
+ it "returns self" do
210
+ (r << ["is_male = ?", true]).should eql r
211
+ end
212
+
213
+ it "supports chaining" do
214
+ r << ["age >= ?", 18] << "created_at IS NULL"
215
+ r.compile.should == ["age >= ? AND created_at IS NULL", 18]
216
+ end
217
+
218
+ it "supports String" do
219
+ r << "field1 IS NULL"
220
+ r << "field2 IS NOT NULL"
221
+ r.compile.should == ["field1 IS NULL AND field2 IS NOT NULL"]
222
+ end
223
+
224
+ it "supports Array" do
225
+ r << ["name = ?", "John"]
226
+ r << ["age < ?", 25]
227
+ r.compile.should == ["name = ? AND age < ?", "John", 25]
228
+
229
+ r = klass.new(" AND ")
230
+ r << ["name = ? OR age = ?", "John", 25]
231
+ r.compile.should == ["name = ? OR age = ?", "John", 25]
232
+
233
+ r = klass.new(" AND ")
234
+ r << ["is_male = ?", true]
235
+ r << ["name = ? OR age = ?", "John", 25]
236
+ r.compile.should == ["is_male = ? AND (name = ? OR age = ?)", true, "John", 25]
237
+ end
238
+
239
+ it "handles Array with empty/blank first statement" do
240
+ (r << ["", 1, 2, 3]).compile.should == []
241
+ (r << [nil, 1, 2, 3]).compile.should == []
242
+ end
243
+
244
+ it "supports Hash" do
245
+ r << {:is_pirate => true}
246
+ r << {:has_beard => true}
247
+ r << {:drinks_rum => true}
248
+ r.compile.should == ["is_pirate = ? AND has_beard = ? AND drinks_rum = ?", true, true, true]
249
+
250
+ r = klass.new(" OR ")
251
+ r << {:is_pirate => true, :smokes_pipe => false}
252
+ [
253
+ ["is_pirate = ? OR smokes_pipe = ?", true, false],
254
+ ["smokes_pipe = ? OR is_pirate = ?", false, true],
255
+ ].should include(r.compile)
256
+ end
257
+ end # #<<
258
+
259
+ describe "#add_each" do
260
+ it "generally works" do
261
+ r.add_each([:is_pirate, :has_beard, :drinks_rum]) do |v|
262
+ ["#{v} = ?", true]
263
+ end.compile.should == ["is_pirate = ? AND has_beard = ? AND drinks_rum = ?", true, true, true]
264
+ end
265
+ end
266
+
267
+ describe "#brackets=" do
268
+ it "only supports true/false/:auto" do
269
+ [true, false, :auto].each do |v|
270
+ (r.brackets = v).should == v
271
+ end
272
+
273
+ Proc.new do
274
+ r.brackets = nil
275
+ end.should raise_error ArgumentError
276
+
277
+ Proc.new do
278
+ r.brackets = :something_invalid
279
+ end.should raise_error ArgumentError
280
+ end
281
+ end
282
+
283
+ describe "#clear" do
284
+ it "generally works" do
285
+ r.clear
286
+ r.compile.should == []
287
+
288
+ r << ["is_male = ?", true]
289
+ r.clear
290
+ r.compile.should == []
291
+ end
292
+
293
+ it "returns self" do
294
+ r.clear.should eql r
295
+ end
296
+ end
297
+
298
+ describe "#to_a" do
299
+ it "generally works" do
300
+ r << ["is_male = ?", true]
301
+ r << ["age >= ? AND age <= ?", 18, 35]
302
+ r.to_a.should == ["is_male = ? AND (age >= ? AND age <= ?)", true, 18, 35]
303
+ end
304
+ end
305
+
306
+ describe "#size" do
307
+ it "generally works" do
308
+ r.size.should == 0
309
+
310
+ r << ["is_male = ?", true]
311
+ r.size.should == 1
312
+
313
+ r << ["age >= ? AND age <= ?", 18, 35]
314
+ r.size.should == 2
315
+ end
316
+ end
317
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "../init.rb")
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_tuple
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Fortuna
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-07-18 00:00:00 +04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A Simple Yet Smart SQL Conditions Builder
17
+ email: alex.r@askit.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.html
24
+ - README.md
25
+ files:
26
+ - README.html
27
+ - README.md
28
+ - VERSION.yml
29
+ - init.rb
30
+ - lib/smart_tuple.rb
31
+ - smart_tuple.gemspec
32
+ has_rdoc: true
33
+ homepage: http://github.com/dadooda/smart_tuple
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --charset=UTF-8
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.3.5
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: A Simple Yet Smart SQL Conditions Builder
60
+ test_files:
61
+ - spec/spec_helper.rb
62
+ - spec/smart_tuple_spec.rb