deli 0.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.
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