Skip to content

Commit a7e4b2c

Browse files
committed
Merge pull request #1170 from suan/better_vendor_resource_handling
allow dashes, periods, and other RFC6838-compliant characters in header vendor
2 parents 3ffbad5 + 460e443 commit a7e4b2c

File tree

4 files changed

+40
-12
lines changed

4 files changed

+40
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
* Your contribution here.
77

8+
* [#1170](https://github.com/ruby-grape/grape/pull/1170): Allow dashes and periods in header vendor - [@suan](https://github.com/suan).
89
* [#1167](https://github.com/ruby-grape/grape/pull/1167): Convenience wrapper `type: File` for validating multipart file parameters - [@dslh](https://github.com/dslh).
910
* [#1167](https://github.com/ruby-grape/grape/pull/1167): Refactor and extend coercion and type validation system - [@dslh](https://github.com/dslh).
1011
* [#1163](https://github.com/ruby-grape/grape/pull/1163): First-class `JSON` parameter type - [@dslh](https://github.com/dslh).

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,17 @@ Using this versioning strategy, clients should pass the desired version in the U
332332
version 'v1', using: :header, vendor: 'twitter'
333333
```
334334

335+
Currently, Grape only supports versioned media types in the following format:
336+
337+
```
338+
vnd.vendor-and-or-resource-v1234+format
339+
```
340+
341+
Basically all tokens between the final `-` and the `+` will be interpreted as the version.
342+
Grape also only supports alphanumerics, periods, and dashes in the vendor/resource/version parts
343+
of the media type, even though [the appropriate RFC](http://tools.ietf.org/html/rfc6838#section-4.2)
344+
technically allows far more characters.
345+
335346
Using this versioning strategy, clients should pass the desired version in the HTTP `Accept` head.
336347

337348
curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline

lib/grape/middleware/versioner/header.rb

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ module Versioner
88
# application/vnd.:vendor-:version+:format
99
#
1010
# Example: For request header
11-
# Accept: application/vnd.mycompany-v1+json
11+
# Accept: application/vnd.mycompany.a-cool-resource-v1+json
1212
#
1313
# The following rack env variables are set:
1414
#
1515
# env['api.type'] => 'application'
16-
# env['api.subtype'] => 'vnd.mycompany-v1+json'
17-
# env['api.vendor] => 'mycompany'
16+
# env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json'
17+
# env['api.vendor] => 'mycompany.a-cool-resource'
1818
# env['api.version] => 'v1'
1919
# env['api.format] => 'json'
2020
#
@@ -23,7 +23,10 @@ module Versioner
2323
# route.
2424
class Header < Base
2525
VENDOR_VERSION_HEADER_REGEX =
26-
/\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/
26+
/\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/
27+
28+
HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/
29+
HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/
2730

2831
def before
2932
strict_header_checks if strict?
@@ -122,7 +125,7 @@ def headers_contain_wrong_vendor?
122125

123126
def headers_contain_wrong_version?
124127
header.values.all? do |header_value|
125-
version?(header_value)
128+
version?(header_value) && !versions.include?(request_version(header_value))
126129
end
127130
end
128131

@@ -169,19 +172,24 @@ def error_headers
169172
# @return [Boolean] whether the content type sets a vendor
170173
def vendor?(media_type)
171174
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
172-
subtype[/\Avnd\.[a-z0-9*.]+/]
175+
subtype[HAS_VENDOR_REGEX]
173176
end
174177

175178
def request_vendor(media_type)
176179
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
177180
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
178181
end
179182

183+
def request_version(media_type)
184+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
185+
subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
186+
end
187+
180188
# @param [String] media_type a content type
181189
# @return [Boolean] whether the content type sets an API version
182190
def version?(media_type)
183191
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
184-
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
192+
subtype[HAS_VERSION_REGEX]
185193
end
186194
end
187195
end

spec/grape/middleware/versioner/header_spec.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@
252252
end
253253
end
254254

255-
context 'when there are multiple versions specified with rescue_from :all' do
255+
context 'when there are multiple versions with complex vendor specified with rescue_from :all' do
256256
subject {
257257
Class.new(Grape::API) do
258258
rescue_from :all
@@ -261,7 +261,11 @@
261261

262262
let(:v1_app) {
263263
Class.new(Grape::API) do
264-
version 'v1', using: :header, vendor: 'test'
264+
version 'v1', using: :header, vendor: 'test.a-cool-resource'
265+
content_type :v1_test, 'application/vnd.test.a-cool-resource-v1+json'
266+
formatter :v1_test, ->(object, _) { object }
267+
format :v1_test
268+
265269
resources :users do
266270
get :hello do
267271
'one'
@@ -272,7 +276,11 @@
272276

273277
let(:v2_app) {
274278
Class.new(Grape::API) do
275-
version 'v2', using: :header, vendor: 'test'
279+
version 'v2', using: :header, vendor: 'test.a-cool-resource'
280+
content_type :v2_test, 'application/vnd.test.a-cool-resource-v2+json'
281+
formatter :v2_test, ->(object, _) { object }
282+
format :v2_test
283+
276284
resources :users do
277285
get :hello do
278286
'two'
@@ -289,13 +297,13 @@ def app
289297

290298
context 'with header versioned endpoints and a rescue_all block defined' do
291299
it 'responds correctly to a v1 request' do
292-
versioned_get '/users/hello', 'v1', using: :header, vendor: 'test'
300+
versioned_get '/users/hello', 'v1', using: :header, vendor: 'test.a-cool-resource'
293301
expect(last_response.body).to eq('one')
294302
expect(last_response.body).not_to include('API vendor or version not found')
295303
end
296304

297305
it 'responds correctly to a v2 request' do
298-
versioned_get '/users/hello', 'v2', using: :header, vendor: 'test'
306+
versioned_get '/users/hello', 'v2', using: :header, vendor: 'test.a-cool-resource'
299307
expect(last_response.body).to eq('two')
300308
expect(last_response.body).not_to include('API vendor or version not found')
301309
end

0 commit comments

Comments
 (0)