Skip to content

Commit

Permalink
Merge pull request #809 from ajvondrak/master
Browse files Browse the repository at this point in the history
Don't use `.format` suffix in paths if you only have one format
  • Loading branch information
dblock committed Nov 13, 2014
2 parents ec2c9ac + 9f05356 commit 2302e26
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [#779](https://github.com/intridea/grape/pull/779): Fixed using `values` with a `default` proc - [@ShPakvel](https://github.com/ShPakvel).
* [#799](https://github.com/intridea/grape/pull/799): Fixed custom validators with required `Hash`, `Array` types - [@bwalex](https://github.com/bwalex).
* [#784](https://github.com/intridea/grape/pull/784): Fixed `present` to not overwrite the previously added contents of the response body whebn called more than once - [@mfunaro](https://github.com/mfunaro).
* [#809](https://github.com/intridea/grape/pull/809): Removed automatic `(.:format)` suffix on paths if you're using only one format (e.g., with `format :json`, `/path` will respond with JSON but `/path.xml` will be a 404) - [@ajvondrak](https://github.com/ajvondrak).
* Your contribution here.

0.9.0 (8/27/2014)
Expand Down
162 changes: 114 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,12 @@ run Twitter::API

And would respond to the following routes:

GET /api/statuses/public_timeline(.json)
GET /api/statuses/home_timeline(.json)
GET /api/statuses/:id(.json)
POST /api/statuses(.json)
PUT /api/statuses/:id(.json)
DELETE /api/statuses/:id(.json)
GET /api/statuses/public_timeline
GET /api/statuses/home_timeline
GET /api/statuses/:id
POST /api/statuses
PUT /api/statuses/:id
DELETE /api/statuses/:id

Grape will also automatically respond to HEAD and OPTIONS for all GET, and just OPTIONS for all other routes.

Expand Down Expand Up @@ -1332,16 +1332,115 @@ end

## API Formats

By default, Grape supports _XML_, _JSON_, _BINARY_, and _TXT_ content-types. The default format is `:txt`.
Your API can declare which content-types to support by using `content_type`. If you do not specify any, Grape will support
_XML_, _JSON_, _BINARY_, and _TXT_ content-types. The default format is `:txt`; you can change this with `default_format`.
Essentially, the two APIs below are equivalent.

Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API implementation.
```ruby
class Twitter::API < Grape::API
# no content_type declarations, so Grape uses the defaults
end

class Twitter::API < Grape::API
# the following declarations are equivalent to the defaults

content_type :xml, 'application/xml'
content_type :json, 'application/json'
content_type :binary, 'application/octet-stream'
content_type :txt, 'text/plain'

default_format :txt
end
```

If you declare any `content_type` whatsoever, the Grape defaults will be overridden. For example, the following API will only
support the `:xml` and `:rss` content-types, but not `:txt`, `:json`, or `:binary`. Importantly, this means the `:txt`
default format is not supported! So, make sure to set a new `default_format`.

```ruby
class Twitter::API < Grape::API
content_type :xml, 'application/xml'
content_type :rss, 'application/xml+rss'

default_format :xml
end
```

Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API endpoint
implementation. The response format (and thus the automatic serialization) is determined in the following order:
* Use the file extension, if specified. If the file is .json, choose the JSON format.
* Use the value of the `format` parameter in the query string, if specified.
* Use the format set by the `format` option, if specified.
* Attempt to find an acceptable format from the `Accept` header.
* Use the default format, if specified by the `default_format` option.
* Default to `:txt`.

For example, consider the following API.

```ruby
class MultipleFormatAPI < Grape::API
content_type :xml, 'application/xml'
content_type :json, 'application/json'

default_format :json

get :hello do
{ hello: 'world' }
end
end
```

* `GET /hello` (with an `Accept: */*` header) does not have an extension or a `format` parameter, so it will respond with
JSON (the default format).
* `GET /hello.xml` has a recognized extension, so it will respond with XML.
* `GET /hello?format=xml` has a recognized `format` parameter, so it will respond with XML.
* `GET /hello.xml?format=json` has a recognized extension (which takes precedence over the `format` parameter), so it will
respond with XML.
* `GET /hello.xls` (with an `Accept: */*` header) has an extension, but that extension is not recognized, so it will respond
with JSON (the default format).
* `GET /hello.xls` with an `Accept: application/xml` header has an unrecognized extension, but the `Accept` header
corresponds to a recognized format, so it will respond with XML.
* `GET /hello.xls` with an `Accept: text/plain` header has an unrecognized extension *and* an unrecognized `Accept` header,
so it will respond with JSON (the default format).

You can override this process explicitly by specifying `env['api.format']` in the API itself.
For example, the following API will let you upload arbitrary files and return their contents as an attachment with the correct MIME type.

```ruby
class Twitter::API < Grape::API
post "attachment" do
filename = params[:file][:filename]
content_type MIME::Types.type_for(filename)[0].to_s
env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is"
header "Content-Disposition", "attachment; filename*=UTF-8''#{URI.escape(filename)}"
params[:file][:tempfile].read
end
end
```

You can have your API only respond to a single format with `format`. If you use this, the API will **not** respond to file
extensions. For example, consider the following API.

```ruby
class SingleFormatAPI < Grape::API
format :json

get :hello do
{ hello: 'world' }
end
end
```

Your API can declare which types to support by using `content_type`. Response format is determined by the
request's extension, an explicit `format` parameter in the query string, or `Accept` header.
* `GET /hello` will respond with JSON.
* `GET /hello.xml`, `GET /hello.json`, `GET /hello.foobar`, or *any* other extension will respond with an HTTP 404 error code.
* `GET /hello?format=xml` will respond with an HTTP 406 error code, because the XML format specified by the request parameter
is not supported.
* `GET /hello` with an `Accept: application/xml` header will still respond with JSON, since it could not negotiate a
recognized content-type from the headers and JSON is the effective default.

The following API will only respond to the JSON content-type and will not parse any other input than `application/json`,
`application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests
will fail with an HTTP 406 error code.
The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other
input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and
`multipart/mixed`. All other requests will fail with an HTTP 406 error code.

```ruby
class Twitter::API < Grape::API
Expand Down Expand Up @@ -1394,46 +1493,13 @@ class Twitter::API < Grape::API
end
```

Built-in formats are the following.
Built-in formatters are the following.

* `:json`: use object's `to_json` when available, otherwise call `MultiJson.dump`
* `:xml`: use object's `to_xml` when available, usually via `MultiXml`, otherwise call `to_s`
* `:txt`: use object's `to_txt` when available, otherwise `to_s`
* `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json`
* `:binary`

Use `default_format` to set the fallback format when the format could not be determined from the `Accept` header.
See below for the order for choosing the API format.

```ruby
class Twitter::API < Grape::API
default_format :json
end
```

The order for choosing the format is the following.

* Use the file extension, if specified. If the file is .json, choose the JSON format.
* Use the value of the `format` parameter in the query string, if specified.
* Use the format set by the `format` option, if specified.
* Attempt to find an acceptable format from the `Accept` header.
* Use the default format, if specified by the `default_format` option.
* Default to `:txt`.

You can override this process explicitly by specifying `env['api.format']` in the API itself.
For example, the following API will let you upload arbitrary files and return their contents as an attachment with the correct MIME type.

```ruby
class Twitter::API < Grape::API
post "attachment" do
filename = params[:file][:filename]
content_type MIME::Types.type_for(filename)[0].to_s
env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is"
header "Content-Disposition", "attachment; filename*=UTF-8''#{URI.escape(filename)}"
params[:file][:tempfile].read
end
end
```
* `:binary`: data will be returned "as is"

### JSONP

Expand Down
8 changes: 7 additions & 1 deletion lib/grape/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def root_prefix
split_setting(:root_prefix, '/')
end

def uses_specific_format?
!!(settings[:format] && settings[:content_types].size == 1)
end

def uses_path_versioning?
!!(settings[:version] && settings[:version_options][:using] == :path)
end
Expand All @@ -33,7 +37,9 @@ def has_path?
end

def suffix
if !uses_path_versioning? || (has_namespace? || has_path?)
if uses_specific_format?
''
elsif !uses_path_versioning? || (has_namespace? || has_path?)
'(.:format)'
else
'(/.:format)'
Expand Down
10 changes: 6 additions & 4 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ def app
describe 'root routes should work with' do
before do
subject.format :txt
subject.content_type :json, "application/json"
subject.formatter :json, lambda { |object, env| object }
def subject.enable_root_route!
get("/") { "root" }
end
Expand Down Expand Up @@ -784,14 +786,14 @@ def subject.enable_root_route!
it 'sets content type for json error' do
subject.format :json
subject.get('/error') { error!('error in json', 500) }
get '/error.json'
get '/error'
expect(last_response.headers['Content-Type']).to eql 'application/json'
end

it 'sets content type for xml error' do
subject.format :xml
subject.get('/error') { error!('error in xml', 500) }
get '/error.xml'
get '/error'
expect(last_response.headers['Content-Type']).to eql 'application/xml'
end

Expand Down Expand Up @@ -2420,9 +2422,9 @@ def static
get '/meaning_of_life'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
it 'forces txt with the wrong extension' do
it 'does not accept any extensions' do
get '/meaning_of_life.json'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
expect(last_response.status).to eq(404)
end
it 'forces txt from a non-accepting header' do
get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
Expand Down
23 changes: 23 additions & 0 deletions spec/grape/path_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,19 @@ module Grape
end

describe "#suffix" do
context "when using a specific format" do
it "is empty" do
path = Path.new(nil, nil, {})
allow(path).to receive(:uses_specific_format?) { true }

expect(path.suffix).to eql('')
end
end

context "when path versioning is used" do
it "includes a '/'" do
path = Path.new(nil, nil, {})
allow(path).to receive(:uses_specific_format?) { false }
allow(path).to receive(:uses_path_versioning?) { true }

expect(path.suffix).to eql('(/.:format)')
Expand All @@ -194,20 +204,23 @@ module Grape
context "when path versioning is not used" do
it "does not include a '/' when the path has a namespace" do
path = Path.new(nil, 'namespace', {})
allow(path).to receive(:uses_specific_format?) { false }
allow(path).to receive(:uses_path_versioning?) { true }

expect(path.suffix).to eql('(.:format)')
end

it "does not include a '/' when the path has a path" do
path = Path.new('/path', nil, {})
allow(path).to receive(:uses_specific_format?) { false }
allow(path).to receive(:uses_path_versioning?) { true }

expect(path.suffix).to eql('(.:format)')
end

it "includes a '/' otherwise" do
path = Path.new(nil, nil, {})
allow(path).to receive(:uses_specific_format?) { false }
allow(path).to receive(:uses_path_versioning?) { true }

expect(path.suffix).to eql('(/.:format)')
Expand All @@ -223,6 +236,16 @@ module Grape

expect(path.path_with_suffix).to eql('/the/pathsuffix')
end

context "when using a specific format" do
it "does not have a suffix" do
path = Path.new(nil, nil, {})
allow(path).to receive(:path) { '/the/path' }
allow(path).to receive(:uses_specific_format?) { true }

expect(path.path_with_suffix).to eql('/the/path')
end
end
end

end
Expand Down

0 comments on commit 2302e26

Please sign in to comment.