forked from zuk/DrowsyDromedary
-
Notifications
You must be signed in to change notification settings - Fork 0
/
drowsy_dromedary.rb
303 lines (253 loc) · 8.54 KB
/
drowsy_dromedary.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
require 'grape'
require 'mongo'
require 'yaml'
require 'json'
require 'time'
# FIXME: monkeypatch! there's probably a safer way to do this...
class Time
def to_json(*a)
# FIXME: higher accuracy is problematic, as it seems to add milliseconds for encode/decode time
"{\"$date\": \"#{iso8601(1)}\"}"
end
def as_json(options = {})
{ "$date" => iso8601(1) }
end
end
class DrowsyDromedary < Grape::API
version 'v1', :using => :header #, :vendor => 'mongodb'
error_format :json
#default_format :json
format :json
def initialize
super
@dbs ||= {}
# File.open("config.yml") do |f|
# @config = YAML.load(f)
# end
end
helpers do
# def current_user
# @current_user ||= User.authorize!(env)
# end
# def authenticate!
# error!('401 Unauthorized', 401) unless current_user
# end
def connect
@connection ||= Mongo::Connection.new
end
def create_db(db)
@dbs ||= {}
@dbs[db] = connect.db(db, :strict => true)
@dbs[db].collection_names # calling .collection_names seems to create the database
@dbs[db]
end
def connect_to_db(db, opts = {})
@dbs ||= {}
return @dbs[db] if @dbs[db]
c = connect
if opts[:strict] == false
c.db(db, :strict => false)
else
if c.database_names.include?(db.to_s)
c.db(db, :strict => true)
else
return false
end
end
end
def check_required_params(required)
required = [required] unless required.kind_of?(Array)
required = required.collect{|r| r.to_s}
missing = required - ((params.keys - @env["rack.routing_args"].keys) & required)
unless missing.empty?
error!(["The following parameters are required for this request but were missing: ", missing], 400)
end
end
def extract_data_from_params
# TODO: if data type is JSON, JSON.parse(@env["rack.request.form_vars"]) instead.
data = params.dup
@env["rack.routing_args"].keys.each do |key|
data.delete(key)
end
process_data_recursively data
data
end
def process_data_recursively(data)
data.keys.each do |k|
if data[k].kind_of? Hash
if data[k]["$date"]
data[k] = Time.parse data[k]["$date"]
else
process_data_recursively(data[k])
end
end
end
end
def extract_selector_from_params
if params[:selector]
selector = JSON.parse(params[:selector])
selector
else
nil
end
end
def extract_sort_from_params
if params[:sort]
sort = nil
begin
sort = JSON.parse(params[:sort])
rescue JSON::ParserError
sort = params[:sort]
end
sort
else
nil
end
end
end
rescue_from BSON::InvalidObjectId do |e|
if e.nil? || e == "" # why is e coming up empty?
e = "Invalid ObjectID!"
end
error_response({:message => e, :status => 400})
end
# Error handler for Mongo::OperationFailure which happen
# on things like unique key violation
rescue_from Mongo::OperationFailure do |e|
if e.message =~ /^11000/
# If error code is 11000 we have a Duplicate key violation so we send 409 (conflict)
error_response({ :message => "Duplicate key error: #{e.inspect}", :status => 409 })
else
# everything else isn't defined yet so we send 500 (internal server error)
error_response({ :message => e.inspect, :status => 500 })
end
end
get '/' do
connect.database_names
end
post '/' do
check_required_params(:db)
if connect.database_names.include? params[:db]
status 200
connect_to_db(params[:db]).collection_names
else
db = create_db(params[:db])
if db
status 201
db.collection_names
else
error!("Database #{params[:db].inspect} was not created.", 500)
end
end
end
# TODO: implement DELETE to drop database (or maybe not? too risky?)
resource '/:db' do
before do
db_opts = {}
if params[:strict]
if params[:strict] == '0' || params[:strict] == 'false'
db_opts[:strict] = false
end
end
@db = connect_to_db(params[:db], db_opts)
unless @db
error!("There is no database named #{params[:db].inspect}!", 404)
end
end
get do
@db.collection_names
end
post do
check_required_params(:collection)
if @db.collection_names.include? params[:collection]
status 200
@db.collection(params[:collection]).find().to_a
else
coll = @db.create_collection(params[:collection])
if coll
redirect "/#{params[:db]}/#{params[:collection]}", :status => 201
status 201
else
error!("Database #{params[:db].inspect} was not created.", 500)
end
end
end
resource '/:collection' do
desc "Retrieve all items in the collection"
get do
selector = extract_selector_from_params
sort = extract_sort_from_params
limit = params[:limit].to_i || 0
@db.collection(params[:collection]).find(selector, :sort => sort).limit(limit).to_a
end
desc "Add a new item to the collection"
post do
data = extract_data_from_params
id = @db.collection(params[:collection]).insert(data)
# FIXME: save ourselves the extra query and just return `data`?
@db.collection(params[:collection]).find_one(id)
end
desc "Drop the collection"
delete do
result = @db.drop_collection(params[:collection])
unless result
error!("Could not drop collection #{params[:colleciton].inspect}!", 500)
end
end
# TODO: implement DELETE to allow mass deletion of items in collection
desc "Retrieve the item with id :id from the collection"
get '/:id' do
id = BSON::ObjectId(params[:id])
item = @db.collection(params[:collection]).find_one(id)
unless item
error!("Item #{params[:id].inspect} doesn't exist in #{params[:collection].inspect}!", 404)
end
item
end
desc "Replace or create the item with id :id in the collection"
put '/:id' do
id = BSON::ObjectId(params[:id])
data = extract_data_from_params
# TODO: switch to find_and_modify for better performance?
# item = @db.collection(params[:collection]).find_one(id)
# unless item
# error!("Item #{params[:id].inspect} doesn't exist in #{params[:collection].inspect}!", 404)
# end
data['_id'] = id
@db.collection(params[:collection]).save(data)
# FIXME: save ourselves the extra query and just return `data`? (may need to stringify data['_id'] first though)
@db.collection(params[:collection]).find_one(id)
end
desc "Replaces the given attributes in the item with id :id in the collection"
patch '/:id' do
id = BSON::ObjectId(params[:id])
data = extract_data_from_params
item = @db.collection(params[:collection]).find_one(id)
unless item
error!("Item #{params[:id].inspect} doesn't exist in #{params[:collection].inspect}!", 404)
end
data.delete 'id'
data.delete '_id'
#@db.collection(params[:collection]).save(item)
@db.collection(params[:collection]).update({"_id" => id}, {"$set" => data})
# FIXME: save ourselves the extra query and just return `item`?
@db.collection(params[:collection]).find_one(id)
end
desc "Delete the item with id :id from the collection"
delete '/:id' do
id = BSON::ObjectId(params[:id])
# MongoDB in version 2.4 doesn't return any error or indication that the document to be deleted didn't exist
# To mitigate this and prevent WakefulWeasel from endless loops deleting we check for existance and then delete
# If document doesn't exist we return 500 error
# This is better than returning 200 on a delete for a non existing document and getting weird side effect, but
# pretty is different.
# if @db.collection(params[:collection]).remove({'_id' => id})
if [email protected](params[:collection]).find_one(id).nil? && @db.collection(params[:collection]).remove({'_id' => id})
{} # FIXME: we probably want to just return nil (i.e. null) here, but this is not parsable by JSON.parse()
else
error!("Item #{params[:id].inspect} could not be deleted from #{params[:collection].inspect}!", 500)
end
end
end
end
end