grape 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- data/CHANGELOG.md +29 -5
- data/Gemfile +1 -0
- data/README.md +169 -80
- data/Rakefile +19 -3
- data/lib/grape.rb +6 -3
- data/lib/grape/api.rb +24 -3
- data/lib/grape/endpoint.rb +34 -20
- data/lib/grape/http/request.rb +28 -0
- data/lib/grape/middleware/base.rb +1 -1
- data/lib/grape/middleware/filter.rb +1 -1
- data/lib/grape/middleware/formatter.rb +33 -22
- data/lib/grape/middleware/versioner.rb +2 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +3 -1
- data/lib/grape/util/hash_stack.rb +11 -0
- data/lib/grape/validations.rb +47 -11
- data/lib/grape/validations/default.rb +24 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +171 -11
- data/spec/grape/endpoint_spec.rb +41 -1
- data/spec/grape/entity_spec.rb +38 -0
- data/spec/grape/middleware/formatter_spec.rb +48 -0
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner_spec.rb +4 -1
- data/spec/grape/validations/default_spec.rb +67 -0
- data/spec/grape/validations_spec.rb +9 -0
- data/spec/shared/versioning_examples.rb +13 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/versioned_helpers.rb +6 -0
- metadata +11 -4
data/lib/grape/validations.rb
CHANGED
@@ -19,12 +19,27 @@ module Grape
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def validate!(params)
|
22
|
-
|
22
|
+
attributes = AttributesIterator.new(self, @scope, params)
|
23
|
+
attributes.each do |resource_params, attr_name|
|
24
|
+
if @required || resource_params.has_key?(attr_name)
|
25
|
+
validate_param!(attr_name, resource_params)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class AttributesIterator
|
31
|
+
include Enumerable
|
32
|
+
|
33
|
+
def initialize(validator, scope, params)
|
34
|
+
@attrs = validator.attrs
|
35
|
+
@params = scope.params(params)
|
36
|
+
@params = (@params.is_a?(Array) ? @params : [@params])
|
37
|
+
end
|
23
38
|
|
24
|
-
|
25
|
-
@
|
26
|
-
|
27
|
-
|
39
|
+
def each
|
40
|
+
@params.each do |resource_params|
|
41
|
+
@attrs.each do |attr_name|
|
42
|
+
yield resource_params, attr_name
|
28
43
|
end
|
29
44
|
end
|
30
45
|
end
|
@@ -76,9 +91,13 @@ module Grape
|
|
76
91
|
|
77
92
|
def initialize(api, element, parent, &block)
|
78
93
|
@element = element
|
79
|
-
@parent
|
80
|
-
@api
|
94
|
+
@parent = parent
|
95
|
+
@api = api
|
96
|
+
@declared_params = []
|
97
|
+
|
81
98
|
instance_eval(&block)
|
99
|
+
|
100
|
+
configure_declared_params
|
82
101
|
end
|
83
102
|
|
84
103
|
def requires(*attrs)
|
@@ -116,7 +135,24 @@ module Grape
|
|
116
135
|
name.to_s
|
117
136
|
end
|
118
137
|
|
138
|
+
protected
|
139
|
+
|
140
|
+
def push_declared_params(attrs)
|
141
|
+
@declared_params.concat attrs
|
142
|
+
end
|
143
|
+
|
119
144
|
private
|
145
|
+
|
146
|
+
# Pushes declared params to parent or settings
|
147
|
+
def configure_declared_params
|
148
|
+
if @parent
|
149
|
+
@parent.push_declared_params [element => @declared_params]
|
150
|
+
else
|
151
|
+
@api.settings.peek[:declared_params] ||= []
|
152
|
+
@api.settings[:declared_params].concat @declared_params
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
120
156
|
def validates(attrs, validations)
|
121
157
|
doc_attrs = { :required => validations.keys.include?(:presence) }
|
122
158
|
|
@@ -133,6 +169,10 @@ module Grape
|
|
133
169
|
doc_attrs[:desc] = desc
|
134
170
|
end
|
135
171
|
|
172
|
+
if default = validations[:default]
|
173
|
+
doc_attrs[:default] = default
|
174
|
+
end
|
175
|
+
|
136
176
|
full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
|
137
177
|
@api.document_attribute(full_attrs, doc_attrs)
|
138
178
|
|
@@ -164,10 +204,6 @@ module Grape
|
|
164
204
|
end
|
165
205
|
end
|
166
206
|
|
167
|
-
def push_declared_params(attrs)
|
168
|
-
@api.settings.peek[:declared_params] ||= []
|
169
|
-
@api.settings[:declared_params] += attrs
|
170
|
-
end
|
171
207
|
end
|
172
208
|
|
173
209
|
# This module is mixed into the API Class.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Grape
|
2
|
+
module Validations
|
3
|
+
class DefaultValidator < Validator
|
4
|
+
def initialize(attrs, options, required, scope)
|
5
|
+
@default = options
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate_param!(attr_name, params)
|
10
|
+
params[attr_name] = @default unless params.has_key?(attr_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate!(params)
|
14
|
+
params = AttributesIterator.new(self, @scope, params)
|
15
|
+
params.each do |resource_params, attr_name|
|
16
|
+
if resource_params[attr_name].nil?
|
17
|
+
validate_param!(attr_name, resource_params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
data/lib/grape/version.rb
CHANGED
data/spec/grape/api_spec.rb
CHANGED
@@ -97,6 +97,16 @@ describe Grape::API do
|
|
97
97
|
# pending 'routes if any media type is allowed'
|
98
98
|
end
|
99
99
|
|
100
|
+
describe '.version using accept_version_header' do
|
101
|
+
it_should_behave_like 'versioning' do
|
102
|
+
let(:macro_options) do
|
103
|
+
{
|
104
|
+
:using => :accept_version_header
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
100
110
|
describe '.represent' do
|
101
111
|
it 'requires a :with option' do
|
102
112
|
expect{ subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
|
@@ -181,6 +191,36 @@ describe Grape::API do
|
|
181
191
|
end
|
182
192
|
end
|
183
193
|
|
194
|
+
describe '.route_param' do
|
195
|
+
it 'adds a parameterized route segment namespace' do
|
196
|
+
subject.namespace :users do
|
197
|
+
route_param :id do
|
198
|
+
get do
|
199
|
+
params[:id]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
get '/users/23'
|
205
|
+
last_response.body.should == '23'
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'should be able to define requirements with a single hash' do
|
209
|
+
subject.namespace :users do
|
210
|
+
route_param :id, :requirements => /[0-9]+/ do
|
211
|
+
get do
|
212
|
+
params[:id]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
get '/users/michael'
|
218
|
+
last_response.status.should == 404
|
219
|
+
get '/users/23'
|
220
|
+
last_response.status.should == 200
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
184
224
|
describe '.route' do
|
185
225
|
it 'allows for no path' do
|
186
226
|
subject.namespace :votes do
|
@@ -245,6 +285,13 @@ describe Grape::API do
|
|
245
285
|
versioned_get "/", "v1", :using => :param
|
246
286
|
end
|
247
287
|
|
288
|
+
it 'Accept-Version header versioned APIs' do
|
289
|
+
subject.version 'v1', :using => :accept_version_header
|
290
|
+
subject.enable_root_route!
|
291
|
+
|
292
|
+
versioned_get "/", "v1", :using => :accept_version_header
|
293
|
+
end
|
294
|
+
|
248
295
|
it 'unversioned APIs' do
|
249
296
|
subject.enable_root_route!
|
250
297
|
|
@@ -329,6 +376,7 @@ describe Grape::API do
|
|
329
376
|
send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json' }
|
330
377
|
last_response.status.should == (verb == :post ? 201 : 200)
|
331
378
|
last_response.body.should eql MultiJson.dump(object)
|
379
|
+
last_request.params.should eql Hash.new
|
332
380
|
end
|
333
381
|
it "stores input in api.request.input" do
|
334
382
|
subject.format :json
|
@@ -339,6 +387,17 @@ describe Grape::API do
|
|
339
387
|
last_response.status.should == (verb == :post ? 201 : 200)
|
340
388
|
last_response.body.should eql MultiJson.dump(object).to_json
|
341
389
|
end
|
390
|
+
context "chunked transfer encoding" do
|
391
|
+
it "stores input in api.request.input" do
|
392
|
+
subject.format :json
|
393
|
+
subject.send(verb) do
|
394
|
+
env['api.request.input']
|
395
|
+
end
|
396
|
+
send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil }
|
397
|
+
last_response.status.should == (verb == :post ? 201 : 200)
|
398
|
+
last_response.body.should eql MultiJson.dump(object).to_json
|
399
|
+
end
|
400
|
+
end
|
342
401
|
end
|
343
402
|
end
|
344
403
|
end
|
@@ -928,6 +987,21 @@ describe Grape::API do
|
|
928
987
|
last_response.status.should == 400
|
929
988
|
last_response.body.should == 'New Error'
|
930
989
|
end
|
990
|
+
|
991
|
+
it 'can rescue exceptions raised in the formatter' do
|
992
|
+
formatter = stub(:formatter)
|
993
|
+
formatter.stub(:call) { raise StandardError }
|
994
|
+
Grape::Formatter::Base.stub(:formatter_for) { formatter }
|
995
|
+
|
996
|
+
subject.rescue_from :all do |e|
|
997
|
+
rack_response('Formatter Error', 500)
|
998
|
+
end
|
999
|
+
subject.get('/formatter_exception') { 'Hello world' }
|
1000
|
+
|
1001
|
+
get '/formatter_exception'
|
1002
|
+
last_response.status.should eql 500
|
1003
|
+
last_response.body.should == 'Formatter Error'
|
1004
|
+
end
|
931
1005
|
end
|
932
1006
|
|
933
1007
|
describe '.rescue_from klass, block' do
|
@@ -1193,6 +1267,15 @@ describe Grape::API do
|
|
1193
1267
|
end
|
1194
1268
|
|
1195
1269
|
describe '.parser' do
|
1270
|
+
it 'parses data in format requested by content-type' do
|
1271
|
+
subject.format :json
|
1272
|
+
subject.post '/data' do
|
1273
|
+
{ :x => params[:x] }
|
1274
|
+
end
|
1275
|
+
post "/data", '{"x":42}', { 'CONTENT_TYPE' => 'application/json' }
|
1276
|
+
last_response.status.should == 201
|
1277
|
+
last_response.body.should == '{"x":42}'
|
1278
|
+
end
|
1196
1279
|
context 'lambda parser' do
|
1197
1280
|
before :each do
|
1198
1281
|
subject.content_type :txt, "text/plain"
|
@@ -1240,6 +1323,42 @@ describe Grape::API do
|
|
1240
1323
|
last_response.body.should eql 'Disallowed type attribute: "symbol"'
|
1241
1324
|
end
|
1242
1325
|
end
|
1326
|
+
context "none parser class" do
|
1327
|
+
before :each do
|
1328
|
+
subject.parser :json, nil
|
1329
|
+
subject.put "data" do
|
1330
|
+
"body: #{env['api.request.body']}"
|
1331
|
+
end
|
1332
|
+
end
|
1333
|
+
it "does not parse data" do
|
1334
|
+
put '/data', 'not valid json', "CONTENT_TYPE" => "application/json"
|
1335
|
+
last_response.status.should == 200
|
1336
|
+
last_response.body.should == "body: not valid json"
|
1337
|
+
end
|
1338
|
+
end
|
1339
|
+
end
|
1340
|
+
|
1341
|
+
describe '.default_format' do
|
1342
|
+
before :each do
|
1343
|
+
subject.format :json
|
1344
|
+
subject.default_format :json
|
1345
|
+
end
|
1346
|
+
it 'returns data in default format' do
|
1347
|
+
subject.get '/data' do
|
1348
|
+
{ :x => 42 }
|
1349
|
+
end
|
1350
|
+
get "/data"
|
1351
|
+
last_response.status.should == 200
|
1352
|
+
last_response.body.should == '{"x":42}'
|
1353
|
+
end
|
1354
|
+
it 'parses data in default format' do
|
1355
|
+
subject.post '/data' do
|
1356
|
+
{ :x => params[:x] }
|
1357
|
+
end
|
1358
|
+
post "/data", '{"x":42}', "CONTENT_TYPE" => ""
|
1359
|
+
last_response.status.should == 201
|
1360
|
+
last_response.body.should == '{"x":42}'
|
1361
|
+
end
|
1243
1362
|
end
|
1244
1363
|
|
1245
1364
|
describe '.default_error_status' do
|
@@ -1601,6 +1720,24 @@ describe Grape::API do
|
|
1601
1720
|
last_response.body.should == 'yo'
|
1602
1721
|
end
|
1603
1722
|
|
1723
|
+
it 'applies the settings to nested mounted apis' do
|
1724
|
+
subject.version 'v1', :using => :path
|
1725
|
+
|
1726
|
+
subject.namespace :cool do
|
1727
|
+
inner_app = Class.new(Grape::API)
|
1728
|
+
inner_app.get('/awesome') do
|
1729
|
+
"yo"
|
1730
|
+
end
|
1731
|
+
|
1732
|
+
app = Class.new(Grape::API)
|
1733
|
+
app.mount inner_app
|
1734
|
+
mount app
|
1735
|
+
end
|
1736
|
+
|
1737
|
+
get '/v1/cool/awesome'
|
1738
|
+
last_response.body.should == 'yo'
|
1739
|
+
end
|
1740
|
+
|
1604
1741
|
it 'inherits rescues even when some defined by mounted' do
|
1605
1742
|
subject.rescue_from :all do |e|
|
1606
1743
|
rack_response("rescued from #{e.message}", 202)
|
@@ -1801,6 +1938,14 @@ describe Grape::API do
|
|
1801
1938
|
get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'text/html' }
|
1802
1939
|
last_response.body.should == { :meaning_of_life => 42 }.to_json
|
1803
1940
|
end
|
1941
|
+
it 'can be overwritten with an explicit content type' do
|
1942
|
+
subject.get '/meaning_of_life_with_content_type' do
|
1943
|
+
content_type "text/plain"
|
1944
|
+
{ :meaning_of_life => 42 }.to_s
|
1945
|
+
end
|
1946
|
+
get '/meaning_of_life_with_content_type'
|
1947
|
+
last_response.body.should == { :meaning_of_life => 42 }.to_s
|
1948
|
+
end
|
1804
1949
|
it 'raised :error from middleware' do
|
1805
1950
|
middleware = Class.new(Grape::Middleware::Base) do
|
1806
1951
|
def before
|
@@ -1952,18 +2097,33 @@ XML
|
|
1952
2097
|
end
|
1953
2098
|
|
1954
2099
|
context "cascading" do
|
1955
|
-
|
1956
|
-
|
1957
|
-
|
1958
|
-
|
1959
|
-
|
2100
|
+
context "via version" do
|
2101
|
+
it "cascades" do
|
2102
|
+
subject.version 'v1', :using => :path, :cascade => true
|
2103
|
+
get "/v1/hello"
|
2104
|
+
last_response.status.should == 404
|
2105
|
+
last_response.headers["X-Cascade"].should == "pass"
|
2106
|
+
end
|
2107
|
+
it "does not cascade" do
|
2108
|
+
subject.version 'v2', :using => :path, :cascade => false
|
2109
|
+
get "/v2/hello"
|
2110
|
+
last_response.status.should == 404
|
2111
|
+
last_response.headers.keys.should_not include "X-Cascade"
|
2112
|
+
end
|
1960
2113
|
end
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1965
|
-
|
1966
|
-
|
2114
|
+
context "via endpoint" do
|
2115
|
+
it "cascades" do
|
2116
|
+
subject.cascade true
|
2117
|
+
get "/hello"
|
2118
|
+
last_response.status.should == 404
|
2119
|
+
last_response.headers["X-Cascade"].should == "pass"
|
2120
|
+
end
|
2121
|
+
it "does not cascade" do
|
2122
|
+
subject.cascade false
|
2123
|
+
get "/hello"
|
2124
|
+
last_response.status.should == 404
|
2125
|
+
last_response.headers.keys.should_not include "X-Cascade"
|
2126
|
+
end
|
1967
2127
|
end
|
1968
2128
|
end
|
1969
2129
|
end
|
data/spec/grape/endpoint_spec.rb
CHANGED
@@ -64,6 +64,12 @@ describe Grape::Endpoint do
|
|
64
64
|
get '/headers', nil, { "HTTP_X_GRAPE_CLIENT" => "1" }
|
65
65
|
JSON.parse(last_response.body)["X-Grape-Client"].should == "1"
|
66
66
|
end
|
67
|
+
it 'includes headers passed as symbols' do
|
68
|
+
env = Rack::MockRequest.env_for("/headers")
|
69
|
+
env[:HTTP_SYMBOL_HEADER] = "Goliath passes symbols"
|
70
|
+
body = subject.call(env)[2].body.first
|
71
|
+
JSON.parse(body)["Symbol-Header"].should == "Goliath passes symbols"
|
72
|
+
end
|
67
73
|
end
|
68
74
|
|
69
75
|
describe '#cookies' do
|
@@ -167,12 +173,16 @@ describe Grape::Endpoint do
|
|
167
173
|
subject.params do
|
168
174
|
requires :first
|
169
175
|
optional :second
|
176
|
+
optional :third, :default => 'third-default'
|
177
|
+
group :nested do
|
178
|
+
optional :fourth
|
179
|
+
end
|
170
180
|
end
|
171
181
|
end
|
172
182
|
|
173
183
|
it 'has as many keys as there are declared params' do
|
174
184
|
subject.get '/declared' do
|
175
|
-
declared(params).keys.size.should ==
|
185
|
+
declared(params).keys.size.should == 4
|
176
186
|
""
|
177
187
|
end
|
178
188
|
|
@@ -180,6 +190,36 @@ describe Grape::Endpoint do
|
|
180
190
|
last_response.status.should == 200
|
181
191
|
end
|
182
192
|
|
193
|
+
it 'has a optional param with default value all the time' do
|
194
|
+
subject.get '/declared' do
|
195
|
+
params[:third].should == 'third-default'
|
196
|
+
""
|
197
|
+
end
|
198
|
+
|
199
|
+
get '/declared?first=one'
|
200
|
+
last_response.status.should == 200
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'builds nested params' do
|
204
|
+
subject.get '/declared' do
|
205
|
+
declared(params)[:nested].keys.size.should == 1
|
206
|
+
""
|
207
|
+
end
|
208
|
+
|
209
|
+
get '/declared?first=present&nested[fourth]=1'
|
210
|
+
last_response.status.should == 200
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'builds nested params when given array' do
|
214
|
+
subject.get '/declared' do
|
215
|
+
declared(params)[:nested].size.should == 2
|
216
|
+
""
|
217
|
+
end
|
218
|
+
|
219
|
+
get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2'
|
220
|
+
last_response.status.should == 200
|
221
|
+
end
|
222
|
+
|
183
223
|
it 'filters out any additional params that are given' do
|
184
224
|
subject.get '/declared' do
|
185
225
|
declared(params).key?(:other).should == false
|
data/spec/grape/entity_spec.rb
CHANGED
@@ -238,6 +238,44 @@ XML
|
|
238
238
|
last_response.body.should == 'abcDef({"example":{"name":"johnnyiller"}})'
|
239
239
|
end
|
240
240
|
|
241
|
+
context "present with multiple entities" do
|
242
|
+
let(:user) do
|
243
|
+
end
|
244
|
+
|
245
|
+
before :each do
|
246
|
+
end
|
247
|
+
|
248
|
+
it "present with multiple entities using optional symbol" do
|
249
|
+
user = Class.new do
|
250
|
+
attr_reader :name
|
251
|
+
def initialize(args)
|
252
|
+
@name = args[:name] || "no name set"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
user1 = user.new({:name => 'user1'})
|
256
|
+
user2 = user.new({:name => 'user2'})
|
257
|
+
|
258
|
+
entity = Class.new(Grape::Entity)
|
259
|
+
entity.expose :name
|
260
|
+
|
261
|
+
subject.format :json
|
262
|
+
subject.get '/example' do
|
263
|
+
present :page, 1
|
264
|
+
present :user1, user1, :with => entity
|
265
|
+
present :user2, user2, :with => entity
|
266
|
+
end
|
267
|
+
get '/example'
|
268
|
+
expect_response_json = {
|
269
|
+
"page" => 1,
|
270
|
+
"user1" => {"name" => "user1"},
|
271
|
+
"user2" => {"name" => "user2"}
|
272
|
+
}
|
273
|
+
JSON(last_response.body).should == expect_response_json
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
|
241
279
|
end
|
242
280
|
|
243
281
|
end
|