deli 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/deli/query.rb ADDED
@@ -0,0 +1,133 @@
1
+ module Deli
2
+ class Query
3
+ class << self
4
+ def parse_string(string)
5
+ parse_query(::URI.parse(string).query)
6
+ end
7
+
8
+ # from rack
9
+ def parse_query(qs, d = nil)
10
+ params = {}
11
+
12
+ (qs || '').split(d ? /[#{d}] */n : /[&;] */n).each do |p|
13
+ k, v = p.split('=', 2).map { |x| unescape(x) }
14
+ if cur = params[k]
15
+ if cur.class == Array
16
+ params[k] << v
17
+ else
18
+ params[k] = [cur, v]
19
+ end
20
+ else
21
+ params[k] = v
22
+ end
23
+ end
24
+
25
+ return params
26
+ end
27
+
28
+ # from rack
29
+ def unescape(string)
30
+ string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) { [$1.delete('%')].pack('H*') }
31
+ end
32
+
33
+ # from rack, except we don't escape anything!
34
+ # keeps url's nicely formatted...
35
+ # up to you to make sure you have the right input
36
+ def build_query(params)
37
+ params.map do |k, v|
38
+ if v.class == Array
39
+ build_query(v.map { |x| [k, x] })
40
+ else
41
+ "#{k}=#{v}"
42
+ end
43
+ end.join("&").gsub(/\s+/, "+")
44
+ end
45
+ end
46
+
47
+ attr_reader :controller
48
+
49
+ def initialize(deli_controller)
50
+ @controller = deli_controller
51
+ end
52
+
53
+ def matching(params)
54
+ controller.params.select do |param|
55
+ params[param.key].present?
56
+ end
57
+ end
58
+
59
+ def find(key)
60
+ controller.params.detect do |param|
61
+ param.key == key
62
+ end
63
+ end
64
+
65
+ def conditions?(params)
66
+ controller.params.select do |param|
67
+ param.key !~ /^(?:page|limit|sort)$/
68
+ end.any? do |param|
69
+ params[param.key].present?
70
+ end
71
+ end
72
+
73
+ def append_join(key)
74
+ joins.push(key.to_sym)
75
+ end
76
+
77
+ def joins
78
+ @joins ||= []
79
+ end
80
+
81
+ def default_sort
82
+ if @default_sort_exists.nil?
83
+ sort = find("sort")
84
+ @default_sort_exists = sort.present? && sort.default.present?
85
+ @default_sort = sort.default if @default_sort_exists
86
+ end
87
+
88
+ @default_sort
89
+ end
90
+
91
+ def parse_string(string)
92
+ ::Deli::Query.parse_string(string)
93
+ end
94
+
95
+ def parse_query(qs, d = nil)
96
+ ::Deli::Query.parse_query(qs, d)
97
+ end
98
+
99
+ def parse(url_or_params)
100
+ case url_or_params
101
+ when ::String
102
+ parse_string(url_or_params)
103
+ else
104
+ url_or_params
105
+ end
106
+ end
107
+
108
+ def render(params)
109
+ params = parse(params)
110
+ matching(params).inject({}) do |hash, param|
111
+ hash[param.key] = param.render(params[param.key])
112
+ hash
113
+ end
114
+ end
115
+
116
+ def conditions(params)
117
+ to_conditions(render(params.except("page", "limit", "sort")))
118
+ end
119
+
120
+ def to_conditions(hash)
121
+ keys = []
122
+ values = []
123
+ hash.values.collect do |array|
124
+ keys << array[0]
125
+ values << array[1..-1]
126
+ end
127
+
128
+ keys.map! {|i| "(#{i})"} if keys.length > 1
129
+
130
+ [keys.join(" AND "), *values.flatten]
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,14 @@
1
+ module Deli
2
+ class Railtie < Rails::Railtie
3
+ initializer "deli.insert_into_stack" do
4
+ ActiveSupport.on_load :active_record do
5
+ ::ActiveRecord::Base.send :extend, ::Deli::Pagination
6
+ end
7
+
8
+ ActiveSupport.on_load :action_controller do
9
+ ActionController::Base.send :include, ::Deli::Helper
10
+ ActionView::Base.send :include, ::Deli::Helper
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,358 @@
1
+ require 'spec_helper'
2
+
3
+ describe Deli::Adapters::ActiveRecord do
4
+ context "param:string" do
5
+ before do
6
+ @param = Deli::Adapters::ActiveRecord::String.new(:title)
7
+ end
8
+
9
+ it "should render a simple string" do
10
+ result = ["title LIKE ?", "%Hello World%"]
11
+ @param.render("Hello World").should == result
12
+ end
13
+
14
+ it "should split + into individual tokens" do
15
+ result = ["title LIKE ? AND title LIKE ?", "%Hello%", "%World%"]
16
+ @param.render("Hello+World").should == result
17
+ end
18
+
19
+ context "query for exact matches" do
20
+ it "should split single quotes ' into tokens" do
21
+ result = ["title LIKE ?", "%Hello World%"]
22
+ @param.render("'Hello World'").should == result
23
+ end
24
+
25
+ it "should query for exact matches: split single quotes ' into tokens, even with + signs" do
26
+ result = ["title LIKE ?", "%Hello World%"]
27
+ @param.render("'Hello+World'").should == result
28
+ end
29
+ end
30
+
31
+ context "query negations" do
32
+ it "should render a simple string" do
33
+ result = ["title NOT LIKE ? AND title NOT LIKE ?", "%Hello%", "%World%"]
34
+ @param.render("-Hello+-World").should == result
35
+ end
36
+
37
+ # NOT PASSING, FIX REGEX
38
+ # it "should not negate hyphenated phrases" do
39
+ # result = ["title LIKE ?", "%Hello-World%"]
40
+ # @param.render("Hello-World").should == result
41
+ # end
42
+ end
43
+
44
+ context "query OR" do
45
+ it "should render a simple string with +OR+" do
46
+ result = ["(title LIKE ?) OR (title LIKE ?)", "%Hello%", "%World%"]
47
+ @param.render("Hello+OR+World").should == result
48
+ end
49
+
50
+ it "should render a simple string with pipe |" do
51
+ result = ["(title LIKE ?) OR (title LIKE ?)", "%Hello%", "%World%"]
52
+ @param.render("Hello|World").should == result
53
+ end
54
+
55
+ it "should render a complex string" do
56
+ result = ["(title LIKE ? AND title LIKE ?) OR (title LIKE ?)", "%Hello%", "%World%", "%Query%"]
57
+ @param.render("Hello+World+OR+Query").should == result
58
+ end
59
+
60
+ it "should with single quotes" do
61
+ result = ["(title LIKE ? AND title LIKE ?) OR (title LIKE ?)", "%Hello%", "%World%", "%ruby on rails%"]
62
+ @param.render("Hello+World+OR+'ruby+on+rails'").should == result
63
+ end
64
+
65
+ it "should with multiple on each side" do
66
+ result = ["(title LIKE ? AND title LIKE ?) OR (title LIKE ? AND title LIKE ?)", "%Hello%", "%World%", "%Ruby%", "%Javascript%"]
67
+ @param.render("Hello+World+OR+Ruby+Javascript").should == result
68
+ end
69
+ end
70
+
71
+ context "query start and end" do
72
+ it "should render start match" do
73
+ result = ["title LIKE ?", "%phone"]
74
+ @param.render("^phone").should == result
75
+ end
76
+
77
+ it "should render end match" do
78
+ result = ["title LIKE ?", "phone%"]
79
+ @param.render("phone$").should == result
80
+ end
81
+ end
82
+ end
83
+
84
+ context "param:number" do
85
+ before do
86
+ @param = Deli::Adapters::ActiveRecord::Number.new(:login_count)
87
+ end
88
+
89
+ it "should render single digit numbers" do
90
+ num = 8
91
+ result = ["login_count = ?", num]
92
+ @param.render("8").should == result
93
+ end
94
+
95
+ it "should render multiple digit numbers" do
96
+ num = 1920
97
+ result = ["login_count = ?", num]
98
+ @param.render("1_920").should == result
99
+ end
100
+
101
+ it "should render negative single digit numbers" do
102
+ num = -4
103
+ result = ["login_count = ?", num]
104
+ @param.render("-4").should == result
105
+ end
106
+
107
+ it "should render negative multiple digit numbers" do
108
+ num = -470
109
+ result = ["login_count = ?", num]
110
+ @param.render("-470").should == result
111
+ end
112
+
113
+ it "should render range" do
114
+ result = ["login_count >= ? AND login_count <= ?", 3, 21]
115
+ @param.render("3..21").should == result
116
+ end
117
+
118
+ it "should render range with no end" do
119
+ result = ["login_count >= ?", 3]
120
+ @param.render("3..n").should == result
121
+ end
122
+
123
+ it "should render range with no start" do
124
+ result = ["login_count <= ?", 21]
125
+ @param.render("n..21").should == result
126
+ end
127
+
128
+ it "should render multiple numbers with OR" do
129
+ result = ["(login_count = ?) OR (login_count = ?)", 10, 1000]
130
+ @param.render("10,1000").should == result
131
+ end
132
+
133
+ it "should render multiple ranges with OR" do
134
+ result = ["(login_count >= ? AND login_count <= ?) OR (login_count >= ? AND login_count <= ?)", 1, 10, 100, 1_000]
135
+ @param.render("1..10,100..1000").should == result
136
+ end
137
+
138
+ it "should render multiple numbers with OR, along with a range" do
139
+ result = ["(login_count = ?) OR (login_count = ?) OR (login_count >= ? AND login_count <= ?)", 1, 10, 100, 1_000]
140
+ @param.render("1,10,100..1000").should == result
141
+ end
142
+ end
143
+
144
+ context "param:time" do
145
+ before do
146
+ @param = ::Deli::Adapters::ActiveRecord::Time.new(:created_at)
147
+ end
148
+
149
+ it "should render single date" do
150
+ time = Time.zone.parse("2011-02-01") # feb 1, 2011
151
+ result = ["created_at = ?", time]
152
+ @param.render("2011-02-01").should == result
153
+ end
154
+
155
+ it "should render date range" do
156
+ starts_on = Time.zone.parse("2011-02-01")
157
+ ends_on = Time.zone.parse("2011-03-01")
158
+ result = ["created_at >= ? AND created_at <= ?", starts_on, ends_on]
159
+ @param.render("2011-02-01..2011-03-01").should == result
160
+ end
161
+
162
+ it "should render date range with no end" do
163
+ starts_on = Time.zone.parse("2011-02-01")
164
+ result = ["created_at >= ?", starts_on]
165
+ @param.render("2011-02-01..t").should == result
166
+ end
167
+
168
+ it "should render date range with no start" do
169
+ ends_on = Time.zone.parse("2011-03-01")
170
+ result = ["created_at <= ?", ends_on]
171
+ @param.render("t..2011-03-01").should == result
172
+ end
173
+
174
+ it "should render single time" do
175
+ time = Time.zone.parse("2011-12-25@2am") # christmas morning
176
+ result = ["created_at = ?", time]
177
+ @param.render("2011-12-25@2am").should == result
178
+ end
179
+
180
+ it "should render single complex time" do
181
+ time = Time.zone.parse("2011-12-25@2:15:27am")
182
+ result = ["created_at = ?", time]
183
+ @param.render("2011-12-25@2:15:27am").should == result
184
+ end
185
+ end
186
+
187
+ context "param:sort" do
188
+ before do
189
+ @param = ::Deli::Adapters::ActiveRecord::Order.new(:sort)
190
+ end
191
+
192
+ it "should render single sort param (defaults to ASC)" do
193
+ result = "created_at ASC"
194
+ @param.render("created_at").should == result
195
+ end
196
+
197
+ it "should render single sort param DESC" do
198
+ result = "created_at DESC"
199
+ @param.render("created_at-").should == result
200
+ end
201
+
202
+ it "should render single sort param ASC" do
203
+ result = "created_at ASC"
204
+ @param.render("created_at+").should == result
205
+ end
206
+
207
+ it "should render mutiple sort params" do
208
+ result = "created_at ASC, name ASC"
209
+ @param.render("created_at,name+").should == result
210
+ end
211
+ end
212
+
213
+ context ":query" do
214
+ context "raw" do
215
+ before do
216
+ @url = "http://site.com/search?title=Ruby+-'Hello+World'&created_at=t..2011-02-01&sort=created_at,title-&page=12&limit=20"
217
+ @params = {
218
+ :title => "Ruby+-'Hello+World'",
219
+ :created_at => "t..2011-02-01",
220
+ :sort => "created_at,title-",
221
+ :page => "12",
222
+ :limit => "20"
223
+ }.stringify_keys
224
+
225
+ @params.each do |k, v|
226
+ @params[k] = v.gsub("+", " ")
227
+ end
228
+ end
229
+
230
+ it "should parse raw query string" do
231
+ ::Deli::Query.parse_string(@url).should == @params
232
+ end
233
+
234
+ context "definitions" do
235
+ before do
236
+ @controller = ::Deli::Controller.new(:user) do
237
+ match :title, :type => :string
238
+ match :created_at, :type => :datetime
239
+ end
240
+ end
241
+
242
+ it "should has 5 params (title, created_at, sort, page, limit)" do
243
+ @controller.params.length.should == 5
244
+ %w(title created_at sort page limit).each do |key|
245
+ @controller.keys.include?(key).should == true
246
+ end
247
+ end
248
+
249
+ it "should render query" do
250
+ query = {
251
+ :conditions => ["(users.title LIKE ? AND users.title NOT LIKE ?) AND (users.created_at <= ?)", "%Ruby%", "%Hello World%", Time.zone.parse("2011-02-01")],
252
+ :limit => 20,
253
+ :offset => (12 - 1) * 20,
254
+ :order => "users.created_at ASC, users.title DESC"
255
+ }
256
+
257
+ @controller.render(@params).should == query
258
+ end
259
+ end
260
+
261
+ context "database" do
262
+ before do
263
+ @controller = ::Deli::Controller.new(:user) do
264
+ match :first_name
265
+ match :last_name
266
+ match :created_at
267
+ match :has_bookmark, :to => {:bookmarks => :event}, :exact => true, :type => :string
268
+ end
269
+ User.delete_all
270
+ Bookmark.delete_all
271
+ @user = User.create(:first_name => "DaVinci")
272
+ end
273
+
274
+ it "should query the database" do
275
+ params = {"first_name" => "DaVinci+OR+Michelangelo"}
276
+ expected = {:conditions => ["(users.first_name LIKE ?) OR (users.first_name LIKE ?)", "%DaVinci%", "%Michelangelo%"], :limit => 20}
277
+ result = @controller.render(params)
278
+ result.should == expected
279
+ result = User.first(result)
280
+ result.should == @user
281
+ end
282
+
283
+ it "should parse to conditions array" do
284
+ params = {"first_name" => "DaVinci|Michelangelo"}
285
+ expected = {:conditions => ["(users.first_name LIKE ?) OR (users.first_name LIKE ?)", "%DaVinci%", "%Michelangelo%"], :limit => 20}
286
+ result = @controller.render(params)
287
+ result.should == expected
288
+ result = User.first(result)
289
+ result.should == @user
290
+ end
291
+
292
+ it "should handle complex conditions" do
293
+ week_ago = 1.week.ago.beginning_of_day
294
+ params = {"created_at" => "#{week_ago.strftime("%Y-%m-%d")}..t", "first_name" => "DaVinci|Michelangelo"}
295
+ expected = {:conditions => ["((users.first_name LIKE ?) OR (users.first_name LIKE ?)) AND (users.created_at >= ?)", "%DaVinci%", "%Michelangelo%", week_ago], :limit => 20}
296
+ result = @controller.render(params)
297
+ result.should == expected
298
+ result = User.first(result)
299
+ result.should == @user
300
+ end
301
+
302
+ it "should create full activerecord hash" do
303
+ expected = {
304
+ :conditions => ["(users.first_name LIKE ?) OR (users.first_name LIKE ?)", "%DaVinci%", "%Michelangelo%"],
305
+ :limit => 20,
306
+ :offset => 80,
307
+ :order => "users.created_at ASC"
308
+ }
309
+ params = {"first_name" => "DaVinci|Michelangelo", "page" => "5", "limit" => "20", "sort" => "created_at"}
310
+
311
+ @controller.render(params).should == expected
312
+ end
313
+
314
+ # it "should automatically handle joins" do
315
+ # expected = {
316
+ # :conditions => ["(bookmarks.event = ?) AND ((users.first_name LIKE ?) OR (users.first_name LIKE ?))", "selected", "%DaVinci%", "%Michelangelo%"]
317
+ # :limit => 20,
318
+ # :offset => 80,
319
+ # #:joins => [" INNER JOIN \"bookmarks\" ON bookmarks.user_id = users.id "],
320
+ # :order => "users.created_at ASC",
321
+ # }
322
+ # params = {"has_bookmark" => "selected", "first_name" => "DaVinci|Michelangelo", "page" => "5", "limit" => "20", "sort" => "created_at"}
323
+ #
324
+ # @controller.render(params).should == expected
325
+ # end
326
+
327
+ it "should create a named scope" do
328
+ 3.times { # 4 times total
329
+ @user = User.create(:first_name => "DaVinci")
330
+ }
331
+
332
+ params = {"first_name" => "DaVinci|Michelangelo", "limit" => "2", "sort" => "created_at"}
333
+ result = User.paginate(@controller.render(params))
334
+
335
+ result.class.should == ::ActiveRecord::Relation
336
+ result.count.should == 2
337
+ result.total_count.should == 4
338
+ end
339
+
340
+ it "should handle named scopes on associations" do
341
+ params = {"first_name" => "DaVinci|Michelangelo", "limit" => "2", "sort" => "created_at"}
342
+ @controller = ::Deli::Controller.new(:bookmark) do
343
+ match :first_name, :to => {:user => :first_name}, :type => :string
344
+ match :created_at
345
+ end
346
+ 4.times {
347
+ Bookmark.create(:user => @user)
348
+ }
349
+
350
+ result = @user.bookmarks.paginate(@controller.render(params)).joins(:user)
351
+ result.class.should == ::ActiveRecord::Relation
352
+ result.count.should == 2
353
+ result.total_count.should == 4
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end