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/Rakefile +77 -0
- data/lib/deli.rb +18 -0
- data/lib/deli/adapters.rb +9 -0
- data/lib/deli/adapters/active_record.rb +251 -0
- data/lib/deli/adapters/cassandra.rb +52 -0
- data/lib/deli/adapters/mongoid.rb +180 -0
- data/lib/deli/adapters/neo4j.rb +54 -0
- data/lib/deli/adapters/simple.rb +39 -0
- data/lib/deli/configuration.rb +47 -0
- data/lib/deli/controller.rb +75 -0
- data/lib/deli/helper.rb +61 -0
- data/lib/deli/model.rb +13 -0
- data/lib/deli/pagination.rb +117 -0
- data/lib/deli/param.rb +189 -0
- data/lib/deli/query.rb +133 -0
- data/lib/deli/railtie.rb +14 -0
- data/spec/active_record_spec.rb +358 -0
- data/spec/cassandra_spec.rb +40 -0
- data/spec/mongoid_spec.rb +40 -0
- data/spec/neo4j_spec.rb +40 -0
- data/spec/param_spec.rb +318 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/database.rb +69 -0
- data/spec/support/models.rb +27 -0
- metadata +77 -0
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
|
data/lib/deli/railtie.rb
ADDED
@@ -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
|