Skip to content

Commit 0ba3a31

Browse files
author
Ben Rosenblum
committed
Merge branch 'master' of https://github.com/intridea/grape
2 parents 8794387 + f8d280b commit 0ba3a31

File tree

11 files changed

+298
-10
lines changed

11 files changed

+298
-10
lines changed

CHANGELOG.markdown

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
Next Release
2+
============
3+
4+
* [#64](https://github.com/intridea/grape/issues/64), [#180](https://github.com/intridea/grape/pull/180): Added support to get request bodies as parameters - [@bobbytables](https://github.com/bobbytables).
5+
* [#175](https://github.com/intridea/grape/pull/175): Added support for API versioning based on a request parameter - [@jackcasey](https://github.com/jackcasey).
6+
* [#168](https://github.com/intridea/grape/pull/168): Fix: Formatter can parse symbol keys in the headers hash - [@netmask](https://github.com/netmask).
7+
* [#169](https://github.com/intridea/grape/pull/169): Silence multi_json deprecation warnings - [@whiteley](https://github.com/whiteley).
8+
* [#166](https://github.com/intridea/grape/pull/166): Added support for `redirect`, including permanent and temporary - [@allenwei](https://github.com/allenwei).
9+
* [#159](https://github.com/intridea/grape/pull/159): Added `:requirements` to routes, allowing to use reserved characters in paths - [@gaiottino](https://github.com/gaiottino).
10+
* [#156](https://github.com/intridea/grape/pull/156): Added support for adding formatters to entities - [@bobbytables](https://github.com/bobbytables).
11+
12+
0.2.0 (3/28/2012)
13+
=================
14+
15+
* Added support for inheriting exposures from entities - [@bobbytables](https://github.com/bobbytables).
16+
* Extended formatting with `default_format` - [@dblock](https://github.com/dblock).
17+
* Added support for cookies - [@lukaszsliwa](https://github.com/lukaszsliwa).
18+
* Added support for declaring additional content-types - [@joeyAghion](https://github.com/joeyAghion).
19+
* Added support for HTTP PATCH - [@LTe](https://github.com/LTe).
20+
* Added support for describing, documenting and reflecting APIs - [@dblock](https://github.com/dblock).
21+
* Added support for anchoring and vendoring - [@jwkoelewijn](https://github.com/jwkoelewijn).
22+
* Added support for HTTP OPTIONS - [@grimen](https://github.com/grimen).
23+
* Added support for silencing logger - [@evansj](https://github.com/evansj).
24+
* Added support for helper modules - [@freelancing-god](https://github.com/freelancing-god).
25+
* Added support for Accept header-based versioning - [@jch](https://github.com/jch), [@rodzyn](https://github.com/rodzyn).
26+
* Added support for mounting APIs and other Rack applications within APIs - [@mbleigh](https://github.com/mbleigh).
27+
* Added entities, multiple object representations - [@mbleigh](https://github.com/mbleigh).
28+
* Added ability to handle XML in the incoming request body - [@jwillis](https://github.com/jwillis).
29+
* Added support for a configurable logger - [@mbleigh](https://github.com/mbleigh).
30+
* Added support for before and after filters - [@mbleigh](https://github.com/mbleigh).
31+
* Extended `rescue_from`, which can now take a block - [@dblock](https://github.com/dblock).
32+
33+
34+
0.1.5 (6/14/2011)
35+
==================
36+
37+
* Extended exception handling to all exceptions - [@dblock](https://github.com/dblock).
38+
* Added support for returning JSON objects from within error blocks - [@dblock](https://github.com/dblock).
39+
* Added support for handling incoming JSON in body - [@tedkulp](https://github.com/tedkulp).
40+
* Added support for HTTP digest authentication - [@daddz](https://github.com/daddz).
41+
42+
0.1.4 (4/8/2011)
43+
==================
44+
45+
* Allow multiple definitions of the same endpoint under multiple versions - [@chrisrhoden](https://github.com/chrisrhoden).
46+
* Added support for multipart URL parameters - [@mcastilho](https://github.com/mcastilho).
47+
* Added support for custom formatters - [@spraints](https://github.com/spraints).
48+
49+
0.1.3 (1/10/2011)
50+
==================
51+
52+
* Added support for JSON format in route matching - [@aiwilliams](https://github.com/aiwilliams).
53+
* Added suport for custom middleware - [@mbleigh](https://github.com/mbleigh).
54+
55+
0.1.1 (11/14/2010)
56+
==================
57+
58+
* Endpoints properly reset between each request - [@mbleigh](https://github.com/mbleigh).
59+
60+
0.1.0 (11/13/2010)
61+
==================
62+
63+
* Initial public release - [@mbleigh](https://github.com/mbleigh).

README.markdown

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,14 @@ end
110110

111111
## Versioning
112112

113-
There are two strategies in which clients can reach your API's endpoints: `:header`
114-
and `:path`. The default strategy is `:header`.
113+
There are three strategies in which clients can reach your API's endpoints: `:header`, `:path` and `:param`. The default strategy is `:header`.
115114

116-
version 'v1', :using => :header
115+
116+
### Header
117+
118+
```ruby
119+
version 'v1', :using => :header
120+
```
117121

118122
Using this versioning strategy, clients should pass the desired version in the HTTP Accept head.
119123

@@ -124,34 +128,70 @@ supplied. This behavior is similar to routing in Rails. To circumvent this defau
124128
one could use the `:strict` option. When this option is set to `true`, a `404 Not found` error
125129
is returned when no correct Accept header is supplied.
126130

127-
version 'v1', :using => :path
131+
### Path
132+
133+
``` ruby
134+
version 'v1', :using => :path
135+
```
128136

129137
Using this versioning strategy, clients should pass the desired version in the URL.
130138

131139
curl -H http://localhost:9292/v1/statuses/public_timeline
132140

133141
Serialization takes place automatically.
134142

143+
### Param
144+
145+
```ruby
146+
version 'v1', :using => :param
147+
```
148+
149+
Using this versioning strategy, clients should pass the desired version as a request parameter, either in the URL query string or in the request body.
150+
151+
curl -H http://localhost:9292/events?apiver=v1
152+
153+
The default name for the query parameter is 'apiver' but can be specified using the :parameter option.
154+
155+
```ruby
156+
version 'v1', :using => :param, :parameter => "v"
157+
```
158+
159+
curl -H http://localhost:9292/events?v=v1
160+
135161
## Parameters
136162

137163
Parameters are available through the `params` hash object. This includes `GET` and `POST` parameters,
138164
along with any named parameters you specify in your route strings.
139165

140166
```ruby
141-
get do
167+
get do
142168
Article.order(params[:sort_by])
143-
end
169+
end
170+
```
171+
172+
Parameters are also populated from the request body on POST and PUT for JSON and XML content-types.
173+
174+
The Request:
175+
176+
```curl -d '{"some_key": "some_value"}' 'http://localhost:9292/json_endpoint' -H Content-Type:application/json -v```
177+
178+
The Grape Endpoint:
179+
180+
```ruby
181+
post '/json_endpoint' do
182+
params[:some_key]
183+
end
144184
```
145185

146186
## Headers
147187

148188
Headers are available through the `env` hash object.
149189

150190
```ruby
151-
get do
191+
get do
152192
error! 'Unauthorized', 401 unless env['HTTP_SECRET_PASSWORD'] == 'swordfish'
153193
...
154-
end
194+
end
155195
```
156196

157197
## Helpers

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Auth
2626
module Versioner
2727
autoload :Path, 'grape/middleware/versioner/path'
2828
autoload :Header, 'grape/middleware/versioner/header'
29+
autoload :Param, 'grape/middleware/versioner/param'
2930
end
3031
end
3132

lib/grape/endpoint.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,26 @@ def call!(env)
118118
# The parameters passed into the request as
119119
# well as parsed from URL segments.
120120
def params
121-
@params ||= Hashie::Mash.new.deep_merge(request.params).deep_merge(env['rack.routing_args'] || {})
121+
@params ||= Hashie::Mash.new.
122+
deep_merge(request.params).
123+
deep_merge(env['rack.routing_args'] || {}).
124+
deep_merge(self.body_params)
125+
end
126+
127+
# Pull out request body params if the content type matches and we're on a POST or PUT
128+
def body_params
129+
if ['POST', 'PUT'].include?(request.request_method.to_s.upcase)
130+
return case env['CONTENT_TYPE']
131+
when 'application/json'
132+
MultiJson.decode(request.body.read)
133+
when 'application/xml'
134+
MultiXml.parse(request.body.read)
135+
else
136+
{}
137+
end
138+
end
139+
140+
{}
122141
end
123142

124143
# The API version as specified in the URL.

lib/grape/middleware/versioner.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ def using(strategy)
1818
Path
1919
when :header
2020
Header
21+
when :param
22+
Param
2123
else
2224
raise ArgumentError.new("Unknown :using for versioner: #{strategy}")
2325
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'grape/middleware/base'
2+
3+
module Grape
4+
module Middleware
5+
module Versioner
6+
# This middleware sets various version related rack environment variables
7+
# based on the request parameters and removes that parameter from the
8+
# request parameters for subsequent middleware and API.
9+
# If the version substring does not match any potential initialized
10+
# versions, a 404 error is thrown.
11+
# If the version substring is not passed the version (highest mounted)
12+
# version will be used.
13+
#
14+
# Example: For a uri path
15+
# /resource?apiver=v1
16+
#
17+
# The following rack env variables are set and path is rewritten to
18+
# '/resource':
19+
#
20+
# env['api.version'] => 'v1'
21+
class Param < Base
22+
def default_options
23+
{
24+
:parameter => "apiver"
25+
}
26+
end
27+
28+
def before
29+
paramkey = options[:parameter]
30+
potential_version = request.params[paramkey]
31+
32+
unless potential_version.nil?
33+
if options[:versions] && !options[:versions].include?(potential_version)
34+
throw :error, :status => 404, :message => "404 API Version Not Found", :headers => {'X-Cascade' => 'pass'}
35+
end
36+
env['api.version'] = potential_version
37+
env['rack.request.query_hash'].delete(paramkey)
38+
end
39+
end
40+
41+
end
42+
end
43+
end
44+
end

spec/grape/api_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ def app; subject end
3131
end
3232
end
3333

34+
describe '.version using param' do
35+
it_should_behave_like 'versioning' do
36+
let(:macro_options) do
37+
{
38+
:using => :param,
39+
:parameter => "apiver"
40+
}
41+
end
42+
end
43+
end
44+
3445
describe '.version using header' do
3546
it_should_behave_like 'versioning' do
3647
let(:macro_options) do

spec/grape/endpoint_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,45 @@ def app; subject end
167167

168168
get '[email protected]/wrong_middle/1'
169169
last_response.status.should == 404
170+
end
171+
end
172+
173+
context 'from body parameters' do
174+
before(:each) do
175+
subject.post '/request_body' do
176+
params[:user]
177+
end
178+
179+
subject.put '/request_body' do
180+
params[:user]
181+
end
182+
end
183+
184+
it 'should convert JSON bodies to params' do
185+
post '/request_body', MultiJson.encode(user: 'Bobby T.'), {'CONTENT_TYPE' => 'application/json'}
186+
last_response.body.should == 'Bobby T.'
187+
end
188+
189+
it 'should convert JSON bodies to params' do
190+
put '/request_body', MultiJson.encode(user: 'Bobby T.'), {'CONTENT_TYPE' => 'application/json'}
191+
last_response.body.should == 'Bobby T.'
192+
end
193+
194+
it 'should convert XML bodies to params' do
195+
post '/request_body', '<user>Bobby T.</user>', {'CONTENT_TYPE' => 'application/xml'}
196+
last_response.body.should == 'Bobby T.'
197+
end
198+
199+
it 'should convert XML bodies to params' do
200+
put '/request_body', '<user>Bobby T.</user>', {'CONTENT_TYPE' => 'application/xml'}
201+
last_response.body.should == 'Bobby T.'
202+
end
170203

204+
it 'does not include parameters not defined by the body' do
205+
subject.post '/omitted_params' do
206+
body_params[:version].should == nil
207+
end
208+
post '/omitted_params', MultiJson.encode(user: 'Blah'), {'CONTENT_TYPE' => 'application/json'}
171209
end
172210
end
173211
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
require 'spec_helper'
2+
3+
describe Grape::Middleware::Versioner::Param do
4+
5+
let(:app) { lambda{|env| [200, env, env['api.version']]} }
6+
subject { Grape::Middleware::Versioner::Param.new(app, @options || {}) }
7+
8+
it 'should set the API version based on the default param (apiver)' do
9+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1"}})
10+
subject.call(env)[1]["api.version"].should == 'v1'
11+
end
12+
13+
it 'should cut (only) the version out of the params', :focus => true do
14+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1", "other_param" => "5"}})
15+
subject.call(env)[1]['rack.request.query_hash']["apiver"].should be_nil
16+
subject.call(env)[1]['rack.request.query_hash']["other_param"].should == "5"
17+
end
18+
19+
it 'should provide a nil version if no version is given' do
20+
env = Rack::MockRequest.env_for("/")
21+
subject.call(env).last.should be_nil
22+
end
23+
24+
context 'with specified parameter name' do
25+
before{ @options = {:parameter => ['v']}}
26+
it 'should set the API version based on the custom parameter name' do
27+
env = Rack::MockRequest.env_for("/awesome", {:params => {"v" => "v1"}})
28+
s = subject.call(env)[1]["api.version"] == "v1"
29+
end
30+
it 'should not set the API version based on the default param' do
31+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1"}})
32+
s = subject.call(env)[1]["api.version"] == nil
33+
end
34+
end
35+
36+
context 'with specified versions' do
37+
before{ @options = {:versions => ['v1', 'v2']}}
38+
it 'should throw an error if a non-allowed version is specified' do
39+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v3"}})
40+
catch(:error){subject.call(env)}[:status].should == 404
41+
end
42+
43+
it 'should allow versions that have been specified' do
44+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1"}})
45+
subject.call(env)[1]["api.version"].should == 'v1'
46+
end
47+
end
48+
49+
it 'should return a 200 when no version is set (matches the first version found)' do
50+
@options = {
51+
:versions => ['v1'],
52+
:version_options => {:using => :header}
53+
}
54+
env = Rack::MockRequest.env_for("/awesome", {:params => {}})
55+
subject.call(env).first.should == 200
56+
end
57+
58+
end

spec/grape/middleware/versioner_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@
99
it 'should recognize :header' do
1010
klass.using(:header).should == Grape::Middleware::Versioner::Header
1111
end
12+
13+
it 'should recognize :param' do
14+
klass.using(:param).should == Grape::Middleware::Versioner::Param
15+
end
1216
end

0 commit comments

Comments
 (0)