grape-1.0.2/ 0000755 0000041 0000041 00000000000 13231337007 012637 5 ustar www-data www-data grape-1.0.2/Rakefile 0000644 0000041 0000041 00000001047 13231337007 014306 0 ustar www-data www-data require 'rubygems'
require 'bundler'
Bundler.setup :default, :test, :development
Bundler::GemHelper.install_tasks
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
spec.exclude_pattern = 'spec/integration/**/*_spec.rb'
end
RSpec::Core::RakeTask.new(:rcov) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true
end
task :spec
require 'rainbow/ext/string' unless String.respond_to?(:color)
require 'rubocop/rake_task'
RuboCop::RakeTask.new
task default: %i[rubocop spec]
grape-1.0.2/Gemfile.lock 0000644 0000041 0000041 00000012275 13231337007 015070 0 ustar www-data www-data PATH
remote: .
specs:
grape (1.0.2)
activesupport
builder
mustermann-grape (~> 1.0.0)
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
GEM
remote: https://rubygems.org/
specs:
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
appraisal (2.2.0)
bundler
rake
thor (>= 0.14.0)
ast (2.3.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
benchmark-ips (2.7.2)
builder (3.2.3)
claide (1.0.2)
claide-plugins (0.9.2)
cork
nap
open4 (~> 1.3)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colored (1.2)
colored2 (3.1.2)
concurrent-ruby (1.0.5)
cookiejar (0.3.3)
cork (0.3.0)
colored2 (~> 3.1)
coveralls (0.8.21)
json (>= 1.8, < 3)
simplecov (~> 0.14.1)
term-ansicolor (~> 1.3)
thor (~> 0.19.4)
tins (~> 1.6)
danger (4.0.5)
claide (~> 1.0)
claide-plugins (>= 0.9.2)
colored (~> 1.2)
cork (~> 0.1)
faraday (~> 0.9)
faraday-http-cache (~> 1.0)
git (~> 1)
kramdown (~> 1.5)
octokit (~> 4.2)
terminal-table (~> 1)
danger-changelog (0.2.1)
danger-plugin-api (~> 1.0)
danger-plugin-api (1.0.0)
danger (> 2.0)
danger-toc (0.1.0)
activesupport
danger-plugin-api (~> 1.0)
kramdown
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
diff-lcs (1.3)
docile (1.1.5)
equalizer (0.0.11)
faraday (0.13.1)
multipart-post (>= 1.2, < 3)
faraday-http-cache (1.3.1)
faraday (~> 0.8)
ffi (1.9.18)
formatador (0.2.5)
git (1.3.0)
grape-entity (0.6.1)
activesupport (>= 5.0.0)
multi_json (>= 1.3.2)
guard (2.14.2)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
guard-rspec (4.7.3)
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
guard-rubocop (1.3.0)
guard (~> 2.0)
rubocop (~> 0.20)
hashie (3.5.7)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
json (2.1.0)
kramdown (1.16.2)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
lumberjack (1.0.12)
maruku (0.7.3)
method_source (0.9.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
minitest (5.11.1)
multi_json (1.13.1)
multipart-post (2.0.0)
mustermann (1.0.1)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
nap (1.1.0)
nenv (0.3.0)
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3)
open4 (1.3.4)
parallel (1.12.1)
parser (2.4.0.2)
ast (~> 2.3)
powerpack (0.1.1)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
public_suffix (3.0.1)
rack (2.0.3)
rack-accept (0.4.5)
rack (>= 0.4)
rack-jsonp (1.3.1)
rack
rack-test (0.6.3)
rack (>= 1.0)
rainbow (2.2.2)
rake
rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.0)
rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-grape-danger (0.1.1)
danger (~> 4.0.1)
danger-changelog (~> 0.2.0)
ruby-progressbar (1.9.0)
ruby_dep (1.5.0)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
shellany (0.0.1)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
term-ansicolor (1.6.0)
tins (~> 1.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.19.4)
thread_safe (0.3.6)
tins (1.16.3)
tzinfo (1.2.4)
thread_safe (~> 0.1)
unicode-display_width (1.3.0)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
PLATFORMS
ruby
DEPENDENCIES
appraisal
benchmark-ips
bundler
cookiejar
coveralls (~> 0.8.17)
danger-toc (~> 0.1.0)
grape!
grape-entity (~> 0.6)
guard
guard-rspec
guard-rubocop
hashie
maruku
mime-types
rack-jsonp
rack-test (~> 0.6.3)
rake
rspec (~> 3.0)
rubocop (= 0.51.0)
ruby-grape-danger (~> 0.1.0)
BUNDLED WITH
1.16.0
grape-1.0.2/UPGRADING.md 0000644 0000041 0000041 00000067476 13231337007 014525 0 ustar www-data www-data Upgrading Grape
===============
### Upgrading to >= 1.0.0
#### Changes in XML and JSON Parsers
Grape no longer uses `multi_json` or `multi_xml` by default and uses `JSON` and `ActiveSupport::XmlMini` instead. This has no visible impact on JSON processing, but the default behavior of the XML parser has changed. For example, an XML POST containing `Bobby T.` was parsed as `Bobby T.` with `multi_xml`, and as now parsed as `{"__content__"=>"Bobby T."}` with `XmlMini`.
If you were using `MultiJson.load`, `MultiJson.dump` or `MultiXml.parse`, you can substitute those with `Grape::Json.load`, `Grape::Json.dump`, `::Grape::Xml.parse`, or directly with `JSON.load`, `JSON.dump`, `XmlMini.parse`, etc.
To restore previous behavior, add `multi_json` or `multi_xml` to your `Gemfile` and `require` it.
See [#1623](https://github.com/ruby-grape/grape/pull/1623) for more information.
#### Changes in Parameter Class
The default class for `params` has changed from `Hashie::Mash` to `ActiveSupport::HashWithIndifferentAccess` and the `hashie` dependency has been removed. This means that by default you can no longer access parameters by method name.
```ruby
class API < Grape::API
params do
optional :color, type: String
end
get do
params[:color] # use params[:color] instead of params.color
end
end
```
To restore the behavior of prior versions, add `hashie` to your `Gemfile` and `include Grape::Extensions::Hashie::Mash::ParamBuilder` in your API.
```ruby
class API < Grape::API
include Grape::Extensions::Hashie::Mash::ParamBuilder
params do
optional :color, type: String
end
get do
# params.color works
end
end
```
This behavior can also be overridden on individual parameter blocks using `build_with`.
```ruby
params do
build_with Grape::Extensions::Hash::ParamBuilder
optional :color, type: String
end
```
If you're constructing your own `Grape::Request` in a middleware, you can pass different parameter handlers to create the desired `params` class with `build_params_with`.
```ruby
def request
Grape::Request.new(env, build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder)
end
```
See [#1610](https://github.com/ruby-grape/grape/pull/1610) for more information.
#### The `except`, `except_message`, and `proc` options of the `values` validator are deprecated.
The new `except_values` validator should be used in place of the `except` and `except_message` options of
the `values` validator.
Arity one Procs may now be used directly as the `values` option to explicitly test param values.
**Deprecated**
```ruby
params do
requires :a, values: { value: 0..99, except: [3] }
requires :b, values: { value: 0..99, except: [3], except_message: 'not allowed' }
requires :c, values: { except: ['admin'] }
requires :d, values: { proc: -> (v) { v.even? } }
end
```
**New**
```ruby
params do
requires :a, values: 0..99, except_values: [3]
requires :b, values: 0..99, except_values: { value: [3], message: 'not allowed' }
requires :c, except_values: ['admin']
requires :d, values: -> (v) { v.even? }
end
```
See [#1616](https://github.com/ruby-grape/grape/pull/1616) for more information.
### Upgrading to >= 0.19.1
#### DELETE now defaults to status code 200 for responses with a body, or 204 otherwise
Prior to this version, DELETE requests defaulted to a status code of 204 No Content, even when the response included content. This behavior confused some clients and prevented the formatter middleware from running properly. As of this version, DELETE requests will only default to a 204 No Content status code if no response body is provided, and will default to 200 OK otherwise.
Specifically, DELETE behaviour has changed as follows:
- In versions < 0.19.0, all DELETE requests defaulted to a 200 OK status code.
- In version 0.19.0, all DELETE requests defaulted to a 204 No Content status code, even when content was included in the response.
- As of version 0.19.1, DELETE requests default to a 204 No Content status code, unless content is supplied, in which case they default to a 200 OK status code.
To achieve the old behavior, one can specify the status code explicitly:
```ruby
delete :id do
status 204 # or 200, for < 0.19.0 behavior
'foo successfully deleted'
end
```
One can also use the new `return_no_content` helper to explicitly return a 204 status code and an empty body for any request type:
```ruby
delete :id do
return_no_content
'this will not be returned'
end
```
See [#1550](https://github.com/ruby-grape/grape/pull/1550) for more information.
### Upgrading to >= 0.18.1
#### Changes in priority of :any routes
Prior to this version, `:any` routes were searched after matching first route and 405 routes. This behavior has changed and `:any` routes are now searched before 405 processing. In the following example the `:any` route will match first when making a request with an unsupported verb.
```ruby
post :example do
'example'
end
route :any, '*path' do
error! :not_found, 404
end
get '/example' #=> before: 405, after: 404
```
#### Removed param processing from built-in OPTIONS handler
When a request is made to the built-in `OPTIONS` handler, only the `before` and `after`
callbacks associated with the resource will be run. The `before_validation` and
`after_validation` callbacks and parameter validations will be skipped.
See [#1505](https://github.com/ruby-grape/grape/pull/1505) for more information.
#### Changed endpoint params validation
Grape now correctly returns validation errors for all params when multiple params are passed to a requires.
The following code will return `one is missing, two is missing` when calling the endpoint without parameters.
```ruby
params do
requires :one, :two
end
```
Prior to this version the response would be `one is missing`.
See [#1510](https://github.com/ruby-grape/grape/pull/1510) for more information.
#### The default status code for DELETE is now 204 instead of 200.
Breaking change: Sets the default response status code for a delete request to 204.
A status of 204 makes the response more distinguishable and therefore easier to handle on the client side, particularly because a DELETE request typically returns an empty body as the resource was deleted or voided.
To achieve the old behavior, one has to set it explicitly:
```ruby
delete :id do
status 200
'foo successfully deleted'
end
```
For more information see: [#1532](https://github.com/ruby-grape/grape/pull/1532).
### Upgrading to >= 0.17.0
#### Removed official support for Ruby < 2.2.2
Grape is no longer automatically tested against versions of Ruby prior to 2.2.2. This is because of its dependency on activesupport which, with version 5.0.0, now requires at least Ruby 2.2.2.
See [#1441](https://github.com/ruby-grape/grape/pull/1441) for nmore information.
#### Changed priority of `rescue_from` clauses applying
The `rescue_from` clauses declared inside a namespace would take a priority over ones declared in the root scope.
This could possibly affect those users who use different `rescue_from` clauses in root scope and in namespaces.
See [#1405](https://github.com/ruby-grape/grape/pull/1405) for more information.
#### Helper methods injected inside `rescue_from` in middleware
Helper methods are injected inside `rescue_from` may cause undesirable effects. For example, definining a helper method called `error!` will take precendence over the built-in `error!` method and should be renamed.
See [#1451](https://github.com/ruby-grape/grape/issues/1451) for an example.
### Upgrading to >= 0.16.0
#### Replace rack-mount with new router
The `Route#route_xyz` methods have been deprecated since 0.15.1.
Please use `Route#xyz` instead.
Note that the `Route#route_method` was replaced by `Route#request_method`.
The following code would work correctly.
```ruby
TwitterAPI::versions # yields [ 'v1', 'v2' ]
TwitterAPI::routes # yields an array of Grape::Route objects
TwitterAPI::routes[0].version # => 'v1'
TwitterAPI::routes[0].description # => 'Includes custom settings.'
TwitterAPI::routes[0].settings[:custom] # => { key: 'value' }
TwitterAPI::routes[0].request_method # => 'GET'
```
#### `file` method accepts path to file
Now to serve files via Grape just pass the path to the file. Functionality with FileStreamer-like objects is deprecated.
Please, replace your FileStreamer-like objects with paths of served files.
Old style:
```ruby
class FileStreamer
def initialize(file_path)
@file_path = file_path
end
def each(&blk)
File.open(@file_path, 'rb') do |file|
file.each(10, &blk)
end
end
end
# ...
class API < Grape::API
get '/' do
file FileStreamer.new('/path/to/file')
end
end
```
New style:
```ruby
class API < Grape::API
get '/' do
file '/path/to/file'
end
end
```
### Upgrading to >= 0.15.0
#### Changes to availability of `:with` option of `rescue_from` method
The `:with` option of `rescue_from` does not accept value except Proc, String or Symbol now.
If you have been depending the old behavior, you should use lambda block instead.
```ruby
class API < Grape::API
rescue_from :all, with: -> { Rack::Response.new('rescued with a method', 400) }
end
```
#### Changes to behavior of `after` method of middleware on error
The `after` method of the middleware is now also called on error. The following code would work correctly.
```ruby
class ErrorMiddleware < Grape::Middleware::Base
def after
return unless @app_response && @app_response[0] == 500
env['rack.logger'].debug("Raised error on #{env['PATH_INFO']}")
end
end
```
See [#1147](https://github.com/ruby-grape/grape/issues/1147) and [#1240](https://github.com/ruby-grape/grape/issues/1240) for discussion of the issues.
A warning will be logged if an exception is raised in an `after` callback, which points you to middleware that was not called in the previous version and is called now.
```
caught error of type NoMethodError in after callback inside Api::Middleware::SomeMiddleware : undefined method `headers' for nil:NilClass
```
See [#1285](https://github.com/ruby-grape/grape/pull/1285) for more information.
#### Changes to Method Not Allowed routes
A `405 Method Not Allowed` error now causes `Grape::Exceptions::MethodNotAllowed` to be raised, which will be rescued via `rescue_from :all`. Restore old behavior with the following error handler.
```ruby
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
error! e.message, e.status, e.headers
end
```
See [#1283](https://github.com/ruby-grape/grape/pull/1283) for more information.
#### Changes to Grape::Exceptions::Validation parameters
When raising `Grape::Exceptions::Validation` explicitly, replace `message_key` with `message`.
For example,
```ruby
fail Grape::Exceptions::Validation, params: [:oauth_token_secret], message_key: :presence
```
becomes
```ruby
fail Grape::Exceptions::Validation, params: [:oauth_token_secret], message: :presence
```
See [#1295](https://github.com/ruby-grape/grape/pull/1295) for more information.
### Upgrading to >= 0.14.0
#### Changes to availability of DSL methods in filters
The `#declared` method of the route DSL is no longer available in the `before` filter. Using `declared` in a `before` filter will now raise `Grape::DSL::InsideRoute::MethodNotYetAvailable`.
See [#1074](https://github.com/ruby-grape/grape/issues/1074) for discussion of the issue.
#### Changes to header versioning and invalid header version handling
Identical endpoints with different versions now work correctly. A regression introduced in Grape 0.11.0 caused all but the first-mounted version for such an endpoint to wrongly throw an `InvalidAcceptHeader`. As a side effect, requests with a correct vendor but invalid version can no longer be rescued from a `rescue_from` block.
See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information.
#### Bypasses formatters when status code indicates no content
To be consistent with rack and it's handling of standard responses
associated with no content, both default and custom formatters will now
be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567)
See [#1190](https://github.com/ruby-grape/grape/pull/1190) for more information.
#### Redirects respond as plain text with message
`#redirect` now uses `text/plain` regardless of whether that format has
been enabled. This prevents formatters from attempting to serialize the
message body and allows for a descriptive message body to be provided - and
optionally overridden - that better fulfills the theme of the HTTP spec.
See [#1194](https://github.com/ruby-grape/grape/pull/1194) for more information.
### Upgrading to >= 0.12.0
#### Changes in middleware
The Rack response object is no longer converted to an array by the formatter, enabling streaming. If your custom middleware is accessing `@app_response`, update it to expect a `Rack::Response` instance instead of an array.
For example,
```ruby
class CacheBusterMiddleware < Grape::Middleware::Base
def after
@app_response[1]['Expires'] = Time.at(0).utc.to_s
@app_response
end
end
```
becomes
```ruby
class CacheBusterMiddleware < Grape::Middleware::Base
def after
@app_response.headers['Expires'] = Time.at(0).utc.to_s
@app_response
end
end
```
See [#1029](https://github.com/ruby-grape/grape/pull/1029) for more information.
There is a known issue because of this change. When Grape is used with an older
than 1.2.4 version of [warden](https://github.com/hassox/warden) there may be raised
the following exception having the [rack-mount](https://github.com/jm/rack-mount) gem's
lines as last ones in the backtrace:
```
NoMethodError: undefined method `[]' for nil:NilClass
```
The issue can be solved by upgrading warden to 1.2.4 version.
See [#1151](https://github.com/ruby-grape/grape/issues/1151) for more information.
#### Changes in present
Using `present` with objects that responded to `merge` would cause early evaluation of the represented object, with unexpected side-effects, such as missing parameters or environment within rendering code. Grape now only merges represented objects with a previously rendered body, usually when multiple `present` calls are made in the same route.
See [grape-with-roar#5](https://github.com/dblock/grape-with-roar/issues/5) and [#1023](https://github.com/ruby-grape/grape/issues/1023).
#### Changes to regexp validator
Parameters with `nil` value will now pass `regexp` validation. To disallow `nil` value for an endpoint, add `allow_blank: false`.
```ruby
params do
requires :email, allow_blank: false, regexp: /.+@.+/
end
```
See [#957](https://github.com/ruby-grape/grape/pull/957) for more information.
#### Replace error_response with error! in rescue_from blocks
Note: `error_response` is being deprecated, not removed.
```ruby
def error!(message, status = options[:default_status], headers = {}, backtrace = [])
headers = { 'Content-Type' => content_type }.merge(headers)
rack_response(format_message(message, backtrace), status, headers)
end
```
For example,
```
error_response({ message: { message: 'No such page.', id: 'missing_page' }, status: 404, headers: { 'Content-Type' => 'api/error' })
```
becomes
```
error!({ message: 'No such page.', id: 'missing_page' }, 404, { 'Content-Type' => 'api/error' })
```
`error!` also supports just passing a message. `error!('Server error.')` and `format: :json` returns the following JSON response
```
{ 'error': 'Server error.' }
```
with a status code of 500 and a Content Type of text/error.
Optionally, also replace `Rack::Response.new` with `error!.`
The following are equivalent:
```
Rack::Response.new([ e.message ], 500, { "Content-type" => "text/error" }).finish
error!(e)
```
See [#889](https://github.com/ruby-grape/grape/issues/889) for more information.
#### Changes to routes when using `format`
Version 0.10.0 has introduced a change via [#809](https://github.com/ruby-grape/grape/pull/809) whereas routes no longer got file-type suffixes added if you declared a single API `format`. This has been reverted, it's now again possible to call API with proper suffix when single `format` is defined:
```ruby
class API < Grape::API
format :json
get :hello do
{ hello: 'world' }
end
end
```
Will respond with JSON to `/hello` **and** `/hello.json`.
Will respond with 404 to `/hello.xml`, `/hello.txt` etc.
See the [#1001](https://github.com/ruby-grape/grape/pull/1001) and [#914](https://github.com/ruby-grape/grape/issues/914) for more info.
### Upgrading to >= 0.11.0
#### Added Rack 1.6.0 support
Grape now supports, but doesn't require Rack 1.6.0. If you encounter an issue with parsing requests larger than 128KB, explictly require Rack 1.6.0 in your Gemfile.
```ruby
gem 'rack', '~> 1.6.0'
```
See [#559](https://github.com/ruby-grape/grape/issues/559) for more information.
#### Removed route_info
Key route_info is excluded from params.
See [#879](https://github.com/ruby-grape/grape/pull/879) for more information.
#### Fix callbacks within a version block
Callbacks defined in a version block are only called for the routes defined in that block. This was a regression introduced in Grape 0.10.0, and is fixed in this version.
See [#901](https://github.com/ruby-grape/grape/pull/901) for more information.
#### Make type of group of parameters required
Groups of parameters now require their type to be set explicitly as Array or Hash.
Not setting the type now results in MissingGroupTypeError, unsupported type will raise UnsupportedTypeError.
See [#886](https://github.com/ruby-grape/grape/pull/886) for more information.
### Upgrading to >= 0.10.1
#### Changes to `declared(params, include_missing: false)`
Attributes with `nil` values or with values that evaluate to `false` are no longer considered *missing* and will be returned when `include_missing` is set to `false`.
See [#864](https://github.com/ruby-grape/grape/pull/864) for more information.
### Upgrading to >= 0.10.0
#### Changes to content-types
The following content-types have been removed:
* atom (application/atom+xml)
* rss (application/rss+xml)
* jsonapi (application/jsonapi)
This is because they have never been properly supported.
#### Changes to desc
New block syntax:
Former:
```ruby
desc "some descs",
detail: 'more details',
entity: API::Entities::Entity,
params: API::Entities::Status.documentation,
named: 'a name',
headers: [XAuthToken: {
description: 'Valdates your identity',
required: true
}
get nil, http_codes: [
[401, 'Unauthorized', API::Entities::BaseError],
[404, 'not found', API::Entities::Error]
] do
```
Now:
```ruby
desc "some descs" do
detail 'more details'
params API::Entities::Status.documentation
success API::Entities::Entity
failure [
[401, 'Unauthorized', API::Entities::BaseError],
[404, 'not found', API::Entities::Error]
]
named 'a name'
headers [
XAuthToken: {
description: 'Valdates your identity',
required: true
},
XOptionalHeader: {
description: 'Not really needed',
required: false
}
]
end
```
#### Changes to Route Options and Descriptions
A common hack to extend Grape with custom DSL methods was manipulating `@last_description`.
``` ruby
module Grape
module Extensions
module SortExtension
def sort(value)
@last_description ||= {}
@last_description[:sort] ||= {}
@last_description[:sort].merge! value
value
end
end
Grape::API.extend self
end
end
```
You could access this value from within the API with `route.route_sort` or, more generally, via `env['api.endpoint'].options[:route_options][:sort]`.
This will no longer work, use the documented and supported `route_setting`.
``` ruby
module Grape
module Extensions
module SortExtension
def sort(value)
route_setting :sort, sort: value
value
end
end
Grape::API.extend self
end
end
```
To retrieve this value at runtime from within an API, use `env['api.endpoint'].route_setting(:sort)` and when introspecting a mounted API, use `route.route_settings[:sort]`.
#### Accessing Class Variables from Helpers
It used to be possible to fetch an API class variable from a helper function. For example:
```ruby
@@static_variable = 42
helpers do
def get_static_variable
@@static_variable
end
end
get do
get_static_variable
end
```
This will no longer work. Use a class method instead of a helper.
```ruby
@@static_variable = 42
def self.get_static_variable
@@static_variable
end
get do
get_static_variable
end
```
For more information see [#836](https://github.com/ruby-grape/grape/issues/836).
#### Changes to Custom Validators
To implement a custom validator, you need to inherit from `Grape::Validations::Base` instead of `Grape::Validations::Validator`.
For more information see [Custom Validators](https://github.com/ruby-grape/grape#custom-validators) in the documentation.
#### Changes to Raising Grape::Exceptions::Validation
In previous versions raising `Grape::Exceptions::Validation` required a single `param`.
```ruby
raise Grape::Exceptions::Validation, param: :id, message_key: :presence
```
The `param` argument has been deprecated and is now an array of `params`, accepting multiple values.
```ruby
raise Grape::Exceptions::Validation, params: [:id], message_key: :presence
```
#### Changes to routes when using `format`
Routes will no longer get file-type suffixes added if you declare a single API `format`. For example,
```ruby
class API < Grape::API
format :json
get :hello do
{ hello: 'world' }
end
end
```
Pre-0.10.0, this would respond with JSON to `/hello`, `/hello.json`, `/hello.xml`, `/hello.txt`, etc.
Now, this will only respond with JSON to `/hello`, but will be a 404 when trying to access `/hello.json`, `/hello.xml`, `/hello.txt`, etc.
If you declare further `content_type`s, this behavior will be circumvented. For example, the following API will respond with JSON to `/hello`, `/hello.json`, `/hello.xml`, `/hello.txt`, etc.
```ruby
class API < Grape::API
format :json
content_type :json, 'application/json'
get :hello do
{ hello: 'world' }
end
end
```
See the [the updated API Formats documentation](https://github.com/ruby-grape/grape#api-formats) and [#809](https://github.com/ruby-grape/grape/pull/809) for more info.
#### Changes to Evaluation of Permitted Parameter Values
Permitted and default parameter values are now only evaluated lazily for each request when declared as a proc. The following code would raise an error at startup time.
```ruby
params do
optional :v, values: -> { [:x, :y] }, default: -> { :z }
end
```
Remove the proc to get the previous behavior.
```ruby
params do
optional :v, values: [:x, :y], default: :z
end
```
See [#801](https://github.com/ruby-grape/grape/issues/801) for more information.
#### Changes to version
If version is used with a block, the callbacks defined within that version block are not scoped to that individual block. In other words, the callback would be inherited by all versions blocks that follow the first one e.g
```ruby
class API < Grape::API
resource :foo do
version 'v1', :using => :path do
before do
@output ||= 'hello1'
end
get '/' do
@output += '-v1'
end
end
version 'v2', :using => :path do
before do
@output ||= 'hello2'
end
get '/:id' do
@output += '-v2'
end
end
end
end
```
when making a API call `GET /foo/v2/1`, the API would set instance variable `@output` to `hello1-v2`
See [#898](https://github.com/ruby-grape/grape/issues/898) for more information.
### Upgrading to >= 0.9.0
#### Changes in Authentication
The following middleware classes have been removed:
* `Grape::Middleware::Auth::Basic`
* `Grape::Middleware::Auth::Digest`
* `Grape::Middleware::Auth::OAuth2`
When you use theses classes directly like:
```ruby
module API
class Root < Grape::API
class Protected < Grape::API
use Grape::Middleware::Auth::OAuth2,
token_class: 'AccessToken',
parameter: %w(access_token api_key)
```
you have to replace these classes.
As replacement can be used
* `Grape::Middleware::Auth::Basic` => [`Rack::Auth::Basic`](https://github.com/rack/rack/blob/master/lib/rack/auth/basic.rb)
* `Grape::Middleware::Auth::Digest` => [`Rack::Auth::Digest::MD5`](https://github.com/rack/rack/blob/master/lib/rack/auth/digest/md5.rb)
* `Grape::Middleware::Auth::OAuth2` => [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2)
If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth)
and host these files within your application
See [#703](https://github.com/ruby-grape/Grape/pull/703) for more information.
### Upgrading to >= 0.7.0
#### Changes in Exception Handling
Assume you have the following exception classes defined.
```ruby
class ParentError < StandardError; end
class ChildError < ParentError; end
```
In Grape <= 0.6.1, the `rescue_from` keyword only handled the exact exception being raised. The following code would rescue `ParentError`, but not `ChildError`.
```ruby
rescue_from ParentError do |e|
# only rescue ParentError
end
```
This made it impossible to rescue an exception hieararchy, which is a more sensible default. In Grape 0.7.0 or newer, both `ParentError` and `ChildError` are rescued.
```ruby
rescue_from ParentError do |e|
# rescue both ParentError and ChildError
end
```
To only rescue the base exception class, set `rescue_subclasses: false`.
```ruby
rescue_from ParentError, rescue_subclasses: false do |e|
# only rescue ParentError
end
```
See [#544](https://github.com/ruby-grape/grape/pull/544) for more information.
#### Changes in the Default HTTP Status Code
In Grape <= 0.6.1, the default status code returned from `error!` was 403.
```ruby
error! "You may not reticulate this spline!" # yields HTTP error 403
```
This was a bad default value, since 403 means "Forbidden". Change any call to `error!` that does not specify a status code to specify one. The new default value is a more sensible default of 500, which is "Internal Server Error".
```ruby
error! "You may not reticulate this spline!", 403 # yields HTTP error 403
```
You may also use `default_error_status` to change the global default.
```ruby
default_error_status 400
```
See [#525](https://github.com/ruby-grape/Grape/pull/525) for more information.
#### Changes in Parameter Declaration and Validation
In Grape <= 0.6.1, `group`, `optional` and `requires` keywords with a block accepted either an `Array` or a `Hash`.
```ruby
params do
requires :id, type: Integer
group :name do
requires :first_name
requires :last_name
end
end
```
This caused the ambiguity and unexpected errors described in [#543](https://github.com/ruby-grape/Grape/issues/543).
In Grape 0.7.0, the `group`, `optional` and `requires` keywords take an additional `type` attribute which defaults to `Array`. This means that without a `type` attribute, these nested parameters will no longer accept a single hash, only an array (of hashes).
Whereas in 0.6.1 the API above accepted the following json, it no longer does in 0.7.0.
```json
{
"id": 1,
"name": {
"first_name": "John",
"last_name" : "Doe"
}
}
```
The `params` block should now read as follows.
```ruby
params do
requires :id, type: Integer
requires :name, type: Hash do
requires :first_name
requires :last_name
end
end
```
See [#545](https://github.com/ruby-grape/Grape/pull/545) for more information.
### Upgrading to 0.6.0
In Grape <= 0.5.0, only the first validation error was raised and processing aborted. Validation errors are now collected and a single `Grape::Exceptions::ValidationErrors` exception is raised. You can access the collection of validation errors as `.errors`.
```ruby
rescue_from Grape::Exceptions::Validations do |e|
Rack::Response.new({
status: 422,
message: e.message,
errors: e.errors
}.to_json, 422)
end
```
For more information see [#462](https://github.com/ruby-grape/grape/issues/462).
grape-1.0.2/Gemfile 0000644 0000041 0000041 00000001247 13231337007 014136 0 ustar www-data www-data # when changing this file, run appraisal install ; rubocop -a gemfiles/*.gemfile
source 'https://rubygems.org'
gemspec
group :development, :test do
gem 'bundler'
gem 'hashie'
gem 'rake'
gem 'rubocop', '0.51.0'
end
group :development do
gem 'appraisal'
gem 'benchmark-ips'
gem 'guard'
gem 'guard-rspec'
gem 'guard-rubocop'
end
group :test do
gem 'cookiejar'
gem 'coveralls', '~> 0.8.17', require: false
gem 'danger-toc', '~> 0.1.0'
gem 'grape-entity', '~> 0.6'
gem 'maruku'
gem 'mime-types'
gem 'rack-jsonp', require: 'rack/jsonp'
gem 'rack-test', '~> 0.6.3'
gem 'rspec', '~> 3.0'
gem 'ruby-grape-danger', '~> 0.1.0', require: false
end
grape-1.0.2/Dangerfile 0000644 0000041 0000041 00000000075 13231337007 014624 0 ustar www-data www-data danger.import_dangerfile(gem: 'ruby-grape-danger')
toc.check
grape-1.0.2/grape.gemspec 0000644 0000041 0000041 00000001651 13231337007 015305 0 ustar www-data www-data $LOAD_PATH.push File.expand_path('../lib', __FILE__)
require 'grape/version'
Gem::Specification.new do |s|
s.name = 'grape'
s.version = Grape::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ['Michael Bleigh']
s.email = ['michael@intridea.com']
s.homepage = 'https://github.com/ruby-grape/grape'
s.summary = 'A simple Ruby framework for building REST-like APIs.'
s.description = 'A Ruby framework for rapid API development with great conventions.'
s.license = 'MIT'
s.add_runtime_dependency 'activesupport'
s.add_runtime_dependency 'builder'
s.add_runtime_dependency 'mustermann-grape', '~> 1.0.0'
s.add_runtime_dependency 'rack', '>= 1.3.0'
s.add_runtime_dependency 'rack-accept'
s.add_runtime_dependency 'virtus', '>= 1.0.0'
s.files = Dir['**/*'].keep_if { |file| File.file?(file) }
s.test_files = Dir['spec/**/*']
s.require_paths = ['lib']
end
grape-1.0.2/spec/ 0000755 0000041 0000041 00000000000 13231337007 013571 5 ustar www-data www-data grape-1.0.2/spec/grape/ 0000755 0000041 0000041 00000000000 13231337007 014667 5 ustar www-data www-data grape-1.0.2/spec/grape/middleware/ 0000755 0000041 0000041 00000000000 13231337007 017004 5 ustar www-data www-data grape-1.0.2/spec/grape/middleware/exception_spec.rb 0000644 0000041 0000041 00000021055 13231337007 022344 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Error do
# raises a text exception
module ExceptionSpec
class ExceptionApp
class << self
def call(_env)
raise 'rain!'
end
end
end
# raises a non-StandardError (ScriptError) exception
class OtherExceptionApp
class << self
def call(_env)
raise NotImplementedError, 'snow!'
end
end
end
# raises a hash error
class ErrorHashApp
class << self
def error!(message, status)
throw :error, message: { error: message, detail: 'missing widget' }, status: status
end
def call(_env)
error!('rain!', 401)
end
end
end
# raises an error!
class AccessDeniedApp
class << self
def error!(message, status)
throw :error, message: message, status: status
end
def call(_env)
error!('Access Denied', 401)
end
end
end
# raises a custom error
class CustomError < Grape::Exceptions::Base
end
class CustomErrorApp
class << self
def call(_env)
raise CustomError, status: 400, message: 'failed validation'
end
end
end
end
def app
subject
end
context 'with defaults' do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error
run ExceptionSpec::ExceptionApp
end
end
it 'does not trap errors by default' do
expect { get '/' }.to raise_error(RuntimeError, 'rain!')
end
end
context 'with rescue_all' do
context 'StandardError exception' do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true
run ExceptionSpec::ExceptionApp
end
end
it 'sets the message appropriately' do
get '/'
expect(last_response.body).to eq('rain!')
end
it 'defaults to a 500 status' do
get '/'
expect(last_response.status).to eq(500)
end
end
context 'Non-StandardError exception' do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true
run ExceptionSpec::OtherExceptionApp
end
end
it 'does not trap errors other than StandardError' do
expect { get '/' }.to raise_error(NotImplementedError, 'snow!')
end
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, default_status: 500
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to specify a different default status code' do
get '/'
expect(last_response.status).to eq(500)
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :json
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to return errors in json format' do
get '/'
expect(last_response.body).to eq('{"error":"rain!"}')
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :json
run ExceptionSpec::ErrorHashApp
end
end
it 'is possible to return hash errors in json format' do
get '/'
expect(['{"error":"rain!","detail":"missing widget"}',
'{"detail":"missing widget","error":"rain!"}']).to include(last_response.body)
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to return errors in jsonapi format' do
get '/'
expect(last_response.body).to eq('{"error":"rain!"}')
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
run ExceptionSpec::ErrorHashApp
end
end
it 'is possible to return hash errors in jsonapi format' do
get '/'
expect(['{"error":"rain!","detail":"missing widget"}',
'{"detail":"missing widget","error":"rain!"}']).to include(last_response.body)
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :xml
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to return errors in xml format' do
get '/'
expect(last_response.body).to eq("\n\n rain!\n\n")
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :xml
run ExceptionSpec::ErrorHashApp
end
end
it 'is possible to return hash errors in xml format' do
get '/'
expect(["\n\n missing widget\n rain!\n\n",
"\n\n rain!\n missing widget\n\n"]).to include(last_response.body)
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error,
rescue_all: true,
format: :custom,
error_formatters: {
custom: lambda do |message, _backtrace, _options, _env, _original_exception|
{ custom_formatter: message }.inspect
end
}
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to specify a custom formatter' do
get '/'
expect(last_response.body).to eq('{:custom_formatter=>"rain!"}')
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error
run ExceptionSpec::AccessDeniedApp
end
end
it 'does not trap regular error! codes' do
get '/'
expect(last_response.status).to eq(401)
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: false
run ExceptionSpec::CustomErrorApp
end
end
it 'responds to custom Grape exceptions appropriately' do
get '/'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('failed validation')
end
end
context 'with rescue_options :backtrace and :exception set to true' do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error,
rescue_all: true,
format: :json,
rescue_options: { backtrace: true, original_exception: true }
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to return the backtrace and the original exception in json format' do
get '/'
expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original_exception', 'RuntimeError')
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error,
rescue_all: true,
format: :xml,
rescue_options: { backtrace: true, original_exception: true }
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to return the backtrace and the original exception in xml format' do
get '/'
expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original-exception', 'RuntimeError')
end
end
context do
subject do
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error,
rescue_all: true,
format: :txt,
rescue_options: { backtrace: true, original_exception: true }
run ExceptionSpec::ExceptionApp
end
end
it 'is possible to return the backtrace and the original exception in txt format' do
get '/'
expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original exception', 'RuntimeError')
end
end
end
grape-1.0.2/spec/grape/middleware/versioner/ 0000755 0000041 0000041 00000000000 13231337007 021020 5 ustar www-data www-data grape-1.0.2/spec/grape/middleware/versioner/header_spec.rb 0000644 0000041 0000041 00000025764 13231337007 023625 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Versioner::Header do
let(:app) { ->(env) { [200, env, env] } }
subject { Grape::Middleware::Versioner::Header.new(app, @options || {}) }
before do
@options = {
version_options: {
using: :header,
vendor: 'vendor'
}
}
end
context 'api.type and api.subtype' do
it 'sets type and subtype to first choice of content type if no preference given' do
status, _, env = subject.call('HTTP_ACCEPT' => '*/*')
expect(env['api.type']).to eql 'application'
expect(env['api.subtype']).to eql 'vnd.vendor+xml'
expect(status).to eq(200)
end
it 'sets preferred type' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/*')
expect(env['api.type']).to eql 'application'
expect(env['api.subtype']).to eql 'vnd.vendor+xml'
expect(status).to eq(200)
end
it 'sets preferred type and subtype' do
status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain')
expect(env['api.type']).to eql 'text'
expect(env['api.subtype']).to eql 'plain'
expect(status).to eq(200)
end
end
context 'api.format' do
it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
expect(env['api.format']).to eql 'json'
expect(status).to eq(200)
end
it 'is nil if not provided' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
expect(env['api.format']).to eql nil
expect(status).to eq(200)
end
['v1', :v1].each do |version|
context "when version is set to #{version}" do
before do
@options[:versions] = [version]
end
it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
expect(env['api.format']).to eql 'json'
expect(status).to eq(200)
end
it 'is nil if not provided' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
expect(env['api.format']).to eql nil
expect(status).to eq(200)
end
end
end
end
context 'api.vendor' do
it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
expect(env['api.vendor']).to eql 'vendor'
expect(status).to eq(200)
end
it 'is set if format provided' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
expect(env['api.vendor']).to eql 'vendor'
expect(status).to eq(200)
end
it 'fails with 406 Not Acceptable if vendor is invalid' do
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last }
.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include 'API vendor not found'
end
end
context 'when version is set' do
before do
@options[:versions] = ['v1']
end
it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
expect(env['api.vendor']).to eql 'vendor'
expect(status).to eq(200)
end
it 'is set if format provided' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
expect(env['api.vendor']).to eql 'vendor'
expect(status).to eq(200)
end
it 'fails with 406 Not Acceptable if vendor is invalid' do
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last }
.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('API vendor not found')
end
end
end
end
context 'api.version' do
before do
@options[:versions] = ['v1']
end
it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
expect(env['api.version']).to eql 'v1'
expect(status).to eq(200)
end
it 'is set if format provided' do
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
expect(env['api.version']).to eql 'v1'
expect(status).to eq(200)
end
it 'fails with 406 Not Acceptable if version is invalid' do
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('API version not found')
end
end
end
it 'succeeds if :strict is not set' do
expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200)
expect(subject.call({}).first).to eq(200)
end
it 'succeeds if :strict is set to false' do
@options[:version_options][:strict] = false
expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200)
expect(subject.call({}).first).to eq(200)
end
context 'when :strict is set' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
end
it 'fails with 406 Not Acceptable if header is not set' do
expect { subject.call({}).last }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('Accept header must be set.')
end
end
it 'fails with 406 Not Acceptable if header is empty' do
expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('Accept header must be set.')
end
end
it 'succeeds if proper header is set' do
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
end
end
context 'when :strict and cascade: false' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
@options[:version_options][:cascade] = false
end
it 'fails with 406 Not Acceptable if header is not set' do
expect { subject.call({}).last }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql({})
expect(exception.status).to eql 406
expect(exception.message).to include('Accept header must be set.')
end
end
it 'fails with 406 Not Acceptable if header is application/xml' do
expect { subject.call('HTTP_ACCEPT' => 'application/xml').last }
.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql({})
expect(exception.status).to eql 406
expect(exception.message).to include('API vendor or version not found.')
end
end
it 'fails with 406 Not Acceptable if header is empty' do
expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql({})
expect(exception.status).to eql 406
expect(exception.message).to include('Accept header must be set.')
end
end
it 'fails with 406 Not Acceptable if header contains a single invalid accept' do
expect { subject.call('HTTP_ACCEPT' => 'application/json;application/vnd.vendor-v1+json').first }
.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
expect(exception.headers).to eql({})
expect(exception.status).to eql 406
expect(exception.message).to include('API vendor or version not found.')
end
end
it 'succeeds if proper header is set' do
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
end
end
context 'when multiple versions are specified' do
before do
@options[:versions] = %w[v1 v2]
end
it 'succeeds with v1' do
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
end
it 'succeeds with v2' do
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200)
end
it 'fails with another version' do
expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception|
expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
expect(exception.headers).to eql('X-Cascade' => 'pass')
expect(exception.status).to eql 406
expect(exception.message).to include('API version not found')
end
end
end
context 'when there are multiple versions with complex vendor specified with rescue_from :all' do
subject do
Class.new(Grape::API) do
rescue_from :all
end
end
let(:v1_app) do
Class.new(Grape::API) do
version 'v1', using: :header, vendor: 'test.a-cool_resource', cascade: false, strict: true
content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json'
formatter :v1_test, ->(object, _) { object }
format :v1_test
resources :users do
get :hello do
'one'
end
end
end
end
let(:v2_app) do
Class.new(Grape::API) do
version 'v2', using: :header, vendor: 'test.a-cool_resource', strict: true
content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json'
formatter :v2_test, ->(object, _) { object }
format :v2_test
resources :users do
get :hello do
'two'
end
end
end
end
def app
subject.mount v2_app
subject.mount v1_app
subject
end
context 'with header versioned endpoints and a rescue_all block defined' do
it 'responds correctly to a v1 request' do
versioned_get '/users/hello', 'v1', using: :header, vendor: 'test.a-cool_resource'
expect(last_response.body).to eq('one')
expect(last_response.body).not_to include('API vendor or version not found')
end
it 'responds correctly to a v2 request' do
versioned_get '/users/hello', 'v2', using: :header, vendor: 'test.a-cool_resource'
expect(last_response.body).to eq('two')
expect(last_response.body).not_to include('API vendor or version not found')
end
end
end
end
grape-1.0.2/spec/grape/middleware/versioner/path_spec.rb 0000644 0000041 0000041 00000003640 13231337007 023316 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Versioner::Path do
let(:app) { ->(env) { [200, env, env['api.version']] } }
let(:options) { {} }
subject { Grape::Middleware::Versioner::Path.new(app, options) }
it 'sets the API version based on the first path' do
expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1')
end
it 'does not cut the version out of the path' do
expect(subject.call('PATH_INFO' => '/v1/awesome')[1]['PATH_INFO']).to eq('/v1/awesome')
end
it 'provides a nil version if no path is given' do
expect(subject.call('PATH_INFO' => '/').last).to be_nil
end
context 'with a pattern' do
let(:options) { { pattern: /v./i } }
it 'sets the version if it matches' do
expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1')
end
it 'ignores the version if it fails to match' do
expect(subject.call('PATH_INFO' => '/awesome/radical').last).to be_nil
end
end
[%w[v1 v2], %i[v1 v2], [:v1, 'v2'], ['v1', :v2]].each do |versions|
context "with specified versions as #{versions}" do
let(:options) { { versions: versions } }
it 'throws an error if a non-allowed version is specified' do
expect(catch(:error) { subject.call('PATH_INFO' => '/v3/awesome') }[:status]).to eq(404)
end
it 'allows versions that have been specified' do
expect(subject.call('PATH_INFO' => '/v1/asoasd').last).to eq('v1')
end
end
end
context 'with prefix, but requested version is not matched' do
let(:options) { { prefix: '/v1', pattern: /v./i } }
it 'recognizes potential version' do
expect(subject.call('PATH_INFO' => '/v3/foo').last).to eq('v3')
end
end
context 'with mount path' do
let(:options) { { mount_path: '/mounted', versions: [:v1] } }
it 'recognizes potential version' do
expect(subject.call('PATH_INFO' => '/mounted/v1/foo').last).to eq('v1')
end
end
end
grape-1.0.2/spec/grape/middleware/versioner/accept_version_header_spec.rb 0000644 0000041 0000041 00000006304 13231337007 026676 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Versioner::AcceptVersionHeader do
let(:app) { ->(env) { [200, env, env] } }
subject { Grape::Middleware::Versioner::AcceptVersionHeader.new(app, @options || {}) }
before do
@options = {
version_options: {
using: :accept_version_header
}
}
end
context 'api.version' do
before do
@options[:versions] = ['v1']
end
it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
expect(env['api.version']).to eql 'v1'
expect(status).to eq(200)
end
it 'is set if format provided' do
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
expect(env['api.version']).to eql 'v1'
expect(status).to eq(200)
end
it 'fails with 406 Not Acceptable if version is not supported' do
expect do
subject.call('HTTP_ACCEPT_VERSION' => 'v2').last
end.to throw_symbol(
:error,
status: 406,
headers: { 'X-Cascade' => 'pass' },
message: 'The requested version is not supported.'
)
end
end
it 'succeeds if :strict is not set' do
expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200)
expect(subject.call({}).first).to eq(200)
end
it 'succeeds if :strict is set to false' do
@options[:version_options][:strict] = false
expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200)
expect(subject.call({}).first).to eq(200)
end
context 'when :strict is set' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
end
it 'fails with 406 Not Acceptable if header is not set' do
expect do
subject.call({}).last
end.to throw_symbol(
:error,
status: 406,
headers: { 'X-Cascade' => 'pass' },
message: 'Accept-Version header must be set.'
)
end
it 'fails with 406 Not Acceptable if header is empty' do
expect do
subject.call('HTTP_ACCEPT_VERSION' => '').last
end.to throw_symbol(
:error,
status: 406,
headers: { 'X-Cascade' => 'pass' },
message: 'Accept-Version header must be set.'
)
end
it 'succeeds if proper header is set' do
expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200)
end
end
context 'when :strict and cascade: false' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
@options[:version_options][:cascade] = false
end
it 'fails with 406 Not Acceptable if header is not set' do
expect do
subject.call({}).last
end.to throw_symbol(
:error,
status: 406,
headers: {},
message: 'Accept-Version header must be set.'
)
end
it 'fails with 406 Not Acceptable if header is empty' do
expect do
subject.call('HTTP_ACCEPT_VERSION' => '').last
end.to throw_symbol(
:error,
status: 406,
headers: {},
message: 'Accept-Version header must be set.'
)
end
it 'succeeds if proper header is set' do
expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200)
end
end
end
grape-1.0.2/spec/grape/middleware/versioner/param_spec.rb 0000644 0000041 0000041 00000012274 13231337007 023465 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Versioner::Param do
let(:app) { ->(env) { [200, env, env['api.version']] } }
let(:options) { {} }
subject { Grape::Middleware::Versioner::Param.new(app, options) }
it 'sets the API version based on the default param (apiver)' do
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' })
expect(subject.call(env)[1]['api.version']).to eq('v1')
end
it 'cuts (only) the version out of the params' do
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1', 'other_param' => '5' })
env['rack.request.query_hash'] = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
expect(subject.call(env)[1]['rack.request.query_hash']['apiver']).to be_nil
expect(subject.call(env)[1]['rack.request.query_hash']['other_param']).to eq('5')
end
it 'provides a nil version if no version is given' do
env = Rack::MockRequest.env_for('/')
expect(subject.call(env).last).to be_nil
end
context 'with specified parameter name' do
let(:options) { { version_options: { parameter: 'v' } } }
it 'sets the API version based on the custom parameter name' do
env = Rack::MockRequest.env_for('/awesome', params: { 'v' => 'v1' })
expect(subject.call(env)[1]['api.version']).to eq('v1')
end
it 'does not set the API version based on the default param' do
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' })
expect(subject.call(env)[1]['api.version']).to be_nil
end
end
context 'with specified versions' do
let(:options) { { versions: %w[v1 v2] } }
it 'throws an error if a non-allowed version is specified' do
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v3' })
expect(catch(:error) { subject.call(env) }[:status]).to eq(404)
end
it 'allows versions that have been specified' do
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' })
expect(subject.call(env)[1]['api.version']).to eq('v1')
end
end
context 'when no version is set' do
let(:options) do
{
versions: ['v1'],
version_options: { using: :header }
}
end
it 'returns a 200 (matches the first version found)' do
env = Rack::MockRequest.env_for('/awesome', params: {})
expect(subject.call(env).first).to eq(200)
end
end
context 'when there are multiple versions without a custom param' do
subject { Class.new(Grape::API) }
let(:v1_app) do
Class.new(Grape::API) do
version 'v1', using: :param
content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json'
formatter :v1_test, ->(object, _) { object }
format :v1_test
resources :users do
get :hello do
'one'
end
end
end
end
let(:v2_app) do
Class.new(Grape::API) do
version 'v2', using: :param
content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json'
formatter :v2_test, ->(object, _) { object }
format :v2_test
resources :users do
get :hello do
'two'
end
end
end
end
def app
subject.mount v2_app
subject.mount v1_app
subject
end
it 'responds correctly to a v1 request' do
versioned_get '/users/hello', 'v1', using: :param, parameter: :apiver
expect(last_response.body).to eq('one')
expect(last_response.body).not_to include('API vendor or version not found')
end
it 'responds correctly to a v2 request' do
versioned_get '/users/hello', 'v2', using: :param, parameter: :apiver
expect(last_response.body).to eq('two')
expect(last_response.body).not_to include('API vendor or version not found')
end
end
context 'when there are multiple versions with a custom param' do
subject { Class.new(Grape::API) }
let(:v1_app) do
Class.new(Grape::API) do
version 'v1', using: :param, parameter: 'v'
content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json'
formatter :v1_test, ->(object, _) { object }
format :v1_test
resources :users do
get :hello do
'one'
end
end
end
end
let(:v2_app) do
Class.new(Grape::API) do
version 'v2', using: :param, parameter: 'v'
content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json'
formatter :v2_test, ->(object, _) { object }
format :v2_test
resources :users do
get :hello do
'two'
end
end
end
end
def app
subject.mount v2_app
subject.mount v1_app
subject
end
it 'responds correctly to a v1 request' do
versioned_get '/users/hello', 'v1', using: :param, parameter: 'v'
expect(last_response.body).to eq('one')
expect(last_response.body).not_to include('API vendor or version not found')
end
it 'responds correctly to a v2 request' do
versioned_get '/users/hello', 'v2', using: :param, parameter: 'v'
expect(last_response.body).to eq('two')
expect(last_response.body).not_to include('API vendor or version not found')
end
end
end
grape-1.0.2/spec/grape/middleware/auth/ 0000755 0000041 0000041 00000000000 13231337007 017745 5 ustar www-data www-data grape-1.0.2/spec/grape/middleware/auth/strategies_spec.rb 0000644 0000041 0000041 00000004057 13231337007 023464 0 ustar www-data www-data require 'spec_helper'
require 'base64'
describe Grape::Middleware::Auth::Strategies do
context 'Basic Auth' do
def app
proc = ->(u, p) { u && p && u == p }
Rack::Builder.new do |b|
b.use Grape::Middleware::Error
b.use(Grape::Middleware::Auth::Base, type: :http_basic, proc: proc)
b.run ->(_env) { [200, {}, ['Hello there.']] }
end
end
it 'throws a 401 if no auth is given' do
@proc = -> { false }
get '/whatever'
expect(last_response.status).to eq(401)
end
it 'authenticates if given valid creds' do
get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin')
expect(last_response.status).to eq(200)
end
it 'throws a 401 is wrong auth is given' do
get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong')
expect(last_response.status).to eq(401)
end
end
context 'Digest MD5 Auth' do
RSpec::Matchers.define :be_challenge do
match do |actual_response|
actual_response.status == 401 &&
actual_response['WWW-Authenticate'] =~ /^Digest / &&
actual_response.body.empty?
end
end
module StrategiesSpec
class Test < Grape::API
http_digest(realm: 'Test Api', opaque: 'secret') do |username|
{ 'foo' => 'bar' }[username]
end
get '/test' do
[{ hey: 'you' }, { there: 'bar' }, { foo: 'baz' }]
end
end
end
def app
StrategiesSpec::Test
end
it 'is a digest authentication challenge' do
get '/test'
expect(last_response).to be_challenge
end
it 'throws a 401 if no auth is given' do
get '/test'
expect(last_response.status).to eq(401)
end
it 'authenticates if given valid creds' do
digest_authorize 'foo', 'bar'
get '/test'
expect(last_response.status).to eq(200)
end
it 'throws a 401 if given invalid creds' do
digest_authorize 'bar', 'foo'
get '/test'
expect(last_response.status).to eq(401)
end
end
end
grape-1.0.2/spec/grape/middleware/auth/dsl_spec.rb 0000644 0000041 0000041 00000003241 13231337007 022066 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Auth::DSL do
subject { Class.new(Grape::API) }
let(:block) { ->() {} }
let(:settings) do
{
opaque: 'secret',
proc: block,
realm: 'API Authorization',
type: :http_digest
}
end
describe '.auth' do
it 'stets auth parameters' do
expect(subject).to receive(:use).with(Grape::Middleware::Auth::Base, settings)
subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc]
expect(subject.auth).to eq(settings)
end
it 'can be called multiple times' do
expect(subject).to receive(:use).with(Grape::Middleware::Auth::Base, settings)
expect(subject).to receive(:use).with(Grape::Middleware::Auth::Base, settings.merge(realm: 'super_secret'))
subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc]
first_settings = subject.auth
subject.auth :http_digest, realm: 'super_secret', opaque: settings[:opaque], &settings[:proc]
expect(subject.auth).to eq(settings.merge(realm: 'super_secret'))
expect(subject.auth.object_id).not_to eq(first_settings.object_id)
end
end
describe '.http_basic' do
it 'stets auth parameters' do
subject.http_basic realm: 'my_realm', &settings[:proc]
expect(subject.auth).to eq(realm: 'my_realm', type: :http_basic, proc: block)
end
end
describe '.http_digest' do
it 'stets auth parameters' do
subject.http_digest realm: 'my_realm', opaque: 'my_opaque', &settings[:proc]
expect(subject.auth).to eq(realm: 'my_realm', type: :http_digest, proc: block, opaque: 'my_opaque')
end
end
end
grape-1.0.2/spec/grape/middleware/auth/base_spec.rb 0000644 0000041 0000041 00000001351 13231337007 022216 0 ustar www-data www-data require 'spec_helper'
require 'base64'
describe Grape::Middleware::Auth::Base do
subject do
Class.new(Grape::API) do
http_basic realm: 'my_realm' do |user, password|
user && password && user == password
end
get '/authorized' do
'DONE'
end
end
end
def app
subject
end
it 'authenticates if given valid creds' do
get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin')
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('DONE')
end
it 'throws a 401 is wrong auth is given' do
get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong')
expect(last_response.status).to eq(401)
end
end
grape-1.0.2/spec/grape/middleware/error_spec.rb 0000644 0000041 0000041 00000003405 13231337007 021476 0 ustar www-data www-data require 'spec_helper'
require 'grape-entity'
describe Grape::Middleware::Error do
module ErrorSpec
class ErrorEntity < Grape::Entity
expose :code
expose :static
def static
'static text'
end
end
class ErrApp
class << self
attr_accessor :error
attr_accessor :format
def call(_env)
throw :error, error
end
end
end
end
def app
opts = options
Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, opts
run ErrorSpec::ErrApp
end
end
let(:options) { { default_message: 'Aww, hamburgers.' } }
it 'sets the status code appropriately' do
ErrorSpec::ErrApp.error = { status: 410 }
get '/'
expect(last_response.status).to eq(410)
end
it 'sets the error message appropriately' do
ErrorSpec::ErrApp.error = { message: 'Awesome stuff.' }
get '/'
expect(last_response.body).to eq('Awesome stuff.')
end
it 'defaults to a 500 status' do
ErrorSpec::ErrApp.error = {}
get '/'
expect(last_response.status).to eq(500)
end
it 'has a default message' do
ErrorSpec::ErrApp.error = {}
get '/'
expect(last_response.body).to eq('Aww, hamburgers.')
end
context 'with http code' do
let(:options) { { default_message: 'Aww, hamburgers.' } }
it 'adds the status code if wanted' do
ErrorSpec::ErrApp.error = { message: { code: 200 } }
get '/'
expect(last_response.body).to eq({ code: 200 }.to_json)
end
it 'presents an error message' do
ErrorSpec::ErrApp.error = { message: { code: 200, with: ErrorSpec::ErrorEntity } }
get '/'
expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json)
end
end
end
grape-1.0.2/spec/grape/middleware/globals_spec.rb 0000644 0000041 0000041 00000001602 13231337007 021765 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Globals do
subject { Grape::Middleware::Globals.new(blank_app) }
before { allow(subject).to receive(:dup).and_return(subject) }
let(:blank_app) { ->(_env) { [200, {}, 'Hi there.'] } }
it 'calls through to the app' do
expect(subject.call({})).to eq([200, {}, 'Hi there.'])
end
context 'environment' do
it 'should set the grape.request environment' do
subject.call({})
expect(subject.env['grape.request']).to be_a(Grape::Request)
end
it 'should set the grape.request.headers environment' do
subject.call({})
expect(subject.env['grape.request.headers']).to be_a(Hash)
end
it 'should set the grape.request.params environment' do
subject.call('QUERY_STRING' => 'test=1', 'rack.input' => StringIO.new)
expect(subject.env['grape.request.params']).to be_a(Hash)
end
end
end
grape-1.0.2/spec/grape/middleware/stack_spec.rb 0000644 0000041 0000041 00000011771 13231337007 021457 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Stack do
module StackSpec
class FooMiddleware; end
class BarMiddleware; end
class BlockMiddleware
attr_reader :block
def initialize(&block)
@block = block
end
end
end
let(:proc) { ->() {} }
let(:others) { [[:use, StackSpec::BarMiddleware], [:insert_before, StackSpec::BarMiddleware, StackSpec::BlockMiddleware, proc]] }
subject { Grape::Middleware::Stack.new }
before do
subject.use StackSpec::FooMiddleware
end
describe '#use' do
it 'pushes a middleware class onto the stack' do
expect { subject.use StackSpec::BarMiddleware }
.to change { subject.size }.by(1)
expect(subject.last).to eq(StackSpec::BarMiddleware)
end
it 'pushes a middleware class with arguments onto the stack' do
expect { subject.use StackSpec::BarMiddleware, false, my_arg: 42 }
.to change { subject.size }.by(1)
expect(subject.last).to eq(StackSpec::BarMiddleware)
expect(subject.last.args).to eq([false, { my_arg: 42 }])
end
it 'pushes a middleware class with block arguments onto the stack' do
expect { subject.use StackSpec::BlockMiddleware, &proc }
.to change { subject.size }.by(1)
expect(subject.last).to eq(StackSpec::BlockMiddleware)
expect(subject.last.args).to eq([])
expect(subject.last.block).to eq(proc)
end
end
describe '#insert' do
it 'inserts a middleware class at the integer index' do
expect { subject.insert 0, StackSpec::BarMiddleware }
.to change { subject.size }.by(1)
expect(subject[0]).to eq(StackSpec::BarMiddleware)
expect(subject[1]).to eq(StackSpec::FooMiddleware)
end
end
describe '#insert_before' do
it 'inserts a middleware before another middleware class' do
expect { subject.insert_before StackSpec::FooMiddleware, StackSpec::BarMiddleware }
.to change { subject.size }.by(1)
expect(subject[0]).to eq(StackSpec::BarMiddleware)
expect(subject[1]).to eq(StackSpec::FooMiddleware)
end
it 'inserts a middleware before an anonymous class given by its superclass' do
subject.use Class.new(StackSpec::BlockMiddleware)
expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware }
.to change { subject.size }.by(1)
expect(subject[1]).to eq(StackSpec::BarMiddleware)
expect(subject[2]).to eq(StackSpec::BlockMiddleware)
end
it 'raises an error on an invalid index' do
expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware }
.to raise_error(RuntimeError, 'No such middleware to insert before: StackSpec::BlockMiddleware')
end
end
describe '#insert_after' do
it 'inserts a middleware after another middleware class' do
expect { subject.insert_after StackSpec::FooMiddleware, StackSpec::BarMiddleware }
.to change { subject.size }.by(1)
expect(subject[1]).to eq(StackSpec::BarMiddleware)
expect(subject[0]).to eq(StackSpec::FooMiddleware)
end
it 'inserts a middleware after an anonymous class given by its superclass' do
subject.use Class.new(StackSpec::BlockMiddleware)
expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware }
.to change { subject.size }.by(1)
expect(subject[1]).to eq(StackSpec::BlockMiddleware)
expect(subject[2]).to eq(StackSpec::BarMiddleware)
end
it 'raises an error on an invalid index' do
expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware }
.to raise_error(RuntimeError, 'No such middleware to insert after: StackSpec::BlockMiddleware')
end
end
describe '#merge_with' do
it 'applies a collection of operations and middlewares' do
expect { subject.merge_with(others) }
.to change { subject.size }.by(2)
expect(subject[0]).to eq(StackSpec::FooMiddleware)
expect(subject[1]).to eq(StackSpec::BlockMiddleware)
expect(subject[2]).to eq(StackSpec::BarMiddleware)
end
end
describe '#build' do
it 'returns a rack builder instance' do
expect(subject.build).to be_instance_of(Rack::Builder)
end
context 'when @others are present' do
let(:others) { [[:insert_after, Grape::Middleware::Formatter, StackSpec::BarMiddleware]] }
it 'applies the middleware specs stored in @others' do
subject.concat others
subject.use Grape::Middleware::Formatter
subject.build
expect(subject[0]).to eq StackSpec::FooMiddleware
expect(subject[1]).to eq Grape::Middleware::Formatter
expect(subject[2]).to eq StackSpec::BarMiddleware
end
end
end
describe '#concat' do
it 'adds non :use specs to @others' do
expect { subject.concat others }.to change(subject, :others).from([]).to([[others.last]])
end
it 'calls +merge_with+ with the :use specs' do
expect(subject).to receive(:merge_with).with [[:use, StackSpec::BarMiddleware]]
subject.concat others
end
end
end
grape-1.0.2/spec/grape/middleware/versioner_spec.rb 0000644 0000041 0000041 00000001131 13231337007 022353 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Versioner do
let(:klass) { Grape::Middleware::Versioner }
it 'recognizes :path' do
expect(klass.using(:path)).to eq(Grape::Middleware::Versioner::Path)
end
it 'recognizes :header' do
expect(klass.using(:header)).to eq(Grape::Middleware::Versioner::Header)
end
it 'recognizes :param' do
expect(klass.using(:param)).to eq(Grape::Middleware::Versioner::Param)
end
it 'recognizes :accept_version_header' do
expect(klass.using(:accept_version_header)).to eq(Grape::Middleware::Versioner::AcceptVersionHeader)
end
end
grape-1.0.2/spec/grape/middleware/base_spec.rb 0000644 0000041 0000041 00000011426 13231337007 021261 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Base do
subject { Grape::Middleware::Base.new(blank_app) }
let(:blank_app) { ->(_) { [200, {}, 'Hi there.'] } }
before do
# Keep it one object for testing.
allow(subject).to receive(:dup).and_return(subject)
end
it 'has the app as an accessor' do
expect(subject.app).to eq(blank_app)
end
it 'calls through to the app' do
expect(subject.call({})).to eq([200, {}, 'Hi there.'])
end
context 'callbacks' do
it 'calls #before' do
expect(subject).to receive(:before)
end
it 'calls #after' do
expect(subject).to receive(:after)
end
after { subject.call!({}) }
end
context 'callbacks on error' do
let(:blank_app) { ->(_) { raise StandardError } }
it 'calls #after' do
expect(subject).to receive(:after)
expect { subject.call({}) }.to raise_error(StandardError)
end
end
context 'after callback' do
before do
allow(subject).to receive(:after).and_return([200, {}, 'Hello from after callback'])
end
it 'overwrites application response' do
expect(subject.call!({}).last).to eq('Hello from after callback')
end
end
context 'after callback with errors' do
it 'does not overwrite the application response' do
expect(subject.call({})).to eq([200, {}, 'Hi there.'])
end
context 'with patched warnings' do
before do
@warnings = warnings = []
allow_any_instance_of(Grape::Middleware::Base).to receive(:warn) { |m| warnings << m }
allow(subject).to receive(:after).and_raise(StandardError)
end
it 'does show a warning' do
expect { subject.call({}) }.to raise_error(StandardError)
expect(@warnings).not_to be_empty
end
end
end
it 'is able to access the response' do
subject.call({})
expect(subject.response).to be_kind_of(Rack::Response)
end
describe '#response' do
subject { Grape::Middleware::Base.new(response) }
context Array do
let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } }
it 'status' do
subject.call({})
expect(subject.response.status).to eq(204)
end
it 'body' do
subject.call({})
expect(subject.response.body).to eq(['test'])
end
it 'header' do
subject.call({})
expect(subject.response.header).to have_key(:abc)
end
end
context Rack::Response do
let(:response) { ->(_) { Rack::Response.new('test', 204, abc: 1) } }
it 'status' do
subject.call({})
expect(subject.response.status).to eq(204)
end
it 'body' do
subject.call({})
expect(subject.response.body).to eq(['test'])
end
it 'header' do
subject.call({})
expect(subject.response.header).to have_key(:abc)
end
end
end
context 'options' do
it 'persists options passed at initialization' do
expect(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true
end
context 'defaults' do
module BaseSpec
class ExampleWare < Grape::Middleware::Base
def default_options
{ monkey: true }
end
end
end
it 'persists the default options' do
expect(BaseSpec::ExampleWare.new(blank_app).options[:monkey]).to be true
end
it 'overrides default options when provided' do
expect(BaseSpec::ExampleWare.new(blank_app, monkey: false).options[:monkey]).to be false
end
end
end
context 'header' do
module HeaderSpec
class ExampleWare < Grape::Middleware::Base
def before
header 'X-Test-Before', 'Hi'
end
def after
header 'X-Test-After', 'Bye'
nil
end
end
end
def app
Rack::Builder.app do
use HeaderSpec::ExampleWare
run ->(_) { [200, {}, ['Yeah']] }
end
end
it 'is able to set a header' do
get '/'
expect(last_response.headers['X-Test-Before']).to eq('Hi')
expect(last_response.headers['X-Test-After']).to eq('Bye')
end
end
context 'header overwrite' do
module HeaderOverwritingSpec
class ExampleWare < Grape::Middleware::Base
def before
header 'X-Test-Overwriting', 'Hi'
end
def after
header 'X-Test-Overwriting', 'Bye'
nil
end
end
class API < Grape::API
get('/') do
header 'X-Test-Overwriting', 'Yeah'
'Hello'
end
end
end
def app
Rack::Builder.app do
use HeaderOverwritingSpec::ExampleWare
run HeaderOverwritingSpec::API.new
end
end
it 'overwrites header by after headers' do
get '/'
expect(last_response.headers['X-Test-Overwriting']).to eq('Bye')
end
end
end
grape-1.0.2/spec/grape/middleware/formatter_spec.rb 0000644 0000041 0000041 00000031760 13231337007 022355 0 ustar www-data www-data require 'spec_helper'
describe Grape::Middleware::Formatter do
subject { Grape::Middleware::Formatter.new(app) }
before { allow(subject).to receive(:dup).and_return(subject) }
let(:body) { { 'foo' => 'bar' } }
let(:app) { ->(_env) { [200, {}, [body]] } }
context 'serialization' do
let(:body) { { 'abc' => 'def' } }
it 'looks at the bodies for possibly serializable data' do
_, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json')
bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) }
end
context 'default format' do
let(:body) { ['foo'] }
it 'calls #to_json since default format is json' do
body.instance_eval do
def to_json
'"bar"'
end
end
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') }
end
end
context 'jsonapi' do
let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } }
it 'calls #to_json if the content type is jsonapi' do
body.instance_eval do
def to_json
'{"foos":[{"bar":"baz"}] }'
end
end
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') }
end
end
context 'xml' do
let(:body) { 'string' }
it 'calls #to_xml if the content type is xml' do
body.instance_eval do
def to_xml
''
end
end
subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('') }
end
end
end
context 'error handling' do
let(:formatter) { double(:formatter) }
before do
allow(Grape::Formatter).to receive(:formatter_for) { formatter }
end
it 'rescues formatter-specific exceptions' do
allow(formatter).to receive(:call) { raise Grape::Exceptions::InvalidFormatter.new(String, 'xml') }
expect do
catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') }
end.to_not raise_error
end
it 'does not rescue other exceptions' do
allow(formatter).to receive(:call) { raise StandardError }
expect do
catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') }
end.to raise_error(StandardError)
end
end
context 'detection' do
it 'uses the xml extension if one is provided' do
subject.call('PATH_INFO' => '/info.xml')
expect(subject.env['api.format']).to eq(:xml)
end
it 'uses the json extension if one is provided' do
subject.call('PATH_INFO' => '/info.json')
expect(subject.env['api.format']).to eq(:json)
end
it 'uses the format parameter if one is provided' do
subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=json')
expect(subject.env['api.format']).to eq(:json)
subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=xml')
expect(subject.env['api.format']).to eq(:xml)
end
it 'uses the default format if none is provided' do
subject.call('PATH_INFO' => '/info')
expect(subject.env['api.format']).to eq(:txt)
end
it 'uses the requested format if provided in headers' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json')
expect(subject.env['api.format']).to eq(:json)
end
it 'uses the file extension format if provided before headers' do
subject.call('PATH_INFO' => '/info.txt', 'HTTP_ACCEPT' => 'application/json')
expect(subject.env['api.format']).to eq(:txt)
end
end
context 'accept header detection' do
it 'detects from the Accept header' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml')
expect(subject.env['api.format']).to eq(:xml)
end
it 'uses quality rankings to determine formats' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0')
expect(subject.env['api.format']).to eq(:xml)
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3')
expect(subject.env['api.format']).to eq(:json)
end
it 'handles quality rankings mixed with nothing' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0')
expect(subject.env['api.format']).to eq(:xml)
end
it 'parses headers with other attributes' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7')
expect(subject.env['api.format']).to eq(:json)
end
it 'parses headers with vendor and api version' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test-v1+xml')
expect(subject.env['api.format']).to eq(:xml)
end
context 'with custom vendored content types' do
before do
subject.options[:content_types] = {}
subject.options[:content_types][:custom] = 'application/vnd.test+json'
end
it 'it uses the custom type' do
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json')
expect(subject.env['api.format']).to eq(:custom)
end
end
it 'parses headers with symbols as hash keys' do
subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml', system_time: '091293')
expect(subject.env[:system_time]).to eq('091293')
end
end
context 'content-type' do
it 'is set for json' do
_, headers, = subject.call('PATH_INFO' => '/info.json')
expect(headers['Content-type']).to eq('application/json')
end
it 'is set for xml' do
_, headers, = subject.call('PATH_INFO' => '/info.xml')
expect(headers['Content-type']).to eq('application/xml')
end
it 'is set for txt' do
_, headers, = subject.call('PATH_INFO' => '/info.txt')
expect(headers['Content-type']).to eq('text/plain')
end
it 'is set for custom' do
subject.options[:content_types] = {}
subject.options[:content_types][:custom] = 'application/x-custom'
_, headers, = subject.call('PATH_INFO' => '/info.custom')
expect(headers['Content-type']).to eq('application/x-custom')
end
it 'is set for vendored with registered type' do
subject.options[:content_types] = {}
subject.options[:content_types][:custom] = 'application/vnd.test+json'
_, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json')
expect(headers['Content-type']).to eq('application/vnd.test+json')
end
it 'is set to closest generic for custom vendored/versioned without registered type' do
_, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json')
expect(headers['Content-type']).to eq('application/json')
end
end
context 'format' do
it 'uses custom formatter' do
subject.options[:content_types] = {}
subject.options[:content_types][:custom] = "don't care"
subject.options[:formatters][:custom] = ->(_obj, _env) { 'CUSTOM FORMAT' }
_, _, body = subject.call('PATH_INFO' => '/info.custom')
expect(body.body).to eq(['CUSTOM FORMAT'])
end
context 'default' do
let(:body) { ['blah'] }
it 'uses default json formatter' do
_, _, body = subject.call('PATH_INFO' => '/info.json')
expect(body.body).to eq(['["blah"]'])
end
end
it 'uses custom json formatter' do
subject.options[:formatters][:json] = ->(_obj, _env) { 'CUSTOM JSON FORMAT' }
_, _, body = subject.call('PATH_INFO' => '/info.json')
expect(body.body).to eq(['CUSTOM JSON FORMAT'])
end
end
context 'no content responses' do
let(:no_content_response) { ->(status) { [status, {}, ['']] } }
Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.each do |status|
it "does not modify a #{status} response" do
expected_response = no_content_response[status]
allow(app).to receive(:call).and_return(expected_response)
expect(subject.call({})).to eq(expected_response)
end
end
end
context 'input' do
%w[POST PATCH PUT DELETE].each do |method|
['application/json', 'application/json; charset=utf-8'].each do |content_type|
context content_type do
it "parses the body from #{method} and copies values into rack.request.form_hash" do
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
subject.call(
'PATH_INFO' => '/info',
'REQUEST_METHOD' => method,
'CONTENT_TYPE' => content_type,
'rack.input' => io,
'CONTENT_LENGTH' => io.length
)
expect(subject.env['rack.request.form_hash']['is_boolean']).to be true
expect(subject.env['rack.request.form_hash']['string']).to eq('thing')
end
end
end
it "parses the chunked body from #{method} and copies values into rack.request.from_hash" do
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
subject.call(
'PATH_INFO' => '/infol',
'REQUEST_METHOD' => method,
'CONTENT_TYPE' => 'application/json',
'rack.input' => io,
'HTTP_TRANSFER_ENCODING' => 'chunked'
)
expect(subject.env['rack.request.form_hash']['is_boolean']).to be true
expect(subject.env['rack.request.form_hash']['string']).to eq('thing')
end
it 'rewinds IO' do
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
io.read
subject.call(
'PATH_INFO' => '/infol',
'REQUEST_METHOD' => method,
'CONTENT_TYPE' => 'application/json',
'rack.input' => io,
'HTTP_TRANSFER_ENCODING' => 'chunked'
)
expect(subject.env['rack.request.form_hash']['is_boolean']).to be true
expect(subject.env['rack.request.form_hash']['string']).to eq('thing')
end
it "parses the body from an xml #{method} and copies values into rack.request.from_hash" do
io = StringIO.new('Test')
subject.call(
'PATH_INFO' => '/info.xml',
'REQUEST_METHOD' => method,
'CONTENT_TYPE' => 'application/xml',
'rack.input' => io,
'CONTENT_LENGTH' => io.length
)
if Object.const_defined? :MultiXml
expect(subject.env['rack.request.form_hash']['thing']['name']).to eq('Test')
else
expect(subject.env['rack.request.form_hash']['thing']['name']['__content__']).to eq('Test')
end
end
[Rack::Request::FORM_DATA_MEDIA_TYPES, Rack::Request::PARSEABLE_DATA_MEDIA_TYPES].flatten.each do |content_type|
it "ignores #{content_type}" do
io = StringIO.new('name=Other+Test+Thing')
subject.call(
'PATH_INFO' => '/info',
'REQUEST_METHOD' => method,
'CONTENT_TYPE' => content_type,
'rack.input' => io,
'CONTENT_LENGTH' => io.length
)
expect(subject.env['rack.request.form_hash']).to be_nil
end
end
end
end
context 'send file' do
let(:body) { Grape::ServeFile::FileResponse.new('file') }
let(:app) { ->(_env) { [200, {}, body] } }
it 'returns Grape::Uril::SendFileReponse' do
env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' }
expect(subject.call(env)).to be_a(Grape::ServeFile::SendfileResponse)
end
end
context 'inheritable formatters' do
class InvalidFormatter
def self.call(_, _)
{ message: 'invalid' }.to_json
end
end
let(:app) { ->(_env) { [200, {}, ['']] } }
before do
Grape::Formatter.register :invalid, InvalidFormatter
Grape::ContentTypes::CONTENT_TYPES[:invalid] = 'application/x-invalid'
end
it 'returns response by invalid formatter' do
env = { 'PATH_INFO' => '/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid' }
_, _, bodies = *subject.call(env)
expect(bodies.body.first).to eq({ message: 'invalid' }.to_json)
end
end
context 'custom parser raises exception and rescue options are enabled for backtrace and original_exception' do
it 'adds the backtrace and original_exception to the error output' do
subject = Grape::Middleware::Formatter.new(
app,
rescue_options: { backtrace: true, original_exception: true },
parsers: { json: ->(_object, _env) { raise StandardError, 'fail' } }
)
io = StringIO.new('{invalid}')
error = catch(:error) {
subject.call(
'PATH_INFO' => '/info',
'REQUEST_METHOD' => 'POST',
'CONTENT_TYPE' => 'application/json',
'rack.input' => io,
'CONTENT_LENGTH' => io.length
)
}
expect(error[:message]).to eq 'fail'
expect(error[:backtrace].size).to be >= 1
expect(error[:original_exception].class).to eq StandardError
end
end
end
grape-1.0.2/spec/grape/exceptions/ 0000755 0000041 0000041 00000000000 13231337007 017050 5 ustar www-data www-data grape-1.0.2/spec/grape/exceptions/invalid_accept_header_spec.rb 0000644 0000041 0000041 00000024627 13231337007 024677 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::InvalidAcceptHeader do
shared_examples_for 'a valid request' do
it 'does return with status 200' do
expect(last_response.status).to eq 200
end
it 'does return the expected result' do
expect(last_response.body).to eq('beer received')
end
end
shared_examples_for 'a cascaded request' do
it 'does not find a matching route' do
expect(last_response.status).to eq 404
end
end
shared_examples_for 'a not-cascaded request' do
it 'does not include the X-Cascade=pass header' do
expect(last_response.headers['X-Cascade']).to be_nil
end
it 'does not accept the request' do
expect(last_response.status).to eq 406
end
end
shared_examples_for 'a rescued request' do
it 'does not include the X-Cascade=pass header' do
expect(last_response.headers['X-Cascade']).to be_nil
end
it 'does show rescue handler processing' do
expect(last_response.status).to eq 400
expect(last_response.body).to eq('message was processed')
end
end
context 'API with cascade=false and rescue_from :all handler' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false
subject.rescue_from :all do |e|
rack_response 'message was processed', 400, e[:headers]
end
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid vendor in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a rescued request'
end
end
end
context 'API with cascade=false and without a rescue handler' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid version in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' }
it_should_behave_like 'a not-cascaded request'
end
context 'an invalid vendor in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' }
it_should_behave_like 'a not-cascaded request'
end
end
end
context 'API with cascade=false and with rescue_from :all handler and http_codes' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false
subject.rescue_from :all do |e|
rack_response 'message was processed', 400, e[:headers]
end
subject.desc 'Get beer' do
failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'],
[404, 'Resource not found'], [406, 'API vendor or version not found'],
[500, 'Internal processing error']]
end
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid vendor in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a rescued request'
end
end
end
context 'API with cascade=false, http_codes but without a rescue handler' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false
subject.desc 'Get beer' do
failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'],
[404, 'Resource not found'], [406, 'API vendor or version not found'],
[500, 'Internal processing error']]
end
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid version in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' }
it_should_behave_like 'a not-cascaded request'
end
context 'an invalid vendor in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' }
it_should_behave_like 'a not-cascaded request'
end
end
end
context 'API with cascade=true and rescue_from :all handler' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true
subject.rescue_from :all do |e|
rack_response 'message was processed', 400, e[:headers]
end
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid version in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a cascaded request'
end
context 'an invalid vendor in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a cascaded request'
end
end
end
context 'API with cascade=true and without a rescue handler' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid version in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' }
it_should_behave_like 'a cascaded request'
end
context 'an invalid vendor in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' }
it_should_behave_like 'a cascaded request'
end
end
end
context 'API with cascade=true and with rescue_from :all handler and http_codes' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true
subject.rescue_from :all do |e|
rack_response 'message was processed', 400, e[:headers]
end
subject.desc 'Get beer' do
failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'],
[404, 'Resource not found'], [406, 'API vendor or version not found'],
[500, 'Internal processing error']]
end
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid version in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a cascaded request'
end
context 'an invalid vendor in the request' do
before do
get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99',
'CONTENT_TYPE' => 'application/json'
end
it_should_behave_like 'a cascaded request'
end
end
end
context 'API with cascade=true, http_codes but without a rescue handler' do
subject { Class.new(Grape::API) }
before do
subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true
subject.desc 'Get beer' do
failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'],
[404, 'Resource not found'], [406, 'API vendor or version not found'],
[500, 'Internal processing error']]
end
subject.get '/beer' do
'beer received'
end
end
def app
subject
end
context 'that received a request with correct vendor and version' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' }
it_should_behave_like 'a valid request'
end
context 'that receives' do
context 'an invalid version in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' }
it_should_behave_like 'a cascaded request'
end
context 'an invalid vendor in the request' do
before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' }
it_should_behave_like 'a cascaded request'
end
end
end
end
grape-1.0.2/spec/grape/exceptions/validation_spec.rb 0000644 0000041 0000041 00000001205 13231337007 022537 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::Validation do
it 'fails when params are missing' do
expect { Grape::Exceptions::Validation.new(message: 'presence') }.to raise_error(ArgumentError, 'missing keyword: params')
end
context 'when message is a symbol' do
it 'stores message_key' do
expect(Grape::Exceptions::Validation.new(params: ['id'], message: :presence).message_key).to eq(:presence)
end
end
context 'when message is a String' do
it 'does not store the message_key' do
expect(Grape::Exceptions::Validation.new(params: ['id'], message: 'presence').message_key).to eq(nil)
end
end
end
grape-1.0.2/spec/grape/exceptions/invalid_versioner_option_spec.rb 0000644 0000041 0000041 00000000511 13231337007 025516 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::InvalidVersionerOption do
describe '#message' do
let(:error) do
described_class.new('headers')
end
it 'contains the problem in the message' do
expect(error.message).to include(
'Unknown :using for versioner: headers'
)
end
end
end
grape-1.0.2/spec/grape/exceptions/invalid_formatter_spec.rb 0000644 0000041 0000041 00000000476 13231337007 024127 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::InvalidFormatter do
describe '#message' do
let(:error) do
described_class.new(String, 'xml')
end
it 'contains the problem in the message' do
expect(error.message).to include(
'cannot convert String to xml'
)
end
end
end
grape-1.0.2/spec/grape/exceptions/unknown_validator_spec.rb 0000644 0000041 0000041 00000000464 13231337007 024157 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::UnknownValidator do
describe '#message' do
let(:error) do
described_class.new('gt_10')
end
it 'contains the problem in the message' do
expect(error.message).to include(
'unknown validator: gt_10'
)
end
end
end
grape-1.0.2/spec/grape/exceptions/body_parse_errors_spec.rb 0000644 0000041 0000041 00000010220 13231337007 024125 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::ValidationErrors do
context 'api with rescue_from :all handler' do
subject { Class.new(Grape::API) }
before do
subject.rescue_from :all do |_e|
rack_response 'message was processed', 400
end
subject.params do
requires :beer
end
subject.post '/beer' do
'beer received'
end
end
def app
subject
end
context 'with content_type json' do
it 'can recover from failed body parsing' do
post '/beer', 'test', 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq 400
expect(last_response.body).to eq('message was processed')
end
end
context 'with content_type xml' do
it 'can recover from failed body parsing' do
post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.status).to eq 400
expect(last_response.body).to eq('message was processed')
end
end
context 'with content_type text' do
it 'can recover from failed body parsing' do
post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain'
expect(last_response.status).to eq 400
expect(last_response.body).to eq('message was processed')
end
end
context 'with no specific content_type' do
it 'can recover from failed body parsing' do
post '/beer', 'test', {}
expect(last_response.status).to eq 400
expect(last_response.body).to eq('message was processed')
end
end
end
context 'api with rescue_from :grape_exceptions handler' do
subject { Class.new(Grape::API) }
before do
subject.rescue_from :all do |_e|
rack_response 'message was processed', 400
end
subject.rescue_from :grape_exceptions
subject.params do
requires :beer
end
subject.post '/beer' do
'beer received'
end
end
def app
subject
end
context 'with content_type json' do
it 'returns body parsing error message' do
post '/beer', 'test', 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq 400
expect(last_response.body).to include 'message body does not match declared format'
end
end
context 'with content_type xml' do
it 'returns body parsing error message' do
post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.status).to eq 400
expect(last_response.body).to include 'message body does not match declared format'
end
end
end
context 'api without a rescue handler' do
subject { Class.new(Grape::API) }
before do
subject.params do
requires :beer
end
subject.post '/beer' do
'beer received'
end
end
def app
subject
end
context 'and with content_type json' do
it 'can recover from failed body parsing' do
post '/beer', 'test', 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq 400
expect(last_response.body).to include('message body does not match declared format')
expect(last_response.body).to include('application/json')
end
end
context 'with content_type xml' do
it 'can recover from failed body parsing' do
post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.status).to eq 400
expect(last_response.body).to include('message body does not match declared format')
expect(last_response.body).to include('application/xml')
end
end
context 'with content_type text' do
it 'can recover from failed body parsing' do
post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain'
expect(last_response.status).to eq 400
expect(last_response.body).to eq('beer is missing')
end
end
context 'and with no specific content_type' do
it 'can recover from failed body parsing' do
post '/beer', 'test', {}
expect(last_response.status).to eq 400
# plain response with text/html
expect(last_response.body).to eq('beer is missing')
end
end
end
end
grape-1.0.2/spec/grape/exceptions/missing_mime_type_spec.rb 0000644 0000041 0000041 00000000725 13231337007 024134 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::MissingMimeType do
describe '#message' do
let(:error) do
described_class.new('new_json')
end
it 'contains the problem in the message' do
expect(error.message).to include 'missing mime type for new_json'
end
it 'contains the resolution in the message' do
expect(error.message).to include "or add your own with content_type :new_json, 'application/new_json' "
end
end
end
grape-1.0.2/spec/grape/exceptions/unknown_options_spec.rb 0000644 0000041 0000041 00000000453 13231337007 023663 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::UnknownOptions do
describe '#message' do
let(:error) do
described_class.new(%i[a b])
end
it 'contains the problem in the message' do
expect(error.message).to include(
'unknown options: '
)
end
end
end
grape-1.0.2/spec/grape/exceptions/missing_option_spec.rb 0000644 0000041 0000041 00000000466 13231337007 023456 0 ustar www-data www-data require 'spec_helper'
describe Grape::Exceptions::MissingOption do
describe '#message' do
let(:error) do
described_class.new(:path)
end
it 'contains the problem in the message' do
expect(error.message).to include(
'You must specify :path options.'
)
end
end
end
grape-1.0.2/spec/grape/exceptions/validation_errors_spec.rb 0000644 0000041 0000041 00000004452 13231337007 024142 0 ustar www-data www-data require 'spec_helper'
require 'ostruct'
describe Grape::Exceptions::ValidationErrors do
let(:validation_message) { 'FooBar is invalid' }
let(:validation_error) { OpenStruct.new(params: [validation_message]) }
context 'initialize' do
let(:headers) do
{
'A-Header-Key' => 'A-Header-Value'
}
end
subject do
described_class.new(errors: [validation_error], headers: headers)
end
it 'should assign headers through base class' do
expect(subject.headers).to eq(headers)
end
end
context 'message' do
context 'is not repeated' do
let(:error) do
described_class.new(errors: [validation_error, validation_error])
end
subject(:message) { error.message.split(',').map(&:strip) }
it { expect(message).to include validation_message }
it { expect(message.size).to eq 1 }
end
end
describe '#full_messages' do
context 'with errors' do
let(:validation_error_1) { Grape::Exceptions::Validation.new(params: ['id'], message: :presence) }
let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message: :presence) }
subject { described_class.new(errors: [validation_error_1, validation_error_2]).full_messages }
it 'returns an array with each errors full message' do
expect(subject).to contain_exactly('id is missing', 'name is missing')
end
end
end
context 'api' do
subject { Class.new(Grape::API) }
def app
subject
end
it 'can return structured json with separate fields' do
subject.format :json
subject.rescue_from Grape::Exceptions::ValidationErrors do |e|
error!(e, 400)
end
subject.params do
optional :beer
optional :wine
optional :juice
exactly_one_of :beer, :wine, :juice
end
subject.get '/exactly_one_of' do
'exactly_one_of works!'
end
get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
expect(last_response.status).to eq(400)
expect(JSON.parse(last_response.body)).to eq([
'params' => %w[beer wine],
'messages' => ['are mutually exclusive']
])
end
end
end
grape-1.0.2/spec/grape/parser_spec.rb 0000644 0000041 0000041 00000004474 13231337007 017533 0 ustar www-data www-data require 'spec_helper'
describe Grape::Parser do
subject { described_class }
describe '.builtin_parsers' do
it 'returns an instance of Hash' do
expect(subject.builtin_parsers).to be_an_instance_of(Hash)
end
it 'includes json and xml parsers by default' do
expect(subject.builtin_parsers).to include(json: Grape::Parser::Json, xml: Grape::Parser::Xml)
end
end
describe '.parsers' do
it 'returns an instance of Hash' do
expect(subject.parsers({})).to be_an_instance_of(Hash)
end
it 'includes built-in parsers' do
expect(subject.parsers({})).to include(subject.builtin_parsers)
end
context 'with :parsers option' do
let(:parsers) { { customized: Class.new } }
it 'includes passed :parsers values' do
expect(subject.parsers(parsers: parsers)).to include(parsers)
end
end
context 'with added parser by using `register` keyword' do
let(:added_parser) { Class.new }
before { subject.register :added, added_parser }
it 'includes added parser' do
expect(subject.parsers({})).to include(added: added_parser)
end
end
end
describe '.parser_for' do
let(:options) { {} }
it 'calls .parsers' do
expect(subject).to receive(:parsers).with(options).and_return(subject.builtin_parsers)
subject.parser_for(:json, options)
end
it 'returns parser correctly' do
expect(subject.parser_for(:json)).to eq(Grape::Parser::Json)
end
context 'when parser is available' do
before { subject.register :customized_json, Grape::Parser::Json }
it 'returns registered parser if available' do
expect(subject.parser_for(:customized_json)).to eq(Grape::Parser::Json)
end
end
context 'when parser is an instance of Symbol' do
before do
allow(subject).to receive(:foo).and_return(:bar)
subject.register :foo, :foo
end
it 'returns an instance of Method' do
expect(subject.parser_for(:foo)).to be_an_instance_of(Method)
end
it 'returns object which can be called' do
method = subject.parser_for(:foo)
expect(method.call).to eq(:bar)
end
end
context 'when parser does not exist' do
it 'returns nil' do
expect(subject.parser_for(:undefined)).to be_nil
end
end
end
end
grape-1.0.2/spec/grape/api/ 0000755 0000041 0000041 00000000000 13231337007 015440 5 ustar www-data www-data grape-1.0.2/spec/grape/api/inherited_helpers_spec.rb 0000644 0000041 0000041 00000004707 13231337007 022504 0 ustar www-data www-data require 'spec_helper'
describe Grape::API::Helpers do
let(:user) { 'Miguel Caneo' }
let(:id) { '42' }
module InheritedHelpersSpec
class SuperClass < Grape::API
helpers do
params(:superclass_params) { requires :id, type: String }
def current_user
params[:user]
end
end
end
class OverriddenSubClass < SuperClass
params { use :superclass_params }
helpers do
def current_user
"#{params[:user]} with id"
end
end
get 'resource' do
"#{current_user}: #{params['id']}"
end
end
class SubClass < SuperClass
params { use :superclass_params }
get 'resource' do
"#{current_user}: #{params['id']}"
end
end
class Example < SubClass
params { use :superclass_params }
get 'resource' do
"#{current_user}: #{params['id']}"
end
end
end
context 'non overriding subclass' do
subject { InheritedHelpersSpec::SubClass }
def app
subject
end
context 'given expected params' do
it 'inherits helpers from a superclass' do
get '/resource', id: id, user: user
expect(last_response.body).to eq("#{user}: #{id}")
end
end
context 'with lack of expected params' do
it 'returns missing error' do
get '/resource'
expect(last_response.body).to eq('id is missing')
end
end
end
context 'overriding subclass' do
subject { InheritedHelpersSpec::OverriddenSubClass }
def app
subject
end
context 'given expected params' do
it 'overrides helpers from a superclass' do
get '/resource', id: id, user: user
expect(last_response.body).to eq("#{user} with id: #{id}")
end
end
context 'with lack of expected params' do
it 'returns missing error' do
get '/resource'
expect(last_response.body).to eq('id is missing')
end
end
end
context 'example subclass' do
subject { InheritedHelpersSpec::Example }
def app
subject
end
context 'given expected params' do
it 'inherits helpers from a superclass' do
get '/resource', id: id, user: user
expect(last_response.body).to eq("#{user}: #{id}")
end
end
context 'with lack of expected params' do
it 'returns missing error' do
get '/resource'
expect(last_response.body).to eq('id is missing')
end
end
end
end
grape-1.0.2/spec/grape/api/nested_helpers_spec.rb 0000644 0000041 0000041 00000001663 13231337007 022011 0 ustar www-data www-data require 'spec_helper'
describe Grape::API::Helpers do
module NestedHelpersSpec
module HelperMethods
extend Grape::API::Helpers
def current_user
@current_user ||= params[:current_user]
end
end
class Nested < Grape::API
resource :level1 do
helpers HelperMethods
get do
current_user
end
resource :level2 do
get do
current_user
end
end
end
end
class Main < Grape::API
mount Nested
end
end
subject do
NestedHelpersSpec::Main
end
def app
subject
end
it 'can access helpers from a mounted resource' do
get '/level1', current_user: 'hello'
expect(last_response.body).to eq('hello')
end
it 'can access helpers from a mounted resource in a nested resource' do
get '/level1/level2', current_user: 'world'
expect(last_response.body).to eq('world')
end
end
grape-1.0.2/spec/grape/api/invalid_format_spec.rb 0000644 0000041 0000041 00000001742 13231337007 022001 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
before do
subject.namespace do
format :json
content_type :json, 'application/json'
params do
requires :id, desc: 'Identifier.'
end
get ':id' do
{
id: params[:id],
format: params[:format]
}
end
end
end
context 'get' do
it 'no format' do
get '/foo'
expect(last_response.status).to eq 200
expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: nil))
end
it 'json format' do
get '/foo.json'
expect(last_response.status).to eq 200
expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'json'))
end
it 'invalid format' do
get '/foo.invalid'
expect(last_response.status).to eq 200
expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'invalid'))
end
end
end
grape-1.0.2/spec/grape/api/parameters_modification_spec.rb 0000644 0000041 0000041 00000001566 13231337007 023677 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
before do
subject.namespace :test do
params do
optional :foo, default: '-abcdef'
end
get do
params[:foo].slice!(0)
params[:foo]
end
end
end
context 'when route modifies param value' do
it 'param default should not change' do
get '/test'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'abcdef'
get '/test'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'abcdef'
get '/test?foo=-123456'
expect(last_response.status).to eq 200
expect(last_response.body).to eq '123456'
get '/test'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'abcdef'
end
end
end
grape-1.0.2/spec/grape/api/shared_helpers_spec.rb 0000644 0000041 0000041 00000001232 13231337007 021765 0 ustar www-data www-data require 'spec_helper'
describe Grape::API::Helpers do
subject do
shared_params = Module.new do
extend Grape::API::Helpers
params :pagination do
optional :page, type: Integer
optional :size, type: Integer
end
end
Class.new(Grape::API) do
helpers shared_params
format :json
params do
use :pagination
end
get do
declared(params, include_missing: true)
end
end
end
def app
subject
end
it 'defines parameters' do
get '/'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ page: nil, size: nil }.to_json)
end
end
grape-1.0.2/spec/grape/api/required_parameters_with_invalid_method_spec.rb 0000644 0000041 0000041 00000000605 13231337007 027144 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
before do
subject.namespace do
params do
requires :id, desc: 'Identifier.'
end
get ':id' do
end
end
end
context 'post' do
it '405' do
post '/something'
expect(last_response.status).to eq 405
end
end
end
grape-1.0.2/spec/grape/api/namespace_parameters_in_route_spec.rb 0000644 0000041 0000041 00000001226 13231337007 025063 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
before do
subject.namespace :me do
namespace :pending do
get '/' do
'banana'
end
end
put ':id' do
params[:id]
end
end
end
context 'get' do
it 'responds without ext' do
get '/me/pending'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'banana'
end
end
context 'put' do
it 'responds' do
put '/me/foo'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'foo'
end
end
end
grape-1.0.2/spec/grape/api/custom_validations_spec.rb 0000644 0000041 0000041 00000012523 13231337007 022711 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations do
context 'using a custom length validator' do
before do
module CustomValidationsSpec
class DefaultLength < Grape::Validations::Base
def validate_param!(attr_name, params)
@option = params[:max].to_i if params.key?(:max)
return if params[attr_name].length <= @option
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long"
end
end
end
end
subject do
Class.new(Grape::API) do
params do
requires :text, default_length: 140
end
get do
'bacon'
end
end
end
def app
subject
end
it 'under 140 characters' do
get '/', text: 'abc'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bacon'
end
it 'over 140 characters' do
get '/', text: 'a' * 141
expect(last_response.status).to eq 400
expect(last_response.body).to eq 'text must be at the most 140 characters long'
end
it 'specified in the query string' do
get '/', text: 'a' * 141, max: 141
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bacon'
end
end
context 'using a custom body-only validator' do
before do
module CustomValidationsSpec
class InBody < Grape::Validations::PresenceValidator
def validate(request)
validate!(request.env['api.request.body'])
end
end
end
end
subject do
Class.new(Grape::API) do
params do
requires :text, in_body: true
end
get do
'bacon'
end
end
end
def app
subject
end
it 'allows field in body' do
get '/', text: 'abc'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bacon'
end
it 'ignores field in query' do
get '/', nil, text: 'abc'
expect(last_response.status).to eq 400
expect(last_response.body).to eq 'text is missing'
end
end
context 'using a custom validator with message_key' do
before do
module CustomValidationsSpec
class WithMessageKey < Grape::Validations::PresenceValidator
def validate_param!(attr_name, _params)
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: :presence
end
end
end
end
subject do
Class.new(Grape::API) do
params do
requires :text, with_message_key: true
end
get do
'bacon'
end
end
end
def app
subject
end
it 'fails with message' do
get '/', text: 'foobar'
expect(last_response.status).to eq 400
expect(last_response.body).to eq 'text is missing'
end
end
context 'using a custom request/param validator' do
before do
module CustomValidationsSpec
class Admin < Grape::Validations::Base
def validate(request)
# return if the param we are checking was not in request
# @attrs is a list containing the attribute we are currently validating
return unless request.params.key? @attrs.first
# check if admin flag is set to true
return unless @option
# check if user is admin or not
# as an example get a token from request and check if it's admin or not
raise Grape::Exceptions::Validation, params: @attrs, message: 'Can not set Admin only field.' unless request.headers['X-Access-Token'] == 'admin'
end
end
end
end
subject do
Class.new(Grape::API) do
params do
optional :admin_field, type: String, admin: true
optional :non_admin_field, type: String
optional :admin_false_field, type: String, admin: false
end
get do
'bacon'
end
end
end
def app
subject
end
it 'fail when non-admin user sets an admin field' do
get '/', admin_field: 'tester', non_admin_field: 'toaster'
expect(last_response.status).to eq 400
expect(last_response.body).to include 'Can not set Admin only field.'
end
it 'does not fail when we send non-admin fields only' do
get '/', non_admin_field: 'toaster'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bacon'
end
it 'does not fail when we send non-admin and admin=false fields only' do
get '/', non_admin_field: 'toaster', admin_false_field: 'test'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bacon'
end
it 'does not fail when we send admin fields and we are admin' do
header 'X-Access-Token', 'admin'
get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bacon'
end
it 'fails when we send admin fields and we are not admin' do
header 'X-Access-Token', 'user'
get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test'
expect(last_response.status).to eq 400
expect(last_response.body).to include 'Can not set Admin only field.'
end
end
end
grape-1.0.2/spec/grape/api/optional_parameters_in_route_spec.rb 0000644 0000041 0000041 00000001463 13231337007 024757 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
before do
subject.namespace :api do
get ':id(/:ext)' do
[params[:id], params[:ext]].compact.join('/')
end
put ':id' do
params[:id]
end
end
end
context 'get' do
it 'responds without ext' do
get '/api/foo'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'foo'
end
it 'responds with ext' do
get '/api/foo/bar'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'foo/bar'
end
end
context 'put' do
it 'responds' do
put '/api/foo'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'foo'
end
end
end
grape-1.0.2/spec/grape/api/patch_method_helpers_spec.rb 0000644 0000041 0000041 00000003304 13231337007 023160 0 ustar www-data www-data require 'spec_helper'
describe Grape::API::Helpers do
module PatchHelpersSpec
class PatchPublic < Grape::API
format :json
version 'public-v1', using: :header, vendor: 'grape'
get do
{ ok: 'public' }
end
end
module AuthMethods
def authenticate!; end
end
class PatchPrivate < Grape::API
format :json
version 'private-v1', using: :header, vendor: 'grape'
helpers AuthMethods
before do
authenticate!
end
get do
{ ok: 'private' }
end
end
class Main < Grape::API
mount PatchPublic
mount PatchPrivate
end
end
def app
PatchHelpersSpec::Main
end
context 'patch' do
it 'public' do
patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json'
expect(last_response.status).to eq 405
end
it 'private' do
patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json'
expect(last_response.status).to eq 405
end
it 'default' do
patch '/'
expect(last_response.status).to eq 405
end
end
context 'default' do
it 'public' do
get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ ok: 'public' }.to_json)
end
it 'private' do
get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ ok: 'private' }.to_json)
end
it 'default' do
get '/'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ ok: 'public' }.to_json)
end
end
end
grape-1.0.2/spec/grape/api/deeply_included_options_spec.rb 0000644 0000041 0000041 00000002164 13231337007 023706 0 ustar www-data www-data require 'spec_helper'
module DeeplyIncludedOptionsSpec
module Defaults
extend ActiveSupport::Concern
included do
format :json
end
end
module Admin
module Defaults
extend ActiveSupport::Concern
include DeeplyIncludedOptionsSpec::Defaults
end
class Users < Grape::API
include DeeplyIncludedOptionsSpec::Admin::Defaults
resource :users do
get do
status 200
end
end
end
end
class Main < Grape::API
mount DeeplyIncludedOptionsSpec::Admin::Users
end
end
describe Grape::API do
subject { DeeplyIncludedOptionsSpec::Main }
def app
subject
end
it 'works for unspecified format' do
get '/users'
expect(last_response.status).to eql 200
expect(last_response.content_type).to eql 'application/json'
end
it 'works for specified format' do
get '/users.json'
expect(last_response.status).to eql 200
expect(last_response.content_type).to eql 'application/json'
end
it "doesn't work for format different than specified" do
get '/users.txt'
expect(last_response.status).to eql 404
end
end
grape-1.0.2/spec/grape/api/required_parameters_in_route_spec.rb 0000644 0000041 0000041 00000001226 13231337007 024747 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
before do
subject.namespace :api do
get ':id' do
[params[:id], params[:ext]].compact.join('/')
end
put ':something_id' do
params[:something_id]
end
end
end
context 'get' do
it 'responds' do
get '/api/foo'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'foo'
end
end
context 'put' do
it 'responds' do
put '/api/foo'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'foo'
end
end
end
grape-1.0.2/spec/grape/api/recognize_path_spec.rb 0000644 0000041 0000041 00000001047 13231337007 022002 0 ustar www-data www-data require 'spec_helper'
describe Grape::API do
describe '.recognize_path' do
subject { Class.new(Grape::API) }
it 'fetches endpoint by given path' do
subject.get('/foo/:id') {}
subject.get('/bar/:id') {}
subject.get('/baz/:id') {}
actual = subject.recognize_path('/bar/1234').routes[0].origin
expect(actual).to eq('/bar/:id')
end
it 'returns nil if given path does not match with registered routes' do
subject.get {}
expect(subject.recognize_path('/bar/1234')).to be_nil
end
end
end
grape-1.0.2/spec/grape/entity_spec.rb 0000644 0000041 0000041 00000021574 13231337007 017553 0 ustar www-data www-data require 'spec_helper'
require 'grape_entity'
describe Grape::Entity do
subject { Class.new(Grape::API) }
def app
subject
end
describe '#present' do
it 'sets the object as the body if no options are provided' do
inner_body = nil
subject.get '/example' do
present(abc: 'def')
inner_body = body
end
get '/example'
expect(inner_body).to eql(abc: 'def')
end
it 'calls through to the provided entity class if one is given' do
entity_mock = Object.new
allow(entity_mock).to receive(:represent)
subject.get '/example' do
present Object.new, with: entity_mock
end
get '/example'
end
it 'pulls a representation from the class options if it exists' do
entity = Class.new(Grape::Entity)
allow(entity).to receive(:represent).and_return('Hiya')
subject.represent Object, with: entity
subject.get '/example' do
present Object.new
end
get '/example'
expect(last_response.body).to eq('Hiya')
end
it 'pulls a representation from the class options if the presented object is a collection of objects' do
entity = Class.new(Grape::Entity)
allow(entity).to receive(:represent).and_return('Hiya')
module EntitySpec
class TestObject
end
class FakeCollection
def first
TestObject.new
end
end
end
subject.represent EntitySpec::TestObject, with: entity
subject.get '/example' do
present [EntitySpec::TestObject.new]
end
subject.get '/example2' do
present EntitySpec::FakeCollection.new
end
get '/example'
expect(last_response.body).to eq('Hiya')
get '/example2'
expect(last_response.body).to eq('Hiya')
end
it 'pulls a representation from the class ancestor if it exists' do
entity = Class.new(Grape::Entity)
allow(entity).to receive(:represent).and_return('Hiya')
subclass = Class.new(Object)
subject.represent Object, with: entity
subject.get '/example' do
present subclass.new
end
get '/example'
expect(last_response.body).to eq('Hiya')
end
it 'automatically uses Klass::Entity if that exists' do
some_model = Class.new
entity = Class.new(Grape::Entity)
allow(entity).to receive(:represent).and_return('Auto-detect!')
some_model.const_set :Entity, entity
subject.get '/example' do
present some_model.new
end
get '/example'
expect(last_response.body).to eq('Auto-detect!')
end
it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
some_model = Class.new
entity = Class.new(Grape::Entity)
allow(entity).to receive(:represent).and_return('Auto-detect!')
some_model.const_set :Entity, entity
subject.get '/example' do
present [some_model.new]
end
get '/example'
expect(last_response.body).to eq('Auto-detect!')
end
it 'does not run autodetection for Entity when explicitely provided' do
entity = Class.new(Grape::Entity)
some_array = []
subject.get '/example' do
present some_array, with: entity
end
expect(some_array).not_to receive(:first)
get '/example'
end
it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do
entity = Class.new(Grape::Entity)
some_relation = Class.new
some_model = Class.new
allow(entity).to receive(:represent).and_return('Auto-detect!')
allow(some_relation).to receive(:first)
allow(some_relation).to receive(:klass).and_return(some_model)
some_model.const_set :Entity, entity
subject.get '/example' do
present some_relation
end
expect(some_relation).not_to receive(:first)
get '/example'
expect(last_response.body).to eq('Auto-detect!')
end
it 'autodetection does not use Entity if it is not a presenter' do
some_model = Class.new
entity = Class.new
some_model.class.const_set :Entity, entity
subject.get '/example' do
present some_model
end
get '/example'
expect(entity).not_to receive(:represent)
end
it 'adds a root key to the output if one is given' do
inner_body = nil
subject.get '/example' do
present({ abc: 'def' }, root: :root)
inner_body = body
end
get '/example'
expect(inner_body).to eql(root: { abc: 'def' })
end
%i[json serializable_hash].each do |format|
it "presents with #{format}" do
entity = Class.new(Grape::Entity)
entity.root 'examples', 'example'
entity.expose :id
subject.format format
subject.get '/example' do
c = Class.new do
attr_reader :id
def initialize(id)
@id = id
end
end
present c.new(1), with: entity
end
get '/example'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('{"example":{"id":1}}')
end
it "presents with #{format} collection" do
entity = Class.new(Grape::Entity)
entity.root 'examples', 'example'
entity.expose :id
subject.format format
subject.get '/examples' do
c = Class.new do
attr_reader :id
def initialize(id)
@id = id
end
end
examples = [c.new(1), c.new(2)]
present examples, with: entity
end
get '/examples'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('{"examples":[{"id":1},{"id":2}]}')
end
end
it 'presents with xml' do
entity = Class.new(Grape::Entity)
entity.root 'examples', 'example'
entity.expose :name
subject.format :xml
subject.get '/example' do
c = Class.new do
attr_reader :name
def initialize(args)
@name = args[:name] || 'no name set'
end
end
present c.new(name: 'johnnyiller'), with: entity
end
get '/example'
expect(last_response.status).to eq(200)
expect(last_response.headers['Content-type']).to eq('application/xml')
expect(last_response.body).to eq <<-XML
johnnyiller
XML
end
it 'presents with json' do
entity = Class.new(Grape::Entity)
entity.root 'examples', 'example'
entity.expose :name
subject.format :json
subject.get '/example' do
c = Class.new do
attr_reader :name
def initialize(args)
@name = args[:name] || 'no name set'
end
end
present c.new(name: 'johnnyiller'), with: entity
end
get '/example'
expect(last_response.status).to eq(200)
expect(last_response.headers['Content-type']).to eq('application/json')
expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}')
end
it 'presents with jsonp utilising Rack::JSONP' do
# Include JSONP middleware
subject.use Rack::JSONP
entity = Class.new(Grape::Entity)
entity.root 'examples', 'example'
entity.expose :name
# Rack::JSONP expects a standard JSON response in UTF-8 format
subject.format :json
subject.formatter :json, lambda { |object, _|
object.to_json.encode('utf-8')
}
subject.get '/example' do
c = Class.new do
attr_reader :name
def initialize(args)
@name = args[:name] || 'no name set'
end
end
present c.new(name: 'johnnyiller'), with: entity
end
get '/example?callback=abcDef'
expect(last_response.status).to eq(200)
expect(last_response.headers['Content-type']).to eq('application/javascript')
expect(last_response.body).to include 'abcDef({"example":{"name":"johnnyiller"}})'
end
context 'present with multiple entities' do
it 'present with multiple entities using optional symbol' do
user = Class.new do
attr_reader :name
def initialize(args)
@name = args[:name] || 'no name set'
end
end
user1 = user.new(name: 'user1')
user2 = user.new(name: 'user2')
entity = Class.new(Grape::Entity)
entity.expose :name
subject.format :json
subject.get '/example' do
present :page, 1
present :user1, user1, with: entity
present :user2, user2, with: entity
end
get '/example'
expect_response_json = {
'page' => 1,
'user1' => { 'name' => 'user1' },
'user2' => { 'name' => 'user2' }
}
expect(JSON(last_response.body)).to eq(expect_response_json)
end
end
end
end
grape-1.0.2/spec/grape/api_spec.rb 0000644 0000041 0000041 00000311370 13231337007 017004 0 ustar www-data www-data require 'spec_helper'
require 'shared/versioning_examples'
describe Grape::API do
subject { Class.new(Grape::API) }
def app
subject
end
describe '.prefix' do
it 'routes root through with the prefix' do
subject.prefix 'awesome/sauce'
subject.get do
'Hello there.'
end
get 'awesome/sauce/'
expect(last_response.status).to eql 200
expect(last_response.body).to eql 'Hello there.'
end
it 'routes through with the prefix' do
subject.prefix 'awesome/sauce'
subject.get :hello do
'Hello there.'
end
get 'awesome/sauce/hello'
expect(last_response.body).to eql 'Hello there.'
get '/hello'
expect(last_response.status).to eql 404
end
it 'supports OPTIONS' do
subject.prefix 'awesome/sauce'
subject.get do
'Hello there.'
end
options 'awesome/sauce'
expect(last_response.status).to eql 204
expect(last_response.body).to be_blank
end
it 'disallows POST' do
subject.prefix 'awesome/sauce'
subject.get
post 'awesome/sauce'
expect(last_response.status).to eql 405
end
end
describe '.version' do
context 'when defined' do
it 'returns version value' do
subject.version 'v1'
expect(subject.version).to eq('v1')
end
end
context 'when not defined' do
it 'returns nil' do
expect(subject.version).to be_nil
end
end
end
describe '.version using path' do
it_should_behave_like 'versioning' do
let(:macro_options) do
{
using: :path
}
end
end
end
describe '.version using param' do
it_should_behave_like 'versioning' do
let(:macro_options) do
{
using: :param,
parameter: 'apiver'
}
end
end
end
describe '.version using header' do
it_should_behave_like 'versioning' do
let(:macro_options) do
{
using: :header,
vendor: 'mycompany',
format: 'json'
}
end
end
# Behavior as defined by rfc2616 when no header is defined
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
describe 'no specified accept header' do
# subject.version 'v1', using: :header
# subject.get '/hello' do
# 'hello'
# end
# it 'routes' do
# get '/hello'
# last_response.status.should eql 200
# end
end
# pending 'routes if any media type is allowed'
end
describe '.version using accept_version_header' do
it_should_behave_like 'versioning' do
let(:macro_options) do
{
using: :accept_version_header
}
end
end
end
describe '.represent' do
it 'requires a :with option' do
expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
end
it 'adds the association to the :representations setting' do
klass = Class.new
subject.represent Object, with: klass
expect(subject.namespace_stackable_with_hash(:representations)[Object]).to eq(klass)
end
end
describe '.namespace' do
it 'is retrievable and converted to a path' do
internal_namespace = nil
subject.namespace :awesome do
internal_namespace = namespace
end
expect(internal_namespace).to eql('/awesome')
end
it 'comes after the prefix and version' do
subject.prefix :rad
subject.version 'v1', using: :path
subject.namespace :awesome do
get('/hello') { 'worked' }
end
get '/rad/v1/awesome/hello'
expect(last_response.body).to eq('worked')
end
it 'cancels itself after the block is over' do
internal_namespace = nil
subject.namespace :awesome do
internal_namespace = namespace
end
expect(subject.namespace).to eql('/')
end
it 'is stackable' do
internal_namespace = nil
internal_second_namespace = nil
subject.namespace :awesome do
internal_namespace = namespace
namespace :rad do
internal_second_namespace = namespace
end
end
expect(internal_namespace).to eq('/awesome')
expect(internal_second_namespace).to eq('/awesome/rad')
end
it 'accepts path segments correctly' do
inner_namespace = nil
subject.namespace :members do
namespace '/:member_id' do
inner_namespace = namespace
get '/' do
params[:member_id]
end
end
end
get '/members/23'
expect(last_response.body).to eq('23')
expect(inner_namespace).to eq('/members/:member_id')
end
it 'is callable with nil just to push onto the stack' do
subject.namespace do
version 'v2', using: :path
get('/hello') { 'inner' }
end
subject.get('/hello') { 'outer' }
get '/v2/hello'
expect(last_response.body).to eq('inner')
get '/hello'
expect(last_response.body).to eq('outer')
end
%w[group resource resources segment].each do |als|
it "`.#{als}` is an alias" do
inner_namespace = nil
subject.send(als, :awesome) do
inner_namespace = namespace
end
expect(inner_namespace).to eq '/awesome'
end
end
end
describe '.route_param' do
it 'adds a parameterized route segment namespace' do
subject.namespace :users do
route_param :id do
get do
params[:id]
end
end
end
get '/users/23'
expect(last_response.body).to eq('23')
end
it 'defines requirements with a single hash' do
subject.namespace :users do
route_param :id, requirements: /[0-9]+/ do
get do
params[:id]
end
end
end
get '/users/michael'
expect(last_response.status).to eq(404)
get '/users/23'
expect(last_response.status).to eq(200)
end
context 'with param type definitions' do
it 'is used by passing to options' do
subject.namespace :route_param do
route_param :foo, type: Integer do
get { params.to_json }
end
end
get '/route_param/1234'
expect(last_response.body).to eq('{"foo":1234}')
end
end
end
describe '.route' do
it 'allows for no path' do
subject.namespace :votes do
get do
'Votes'
end
post do
'Created a Vote'
end
end
get '/votes'
expect(last_response.body).to eql 'Votes'
post '/votes'
expect(last_response.body).to eql 'Created a Vote'
end
it 'handles empty calls' do
subject.get '/'
get '/'
expect(last_response.body).to eql ''
end
describe 'root routes should work with' do
before do
subject.format :txt
subject.content_type :json, 'application/json'
subject.formatter :json, ->(object, _env) { object }
def subject.enable_root_route!
get('/') { 'root' }
end
end
after do
expect(last_response.body).to eql 'root'
end
describe 'path versioned APIs' do
before do
subject.version version, using: :path
subject.enable_root_route!
end
context 'when a single version provided' do
let(:version) { 'v1' }
it 'without a format' do
versioned_get '/', 'v1', using: :path
end
it 'with a format' do
get '/v1/.json'
end
end
context 'when array of versions provided' do
let(:version) { %w[v1 v2] }
it { versioned_get '/', 'v1', using: :path }
it { versioned_get '/', 'v2', using: :path }
end
end
it 'header versioned APIs' do
subject.version 'v1', using: :header, vendor: 'test'
subject.enable_root_route!
versioned_get '/', 'v1', using: :header, vendor: 'test'
end
it 'header versioned APIs with multiple headers' do
subject.version %w[v1 v2], using: :header, vendor: 'test'
subject.enable_root_route!
versioned_get '/', 'v1', using: :header, vendor: 'test'
versioned_get '/', 'v2', using: :header, vendor: 'test'
end
it 'param versioned APIs' do
subject.version 'v1', using: :param
subject.enable_root_route!
versioned_get '/', 'v1', using: :param
end
it 'Accept-Version header versioned APIs' do
subject.version 'v1', using: :accept_version_header
subject.enable_root_route!
versioned_get '/', 'v1', using: :accept_version_header
end
it 'unversioned APIs' do
subject.enable_root_route!
get '/'
end
end
it 'allows for multiple paths' do
subject.get(['/abc', '/def']) do
'foo'
end
get '/abc'
expect(last_response.body).to eql 'foo'
get '/def'
expect(last_response.body).to eql 'foo'
end
context 'format' do
module ApiSpec
class DummyFormatClass
end
end
before(:each) do
allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_json).and_return('abc')
allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_txt).and_return('def')
subject.get('/abc') do
ApiSpec::DummyFormatClass.new
end
end
it 'allows .json' do
get '/abc.json'
expect(last_response.status).to eq(200)
expect(last_response.body).to eql 'abc' # json-encoded symbol
end
it 'allows .txt' do
get '/abc.txt'
expect(last_response.status).to eq(200)
expect(last_response.body).to eql 'def' # raw text
end
end
it 'allows for format without corrupting a param' do
subject.get('/:id') do
{ 'id' => params[:id] }
end
get '/awesome.json'
expect(last_response.body).to eql '{"id":"awesome"}'
end
it 'allows for format in namespace with no path' do
subject.namespace :abc do
get do
['json']
end
end
get '/abc.json'
expect(last_response.body).to eql '["json"]'
end
it 'allows for multiple verbs' do
subject.route(%i[get post], '/abc') do
'hiya'
end
subject.endpoints.first.routes.each do |route|
expect(route.path).to eql '/abc(.:format)'
end
get '/abc'
expect(last_response.body).to eql 'hiya'
post '/abc'
expect(last_response.body).to eql 'hiya'
end
%i[put post].each do |verb|
context verb do
['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object|
it "allows a(n) #{object.class} json object in params" do
subject.format :json
subject.send(verb) do
env['api.request.body']
end
send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(verb == :post ? 201 : 200)
expect(last_response.body).to eql ::Grape::Json.dump(object)
expect(last_request.params).to eql({})
end
it 'stores input in api.request.input' do
subject.format :json
subject.send(verb) do
env['api.request.input']
end
send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(verb == :post ? 201 : 200)
expect(last_response.body).to eql ::Grape::Json.dump(object).to_json
end
context 'chunked transfer encoding' do
it 'stores input in api.request.input' do
subject.format :json
subject.send(verb) do
env['api.request.input']
end
send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil
expect(last_response.status).to eq(verb == :post ? 201 : 200)
expect(last_response.body).to eql ::Grape::Json.dump(object).to_json
end
end
end
end
end
it 'allows for multipart paths' do
subject.route(%i[get post], '/:id/first') do
'first'
end
subject.route(%i[get post], '/:id') do
'ola'
end
subject.route(%i[get post], '/:id/first/second') do
'second'
end
get '/1'
expect(last_response.body).to eql 'ola'
post '/1'
expect(last_response.body).to eql 'ola'
get '/1/first'
expect(last_response.body).to eql 'first'
post '/1/first'
expect(last_response.body).to eql 'first'
get '/1/first/second'
expect(last_response.body).to eql 'second'
end
it 'allows for :any as a verb' do
subject.route(:any, '/abc') do
'lol'
end
%w[get post put delete options patch].each do |m|
send(m, '/abc')
expect(last_response.body).to eql 'lol'
end
end
it 'allows for catch-all in a namespace' do
subject.namespace :nested do
get do
'root'
end
get 'something' do
'something'
end
route :any, '*path' do
'catch-all'
end
end
get 'nested'
expect(last_response.body).to eql 'root'
get 'nested/something'
expect(last_response.body).to eql 'something'
get 'nested/missing'
expect(last_response.body).to eql 'catch-all'
post 'nested'
expect(last_response.body).to eql 'catch-all'
post 'nested/something'
expect(last_response.body).to eql 'catch-all'
end
verbs = %w[post get head delete put options patch]
verbs.each do |verb|
it "allows and properly constrain a #{verb.upcase} method" do
subject.send(verb, '/example') do
verb
end
send(verb, '/example')
expect(last_response.body).to eql verb == 'head' ? '' : verb
# Call it with all methods other than the properly constrained one.
(verbs - [verb]).each do |other_verb|
send(other_verb, '/example')
expected_rc = if other_verb == 'options' then 204
elsif other_verb == 'head' && verb == 'get' then 200
else 405
end
expect(last_response.status).to eql expected_rc
end
end
end
it 'returns a 201 response code for POST by default' do
subject.post('example') do
'Created'
end
post '/example'
expect(last_response.status).to eql 201
expect(last_response.body).to eql 'Created'
end
it 'returns a 405 for an unsupported method with an X-Custom-Header' do
subject.before { header 'X-Custom-Header', 'foo' }
subject.get 'example' do
'example'
end
put '/example'
expect(last_response.status).to eql 405
expect(last_response.body).to eql '405 Not Allowed'
expect(last_response.headers['X-Custom-Header']).to eql 'foo'
end
it 'runs only the before filter on 405 bad method' do
subject.namespace :example do
before { header 'X-Custom-Header', 'foo' }
before_validation { raise 'before_validation filter should not run' }
after_validation { raise 'after_validation filter should not run' }
after { raise 'after filter should not run' }
params { requires :only_for_get }
get
end
post '/example'
expect(last_response.status).to eql 405
expect(last_response.headers['X-Custom-Header']).to eql 'foo'
end
it 'runs before filter exactly once on 405 bad method' do
already_run = false
subject.namespace :example do
before do
raise 'before filter ran twice' if already_run
already_run = true
header 'X-Custom-Header', 'foo'
end
get
end
post '/example'
expect(last_response.status).to eql 405
expect(last_response.headers['X-Custom-Header']).to eql 'foo'
end
it 'runs all filters and body with a custom OPTIONS method' do
subject.namespace :example do
before { header 'X-Custom-Header-1', 'foo' }
before_validation { header 'X-Custom-Header-2', 'foo' }
after_validation { header 'X-Custom-Header-3', 'foo' }
after { header 'X-Custom-Header-4', 'foo' }
options { 'yup' }
get
end
options '/example'
expect(last_response.status).to eql 200
expect(last_response.body).to eql 'yup'
expect(last_response.headers['Allow']).to be_nil
expect(last_response.headers['X-Custom-Header-1']).to eql 'foo'
expect(last_response.headers['X-Custom-Header-2']).to eql 'foo'
expect(last_response.headers['X-Custom-Header-3']).to eql 'foo'
expect(last_response.headers['X-Custom-Header-4']).to eql 'foo'
end
context 'when format is xml' do
it 'returns a 405 for an unsupported method' do
subject.format :xml
subject.get 'example' do
'example'
end
put '/example'
expect(last_response.status).to eql 405
expect(last_response.body).to eq <<-XML
405 Not Allowed
XML
end
end
context 'when accessing env' do
it 'returns a 405 for an unsupported method' do
subject.before do
_customheader1 = headers['X-Custom-Header']
_customheader2 = env['HTTP_X_CUSTOM_HEADER']
end
subject.get 'example' do
'example'
end
put '/example'
expect(last_response.status).to eql 405
expect(last_response.body).to eql '405 Not Allowed'
end
end
specify '405 responses includes an Allow header specifying supported methods' do
subject.get 'example' do
'example'
end
subject.post 'example' do
'example'
end
put '/example'
expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, POST, HEAD'
end
specify '405 responses includes an Content-Type header' do
subject.get 'example' do
'example'
end
subject.post 'example' do
'example'
end
put '/example'
expect(last_response.headers['Content-Type']).to eql 'text/plain'
end
describe 'adds an OPTIONS route that' do
before do
subject.before { header 'X-Custom-Header', 'foo' }
subject.before_validation { header 'X-Custom-Header-2', 'bar' }
subject.after_validation { header 'X-Custom-Header-3', 'baz' }
subject.after { header 'X-Custom-Header-4', 'bing' }
subject.params { requires :only_for_get }
subject.get 'example' do
'example'
end
subject.route :any, '*path' do
error! :not_found, 404
end
options '/example'
end
it 'returns a 204' do
expect(last_response.status).to eql 204
end
it 'has an empty body' do
expect(last_response.body).to be_blank
end
it 'has an Allow header' do
expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD'
end
it 'calls before hook' do
expect(last_response.headers['X-Custom-Header']).to eql 'foo'
end
it 'does not call before_validation hook' do
expect(last_response.headers.key?('X-Custom-Header-2')).to be false
end
it 'does not call after_validation hook' do
expect(last_response.headers.key?('X-Custom-Header-3')).to be false
end
it 'calls after hook' do
expect(last_response.headers['X-Custom-Header-4']).to eq 'bing'
end
it 'has no Content-Type' do
expect(last_response.content_type).to be_nil
end
it 'has no Content-Length' do
expect(last_response.content_length).to be_nil
end
end
describe 'adds an OPTIONS route for namespaced endpoints that' do
before do
subject.before { header 'X-Custom-Header', 'foo' }
subject.namespace :example do
before { header 'X-Custom-Header-2', 'foo' }
get :inner do
'example/inner'
end
end
options '/example/inner'
end
it 'returns a 204' do
expect(last_response.status).to eql 204
end
it 'has an empty body' do
expect(last_response.body).to be_blank
end
it 'has an Allow header' do
expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD'
end
it 'calls the outer before filter' do
expect(last_response.headers['X-Custom-Header']).to eql 'foo'
end
it 'calls the inner before filter' do
expect(last_response.headers['X-Custom-Header-2']).to eql 'foo'
end
it 'has no Content-Type' do
expect(last_response.content_type).to be_nil
end
it 'has no Content-Length' do
expect(last_response.content_length).to be_nil
end
end
describe 'adds a 405 Not Allowed route that' do
before do
subject.before { header 'X-Custom-Header', 'foo' }
subject.post :example do
'example'
end
get '/example'
end
it 'returns a 405' do
expect(last_response.status).to eql 405
end
it 'contains error message in body' do
expect(last_response.body).to eq '405 Not Allowed'
end
it 'has an Allow header' do
expect(last_response.headers['Allow']).to eql 'OPTIONS, POST'
end
it 'has a X-Custom-Header' do
expect(last_response.headers['X-Custom-Header']).to eql 'foo'
end
end
context 'allows HEAD on a GET request that' do
before do
subject.get 'example' do
'example'
end
subject.route :any, '*path' do
error! :not_found, 404
end
head '/example'
end
it 'returns a 200' do
expect(last_response.status).to eql 200
end
it 'has an empty body' do
expect(last_response.body).to eql ''
end
end
it 'overwrites the default HEAD request' do
subject.head 'example' do
error! 'nothing to see here', 400
end
subject.get 'example' do
'example'
end
head '/example'
expect(last_response.status).to eql 400
end
end
context 'do_not_route_head!' do
before :each do
subject.do_not_route_head!
subject.get 'example' do
'example'
end
end
it 'options does not contain HEAD' do
options '/example'
expect(last_response.status).to eql 204
expect(last_response.body).to eql ''
expect(last_response.headers['Allow']).to eql 'OPTIONS, GET'
end
it 'does not allow HEAD on a GET request' do
head '/example'
expect(last_response.status).to eql 405
end
end
context 'do_not_route_options!' do
before :each do
subject.do_not_route_options!
subject.get 'example' do
'example'
end
end
it 'does not create an OPTIONS route' do
options '/example'
expect(last_response.status).to eql 405
end
it 'does not include OPTIONS in Allow header' do
options '/example'
expect(last_response.status).to eql 405
expect(last_response.headers['Allow']).to eql 'GET, HEAD'
end
end
describe 'filters' do
it 'adds a before filter' do
subject.before { @foo = 'first' }
subject.before { @bar = 'second' }
subject.get '/' do
"#{@foo} #{@bar}"
end
get '/'
expect(last_response.body).to eql 'first second'
end
it 'adds a before filter to current and child namespaces only' do
subject.get '/' do
"root - #{@foo}"
end
subject.namespace :blah do
before { @foo = 'foo' }
get '/' do
"blah - #{@foo}"
end
namespace :bar do
get '/' do
"blah - bar - #{@foo}"
end
end
end
get '/'
expect(last_response.body).to eql 'root - '
get '/blah'
expect(last_response.body).to eql 'blah - foo'
get '/blah/bar'
expect(last_response.body).to eql 'blah - bar - foo'
end
it 'adds a after_validation filter' do
subject.after_validation { @foo = "first #{params[:id]}:#{params[:id].class}" }
subject.after_validation { @bar = 'second' }
subject.params do
requires :id, type: Integer
end
subject.get '/' do
"#{@foo} #{@bar}"
end
get '/', id: '32'
expect(last_response.body).to eql "first 32:#{integer_class_name} second"
end
it 'adds a after filter' do
m = double('after mock')
subject.after { m.do_something! }
subject.after { m.do_something! }
subject.get '/' do
@var ||= 'default'
end
expect(m).to receive(:do_something!).exactly(2).times
get '/'
expect(last_response.body).to eql 'default'
end
it 'calls all filters when validation passes' do
a = double('before mock')
b = double('before_validation mock')
c = double('after_validation mock')
d = double('after mock')
subject.params do
requires :id, type: Integer
end
subject.resource ':id' do
before { a.do_something! }
before_validation { b.do_something! }
after_validation { c.do_something! }
after { d.do_something! }
get do
'got it'
end
end
expect(a).to receive(:do_something!).exactly(1).times
expect(b).to receive(:do_something!).exactly(1).times
expect(c).to receive(:do_something!).exactly(1).times
expect(d).to receive(:do_something!).exactly(1).times
get '/123'
expect(last_response.status).to eql 200
expect(last_response.body).to eql 'got it'
end
it 'calls only before filters when validation fails' do
a = double('before mock')
b = double('before_validation mock')
c = double('after_validation mock')
d = double('after mock')
subject.params do
requires :id, type: Integer
end
subject.resource ':id' do
before { a.do_something! }
before_validation { b.do_something! }
after_validation { c.do_something! }
after { d.do_something! }
get do
'got it'
end
end
expect(a).to receive(:do_something!).exactly(1).times
expect(b).to receive(:do_something!).exactly(1).times
expect(c).to receive(:do_something!).exactly(0).times
expect(d).to receive(:do_something!).exactly(0).times
get '/abc'
expect(last_response.status).to eql 400
expect(last_response.body).to eql 'id is invalid'
end
it 'calls filters in the correct order' do
i = 0
a = double('before mock')
b = double('before_validation mock')
c = double('after_validation mock')
d = double('after mock')
subject.params do
requires :id, type: Integer
end
subject.resource ':id' do
before { a.here(i += 1) }
before_validation { b.here(i += 1) }
after_validation { c.here(i += 1) }
after { d.here(i += 1) }
get do
'got it'
end
end
expect(a).to receive(:here).with(1).exactly(1).times
expect(b).to receive(:here).with(2).exactly(1).times
expect(c).to receive(:here).with(3).exactly(1).times
expect(d).to receive(:here).with(4).exactly(1).times
get '/123'
expect(last_response.status).to eql 200
expect(last_response.body).to eql 'got it'
end
end
context 'format' do
before do
subject.get('/foo') { 'bar' }
end
it 'sets content type for txt format' do
get '/foo'
expect(last_response.headers['Content-Type']).to eq('text/plain')
end
it 'sets content type for xml' do
get '/foo.xml'
expect(last_response.headers['Content-Type']).to eq('application/xml')
end
it 'sets content type for json' do
get '/foo.json'
expect(last_response.headers['Content-Type']).to eq('application/json')
end
it 'sets content type for serializable hash format' do
get '/foo.serializable_hash'
expect(last_response.headers['Content-Type']).to eq('application/json')
end
it 'sets content type for binary format' do
get '/foo.binary'
expect(last_response.headers['Content-Type']).to eq('application/octet-stream')
end
it 'returns raw data when content type binary' do
image_filename = 'grape.png'
file = File.open(image_filename, 'rb', &:read)
subject.format :binary
subject.get('/binary_file') { File.binread(image_filename) }
get '/binary_file'
expect(last_response.headers['Content-Type']).to eq('application/octet-stream')
expect(last_response.body).to eq(file)
end
it 'returns the content of the file with file' do
file_content = 'This is some file content'
test_file = Tempfile.new('test')
test_file.write file_content
test_file.rewind
subject.get('/file') { file test_file }
get '/file'
expect(last_response.headers['Content-Length']).to eq('25')
expect(last_response.headers['Content-Type']).to eq('text/plain')
expect(last_response.body).to eq(file_content)
end
it 'streams the content of the file with stream' do
test_stream = Enumerator.new do |blk|
blk.yield 'This is some'
blk.yield ' file content'
end
subject.use Rack::Chunked
subject.get('/stream') { stream test_stream }
get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1'
expect(last_response.headers['Content-Type']).to eq('text/plain')
expect(last_response.headers['Content-Length']).to eq(nil)
expect(last_response.headers['Cache-Control']).to eq('no-cache')
expect(last_response.headers['Transfer-Encoding']).to eq('chunked')
expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n")
end
it 'sets content type for error' do
subject.get('/error') { error!('error in plain text', 500) }
get '/error'
expect(last_response.headers['Content-Type']).to eql 'text/plain'
end
it 'sets content type for json error' do
subject.format :json
subject.get('/error') { error!('error in json', 500) }
get '/error.json'
expect(last_response.status).to eql 500
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'
expect(last_response.status).to eql 500
expect(last_response.headers['Content-Type']).to eql 'application/xml'
end
it 'includes extension in format' do
subject.get(':id') { params[:format] }
get '/baz.bar'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'bar'
end
it 'does not include extension in id' do
subject.format :json
subject.get(':id') { params }
get '/baz.bar'
expect(last_response.status).to eq 404
end
context 'with a custom content_type' do
before do
subject.content_type :custom, 'application/custom'
subject.formatter :custom, ->(_object, _env) { 'custom' }
subject.get('/custom') { 'bar' }
subject.get('/error') { error!('error in custom', 500) }
end
it 'sets content type' do
get '/custom.custom'
expect(last_response.headers['Content-Type']).to eql 'application/custom'
end
it 'sets content type for error' do
get '/error.custom'
expect(last_response.headers['Content-Type']).to eql 'application/custom'
end
end
context 'env["api.format"]' do
before do
subject.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''#{CGI.escape(filename)}"
params[:file][:tempfile].read
end
end
['/attachment.png', 'attachment'].each do |url|
it "uploads and downloads a PNG file via #{url}" do
image_filename = 'grape.png'
post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true)
expect(last_response.status).to eq(201)
expect(last_response.headers['Content-Type']).to eq('image/png')
expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png")
File.open(image_filename, 'rb') do |io|
expect(last_response.body).to eq io.read
end
end
end
it 'uploads and downloads a Ruby file' do
filename = __FILE__
post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true)
expect(last_response.status).to eq(201)
expect(last_response.headers['Content-Type']).to eq('application/x-ruby')
expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb")
File.open(filename, 'rb') do |io|
expect(last_response.body).to eq io.read
end
end
end
end
context 'custom middleware' do
module ApiSpec
class PhonyMiddleware
def initialize(app, *args)
@args = args
@app = app
@block = block_given? ? true : nil
end
def call(env)
env['phony.args'] ||= []
env['phony.args'] << @args
env['phony.block'] = true if @block
@app.call(env)
end
end
end
describe '.middleware' do
it 'includes middleware arguments from settings' do
subject.use ApiSpec::PhonyMiddleware, 'abc', 123
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 'abc', 123]]
end
it 'includes all middleware from stacked settings' do
subject.use ApiSpec::PhonyMiddleware, 123
subject.use ApiSpec::PhonyMiddleware, 'abc'
subject.use ApiSpec::PhonyMiddleware, 'foo'
expect(subject.middleware).to eql [
[:use, ApiSpec::PhonyMiddleware, 123],
[:use, ApiSpec::PhonyMiddleware, 'abc'],
[:use, ApiSpec::PhonyMiddleware, 'foo']
]
end
end
describe '.use' do
it 'adds middleware' do
subject.use ApiSpec::PhonyMiddleware, 123
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]]
end
it 'does not show up outside the namespace' do
inner_middleware = nil
subject.use ApiSpec::PhonyMiddleware, 123
subject.namespace :awesome do
use ApiSpec::PhonyMiddleware, 'abc'
inner_middleware = middleware
end
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]]
expect(inner_middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123], [:use, ApiSpec::PhonyMiddleware, 'abc']]
end
it 'calls the middleware' do
subject.use ApiSpec::PhonyMiddleware, 'hello'
subject.get '/' do
env['phony.args'].first.first
end
get '/'
expect(last_response.body).to eql 'hello'
end
it 'adds a block if one is given' do
block = -> {}
subject.use ApiSpec::PhonyMiddleware, &block
expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, block]]
end
it 'uses a block if one is given' do
block = -> {}
subject.use ApiSpec::PhonyMiddleware, &block
subject.get '/' do
env['phony.block'].inspect
end
get '/'
expect(last_response.body).to eq('true')
end
it 'does not destroy the middleware settings on multiple runs' do
block = -> {}
subject.use ApiSpec::PhonyMiddleware, &block
subject.get '/' do
env['phony.block'].inspect
end
2.times do
get '/'
expect(last_response.body).to eq('true')
end
end
it 'mounts behind error middleware' do
m = Class.new(Grape::Middleware::Base) do
def before
throw :error, message: 'Caught in the Net', status: 400
end
end
subject.use m
subject.get '/' do
end
get '/'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('Caught in the Net')
end
end
describe '.insert_before' do
it 'runs before a given middleware' do
m = Class.new(Grape::Middleware::Base) do
def call(env)
env['phony.args'] ||= []
env['phony.args'] << @options[:message]
@app.call(env)
end
end
subject.use ApiSpec::PhonyMiddleware, 'hello'
subject.insert_before ApiSpec::PhonyMiddleware, m, message: 'bye'
subject.get '/' do
env['phony.args'].join(' ')
end
get '/'
expect(last_response.body).to eql 'bye hello'
end
end
describe '.insert_after' do
it 'runs after a given middleware' do
m = Class.new(Grape::Middleware::Base) do
def call(env)
env['phony.args'] ||= []
env['phony.args'] << @options[:message]
@app.call(env)
end
end
subject.use ApiSpec::PhonyMiddleware, 'hello'
subject.insert_after ApiSpec::PhonyMiddleware, m, message: 'bye'
subject.get '/' do
env['phony.args'].join(' ')
end
get '/'
expect(last_response.body).to eql 'hello bye'
end
end
end
describe '.http_basic' do
it 'protects any resources on the same scope' do
subject.http_basic do |u, _p|
u == 'allow'
end
subject.get(:hello) { 'Hello, world.' }
get '/hello'
expect(last_response.status).to eql 401
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
expect(last_response.status).to eql 200
end
it 'is scopable' do
subject.get(:hello) { 'Hello, world.' }
subject.namespace :admin do
http_basic do |u, _p|
u == 'allow'
end
get(:hello) { 'Hello, world.' }
end
get '/hello'
expect(last_response.status).to eql 200
get '/admin/hello'
expect(last_response.status).to eql 401
end
it 'is callable via .auth as well' do
subject.auth :http_basic do |u, _p|
u == 'allow'
end
subject.get(:hello) { 'Hello, world.' }
get '/hello'
expect(last_response.status).to eql 401
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
expect(last_response.status).to eql 200
end
it 'has access to the current endpoint' do
basic_auth_context = nil
subject.http_basic do |u, _p|
basic_auth_context = self
u == 'allow'
end
subject.get(:hello) { 'Hello, world.' }
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
expect(basic_auth_context).to be_a_kind_of(Grape::Endpoint)
end
it 'has access to helper methods' do
subject.helpers do
def authorize(u, p)
u == 'allow' && p == 'whatever'
end
end
subject.http_basic do |u, p|
authorize(u, p)
end
subject.get(:hello) { 'Hello, world.' }
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
expect(last_response.status).to eql 200
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever')
expect(last_response.status).to eql 401
end
it 'can set instance variables accessible to routes' do
subject.http_basic do |u, _p|
@hello = 'Hello, world.'
u == 'allow'
end
subject.get(:hello) { @hello }
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
expect(last_response.status).to eql 200
expect(last_response.body).to eql 'Hello, world.'
end
end
describe '.logger' do
subject do
Class.new(Grape::API) do
def self.io
@io ||= StringIO.new
end
logger ::Logger.new(io)
end
end
it 'returns an instance of Logger class by default' do
expect(subject.logger.class).to eql Logger
end
it 'allows setting a custom logger' do
mylogger = Class.new
subject.logger mylogger
expect(mylogger).to receive(:info).exactly(1).times
subject.logger.info 'this will be logged'
end
it 'defaults to a standard logger log format' do
t = Time.at(100)
allow(Time).to receive(:now).and_return(t)
message = "this will be logged\n"
message = "I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : #{message}" if !defined?(Rails) || Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new('4.0')
expect(subject.io).to receive(:write).with(message)
subject.logger.info 'this will be logged'
end
end
describe '.helpers' do
it 'is accessible from the endpoint' do
subject.helpers do
def hello
'Hello, world.'
end
end
subject.get '/howdy' do
hello
end
get '/howdy'
expect(last_response.body).to eql 'Hello, world.'
end
it 'is scopable' do
subject.helpers do
def generic
'always there'
end
end
subject.namespace :admin do
helpers do
def secret
'only in admin'
end
end
get '/secret' do
[generic, secret].join ':'
end
end
subject.get '/generic' do
[generic, respond_to?(:secret)].join ':'
end
get '/generic'
expect(last_response.body).to eql 'always there:false'
get '/admin/secret'
expect(last_response.body).to eql 'always there:only in admin'
end
it 'is reopenable' do
subject.helpers do
def one
1
end
end
subject.helpers do
def two
2
end
end
subject.get 'howdy' do
[one, two]
end
expect { get '/howdy' }.not_to raise_error
end
it 'allows for modules' do
mod = Module.new do
def hello
'Hello, world.'
end
end
subject.helpers mod
subject.get '/howdy' do
hello
end
get '/howdy'
expect(last_response.body).to eql 'Hello, world.'
end
it 'allows multiple calls with modules and blocks' do
subject.helpers Module.new do
def one
1
end
end
subject.helpers Module.new do
def two
2
end
end
subject.helpers do
def three
3
end
end
subject.get 'howdy' do
[one, two, three]
end
expect { get '/howdy' }.not_to raise_error
end
end
describe '.scope' do
# TODO: refactor this to not be tied to versioning. How about a generic
# .setting macro?
it 'scopes the various settings' do
subject.prefix 'new'
subject.scope :legacy do
prefix 'legacy'
get '/abc' do
'abc'
end
end
subject.get '/def' do
'def'
end
get '/new/abc'
expect(last_response.status).to eql 404
get '/legacy/abc'
expect(last_response.status).to eql 200
get '/legacy/def'
expect(last_response.status).to eql 404
get '/new/def'
expect(last_response.status).to eql 200
end
end
describe '.rescue_from' do
it 'does not rescue errors when rescue_from is not set' do
subject.get '/exception' do
raise 'rain!'
end
expect { get '/exception' }.to raise_error(RuntimeError, 'rain!')
end
it 'uses custom helpers defined by using #helpers method' do
subject.helpers do
def custom_error!(name)
error! "hello #{name}"
end
end
subject.rescue_from(ArgumentError) { custom_error! :bob }
subject.get '/custom_error' do
raise ArgumentError
end
get '/custom_error'
expect(last_response.body).to eq 'hello bob'
end
context 'with multiple apis' do
let(:a) { Class.new(Grape::API) }
let(:b) { Class.new(Grape::API) }
before do
a.helpers do
def foo
error!('foo', 401)
end
end
a.rescue_from(:all) { foo }
a.get { raise 'boo' }
b.helpers do
def foo
error!('bar', 401)
end
end
b.rescue_from(:all) { foo }
b.get { raise 'boo' }
end
it 'avoids polluting global namespace' do
env = Rack::MockRequest.env_for('/')
expect(a.call(env)[2].body).to eq(['foo'])
expect(b.call(env)[2].body).to eq(['bar'])
expect(a.call(env)[2].body).to eq(['foo'])
end
end
it 'rescues all errors if rescue_from :all is called' do
subject.rescue_from :all
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.status).to eql 500
expect(last_response.body).to eq 'rain!'
end
it 'rescues all errors with a json formatter' do
subject.format :json
subject.default_format :json
subject.rescue_from :all
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.status).to eql 500
expect(last_response.body).to eq({ error: 'rain!' }.to_json)
end
it 'rescues only certain errors if rescue_from is called with specific errors' do
subject.rescue_from ArgumentError
subject.get('/rescued') { raise ArgumentError }
subject.get('/unrescued') { raise 'beefcake' }
get '/rescued'
expect(last_response.status).to eql 500
expect { get '/unrescued' }.to raise_error(RuntimeError, 'beefcake')
end
context 'CustomError subclass of Grape::Exceptions::Base' do
before do
module ApiSpec
class CustomError < Grape::Exceptions::Base; end
end
end
it 'does not re-raise exceptions of type Grape::Exceptions::Base' do
subject.get('/custom_exception') { raise ApiSpec::CustomError }
expect { get '/custom_exception' }.not_to raise_error
end
it 'rescues custom grape exceptions' do
subject.rescue_from ApiSpec::CustomError do |e|
rack_response('New Error', e.status)
end
subject.get '/custom_error' do
raise ApiSpec::CustomError, status: 400, message: 'Custom Error'
end
get '/custom_error'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('New Error')
end
end
it 'can rescue exceptions raised in the formatter' do
formatter = double(:formatter)
allow(formatter).to receive(:call) { raise StandardError }
allow(Grape::Formatter).to receive(:formatter_for) { formatter }
subject.rescue_from :all do |_e|
rack_response('Formatter Error', 500)
end
subject.get('/formatter_exception') { 'Hello world' }
get '/formatter_exception'
expect(last_response.status).to eql 500
expect(last_response.body).to eq('Formatter Error')
end
end
describe '.rescue_from klass, block' do
it 'rescues Exception' do
subject.rescue_from RuntimeError do |e|
rack_response("rescued from #{e.message}", 202)
end
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.status).to eql 202
expect(last_response.body).to eq('rescued from rain!')
end
context 'custom errors' do
before do
class ConnectionError < RuntimeError; end
class DatabaseError < RuntimeError; end
class CommunicationError < StandardError; end
end
it 'rescues an error via rescue_from :all' do
subject.rescue_from :all do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/exception' do
raise ConnectionError
end
get '/exception'
expect(last_response.status).to eql 500
expect(last_response.body).to eq('rescued from ConnectionError')
end
it 'rescues a specific error' do
subject.rescue_from ConnectionError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/exception' do
raise ConnectionError
end
get '/exception'
expect(last_response.status).to eql 500
expect(last_response.body).to eq('rescued from ConnectionError')
end
it 'rescues a subclass of an error by default' do
subject.rescue_from RuntimeError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/exception' do
raise ConnectionError
end
get '/exception'
expect(last_response.status).to eql 500
expect(last_response.body).to eq('rescued from ConnectionError')
end
it 'rescues multiple specific errors' do
subject.rescue_from ConnectionError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.rescue_from DatabaseError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/connection' do
raise ConnectionError
end
subject.get '/database' do
raise DatabaseError
end
get '/connection'
expect(last_response.status).to eql 500
expect(last_response.body).to eq('rescued from ConnectionError')
get '/database'
expect(last_response.status).to eql 500
expect(last_response.body).to eq('rescued from DatabaseError')
end
it 'does not rescue a different error' do
subject.rescue_from RuntimeError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/uncaught' do
raise CommunicationError
end
expect { get '/uncaught' }.to raise_error(CommunicationError)
end
end
end
describe '.rescue_from klass, lambda' do
it 'rescues an error with the lambda' do
subject.rescue_from ArgumentError, lambda {
rack_response('rescued with a lambda', 400)
}
subject.get('/rescue_lambda') { raise ArgumentError }
get '/rescue_lambda'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('rescued with a lambda')
end
it 'can execute the lambda with an argument' do
subject.rescue_from ArgumentError, lambda { |e|
rack_response(e.message, 400)
}
subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' }
get '/rescue_lambda'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('lambda takes an argument')
end
end
describe '.rescue_from klass, with: :method_name' do
it 'rescues an error with the specified method name' do
subject.helpers do
def rescue_arg_error
error!('500 ArgumentError', 500)
end
def rescue_no_method_error
error!('500 NoMethodError', 500)
end
end
subject.rescue_from ArgumentError, with: :rescue_arg_error
subject.rescue_from NoMethodError, with: :rescue_no_method_error
subject.get('/rescue_arg_error') { raise ArgumentError }
subject.get('/rescue_no_method_error') { raise NoMethodError }
get '/rescue_arg_error'
expect(last_response.status).to eq(500)
expect(last_response.body).to eq('500 ArgumentError')
get '/rescue_no_method_error'
expect(last_response.status).to eq(500)
expect(last_response.body).to eq('500 NoMethodError')
end
it 'aborts if the specified method name does not exist' do
subject.rescue_from :all, with: :not_exist_method
subject.get('/rescue_method') { raise StandardError }
expect { get '/rescue_method' }.to raise_error(NoMethodError, 'undefined method `not_exist_method\'')
end
it 'correctly chooses exception handler if :all handler is specified' do
subject.helpers do
def rescue_arg_error
error!('500 ArgumentError', 500)
end
def rescue_all_errors
error!('500 AnotherError', 500)
end
end
subject.rescue_from ArgumentError, with: :rescue_arg_error
subject.rescue_from :all, with: :rescue_all_errors
subject.get('/argument_error') { raise ArgumentError }
subject.get('/another_error') { raise NoMethodError }
get '/argument_error'
expect(last_response.status).to eq(500)
expect(last_response.body).to eq('500 ArgumentError')
get '/another_error'
expect(last_response.status).to eq(500)
expect(last_response.body).to eq('500 AnotherError')
end
end
describe '.rescue_from klass, rescue_subclasses: boolean' do
before do
module ApiSpec
module APIErrors
class ParentError < StandardError; end
class ChildError < ParentError; end
end
end
end
it 'rescues error as well as subclass errors with rescue_subclasses option set' do
subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: true do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/caught_child' do
raise ApiSpec::APIErrors::ChildError
end
subject.get '/caught_parent' do
raise ApiSpec::APIErrors::ParentError
end
subject.get '/uncaught_parent' do
raise StandardError
end
get '/caught_child'
expect(last_response.status).to eql 500
get '/caught_parent'
expect(last_response.status).to eql 500
expect { get '/uncaught_parent' }.to raise_error(StandardError)
end
it 'sets rescue_subclasses to true by default' do
subject.rescue_from ApiSpec::APIErrors::ParentError do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/caught_child' do
raise ApiSpec::APIErrors::ChildError
end
get '/caught_child'
expect(last_response.status).to eql 500
end
it 'does not rescue child errors if rescue_subclasses is false' do
subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: false do |e|
rack_response("rescued from #{e.class.name}", 500)
end
subject.get '/uncaught' do
raise ApiSpec::APIErrors::ChildError
end
expect { get '/uncaught' }.to raise_error(ApiSpec::APIErrors::ChildError)
end
end
describe '.error_format' do
it 'rescues all errors and return :txt' do
subject.rescue_from :all
subject.format :txt
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.body).to eql 'rain!'
end
it 'rescues all errors and return :txt with backtrace' do
subject.rescue_from :all, backtrace: true
subject.format :txt
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.body.start_with?("rain!\r\n")).to be true
end
it 'rescues all errors with a default formatter' do
subject.default_format :foo
subject.content_type :foo, 'text/foo'
subject.rescue_from :all
subject.get '/exception' do
raise 'rain!'
end
get '/exception.foo'
expect(last_response.body).to start_with 'rain!'
end
it 'defaults the error formatter to format' do
subject.format :json
subject.rescue_from :all
subject.content_type :json, 'application/json'
subject.content_type :foo, 'text/foo'
subject.get '/exception' do
raise 'rain!'
end
get '/exception.json'
expect(last_response.body).to eq('{"error":"rain!"}')
get '/exception.foo'
expect(last_response.body).to eq('{"error":"rain!"}')
end
context 'class' do
before :each do
module ApiSpec
class CustomErrorFormatter
def self.call(message, _backtrace, _options, _env, _original_exception)
"message: #{message} @backtrace"
end
end
end
end
it 'returns a custom error format' do
subject.rescue_from :all, backtrace: true
subject.error_formatter :txt, ApiSpec::CustomErrorFormatter
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.body).to eq('message: rain! @backtrace')
end
end
describe 'with' do
context 'class' do
before :each do
module ApiSpec
class CustomErrorFormatter
def self.call(message, _backtrace, _option, _env, _original_exception)
"message: #{message} @backtrace"
end
end
end
end
it 'returns a custom error format' do
subject.rescue_from :all, backtrace: true
subject.error_formatter :txt, with: ApiSpec::CustomErrorFormatter
subject.get('/exception') { raise 'rain!' }
get '/exception'
expect(last_response.body).to eq('message: rain! @backtrace')
end
end
end
it 'rescues all errors and return :json' do
subject.rescue_from :all
subject.format :json
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.body).to eql '{"error":"rain!"}'
end
it 'rescues all errors and return :json with backtrace' do
subject.rescue_from :all, backtrace: true
subject.format :json
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
json = ::Grape::Json.load(last_response.body)
expect(json['error']).to eql 'rain!'
expect(json['backtrace'].length).to be > 0
end
it 'rescues error! and return txt' do
subject.format :txt
subject.get '/error' do
error!('Access Denied', 401)
end
get '/error'
expect(last_response.body).to eql 'Access Denied'
end
context 'with json format' do
before { subject.format :json }
it 'rescues error! called with a string and returns json' do
subject.get('/error') { error!(:failure, 401) }
end
it 'rescues error! called with a symbol and returns json' do
subject.get('/error') { error!(:failure, 401) }
end
it 'rescues error! called with a hash and returns json' do
subject.get('/error') { error!({ error: :failure }, 401) }
end
after do
get '/error'
expect(last_response.body).to eql('{"error":"failure"}')
end
end
end
describe '.content_type' do
it 'sets additional content-type' do
subject.content_type :xls, 'application/vnd.ms-excel'
subject.get :excel do
'some binary content'
end
get '/excel.xls'
expect(last_response.content_type).to eq('application/vnd.ms-excel')
end
it 'allows to override content-type' do
subject.get :content do
content_type 'text/javascript'
'var x = 1;'
end
get '/content'
expect(last_response.content_type).to eq('text/javascript')
end
it 'removes existing content types' do
subject.content_type :xls, 'application/vnd.ms-excel'
subject.get :excel do
'some binary content'
end
get '/excel.json'
expect(last_response.status).to eq(406)
expect(last_response.body).to eq("The requested format 'txt' is not supported.")
end
end
describe '.formatter' do
context 'multiple formatters' do
before :each do
subject.formatter :json, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" }
subject.formatter :txt, ->(object, _env) { "custom_formatter: #{object[:some]}" }
subject.get :simple do
{ some: 'hash' }
end
end
it 'sets one formatter' do
get '/simple.json'
expect(last_response.body).to eql '{"custom_formatter":"hash"}'
end
it 'sets another formatter' do
get '/simple.txt'
expect(last_response.body).to eql 'custom_formatter: hash'
end
end
context 'custom formatter' do
before :each do
subject.content_type :json, 'application/json'
subject.content_type :custom, 'application/custom'
subject.formatter :custom, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" }
subject.get :simple do
{ some: 'hash' }
end
end
it 'uses json' do
get '/simple.json'
expect(last_response.body).to eql '{"some":"hash"}'
end
it 'uses custom formatter' do
get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
expect(last_response.body).to eql '{"custom_formatter":"hash"}'
end
end
context 'custom formatter class' do
module ApiSpec
module CustomFormatter
def self.call(object, _env)
"{\"custom_formatter\":\"#{object[:some]}\"}"
end
end
end
before :each do
subject.content_type :json, 'application/json'
subject.content_type :custom, 'application/custom'
subject.formatter :custom, ApiSpec::CustomFormatter
subject.get :simple do
{ some: 'hash' }
end
end
it 'uses json' do
get '/simple.json'
expect(last_response.body).to eql '{"some":"hash"}'
end
it 'uses custom formatter' do
get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
expect(last_response.body).to eql '{"custom_formatter":"hash"}'
end
end
end
describe '.parser' do
it 'parses data in format requested by content-type' do
subject.format :json
subject.post '/data' do
{ x: params[:x] }
end
post '/data', '{"x":42}', 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq('{"x":42}')
end
context 'lambda parser' do
before :each do
subject.content_type :txt, 'text/plain'
subject.content_type :custom, 'text/custom'
subject.parser :custom, ->(object, _env) { { object.to_sym => object.to_s.reverse } }
subject.put :simple do
params[:simple]
end
end
['text/custom', 'text/custom; charset=UTF-8'].each do |content_type|
it "uses parser for #{content_type}" do
put '/simple', 'simple', 'CONTENT_TYPE' => content_type
expect(last_response.status).to eq(200)
expect(last_response.body).to eql 'elpmis'
end
end
end
context 'custom parser class' do
module ApiSpec
module CustomParser
def self.call(object, _env)
{ object.to_sym => object.to_s.reverse }
end
end
end
before :each do
subject.content_type :txt, 'text/plain'
subject.content_type :custom, 'text/custom'
subject.parser :custom, ApiSpec::CustomParser
subject.put :simple do
params[:simple]
end
end
it 'uses custom parser' do
put '/simple', 'simple', 'CONTENT_TYPE' => 'text/custom'
expect(last_response.status).to eq(200)
expect(last_response.body).to eql 'elpmis'
end
end
if Object.const_defined? :MultiXml
context 'multi_xml' do
it "doesn't parse yaml" do
subject.put :yaml do
params[:tag]
end
put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.status).to eq(400)
expect(last_response.body).to eql 'Disallowed type attribute: "symbol"'
end
end
else
context 'default xml parser' do
it 'parses symbols' do
subject.put :yaml do
params[:tag]
end
put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.status).to eq(200)
expect(last_response.body).to eql '{"type"=>"symbol", "__content__"=>"a123"}'
end
end
end
context 'none parser class' do
before :each do
subject.parser :json, nil
subject.put 'data' do
"body: #{env['api.request.body']}"
end
end
it 'does not parse data' do
put '/data', 'not valid json', 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('body: not valid json')
end
end
end
describe '.default_format' do
before :each do
subject.format :json
subject.default_format :json
end
it 'returns data in default format' do
subject.get '/data' do
{ x: 42 }
end
get '/data'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('{"x":42}')
end
it 'parses data in default format' do
subject.post '/data' do
{ x: params[:x] }
end
post '/data', '{"x":42}', 'CONTENT_TYPE' => ''
expect(last_response.status).to eq(201)
expect(last_response.body).to eq('{"x":42}')
end
end
describe '.default_error_status' do
it 'allows setting default_error_status' do
subject.rescue_from :all
subject.default_error_status 200
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.status).to eql 200
end
it 'has a default error status' do
subject.rescue_from :all
subject.get '/exception' do
raise 'rain!'
end
get '/exception'
expect(last_response.status).to eql 500
end
it 'uses the default error status in error!' do
subject.rescue_from :all
subject.default_error_status 400
subject.get '/exception' do
error! 'rain!'
end
get '/exception'
expect(last_response.status).to eql 400
end
end
context 'http_codes' do
let(:error_presenter) do
Class.new(Grape::Entity) do
expose :code
expose :static
def static
'some static text'
end
end
end
it 'is used as presenter' do
subject.desc 'some desc', http_codes: [
[408, 'Unauthorized', error_presenter]
]
subject.get '/exception' do
error!({ code: 408 }, 408)
end
get '/exception'
expect(last_response.status).to eql 408
expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
end
it 'presented with' do
error = { code: 408, with: error_presenter }.freeze
subject.get '/exception' do
error! error, 408
end
get '/exception'
expect(last_response.status).to eql 408
expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
end
end
context 'routes' do
describe 'empty api structure' do
it 'returns an empty array of routes' do
expect(subject.routes).to eq([])
end
end
describe 'single method api structure' do
before(:each) do
subject.get :ping do
'pong'
end
end
it 'returns one route' do
expect(subject.routes.size).to eq(1)
route = subject.routes[0]
expect(route.version).to be_nil
expect(route.path).to eq('/ping(.:format)')
expect(route.request_method).to eq('GET')
end
end
describe 'api structure with two versions and a namespace' do
before :each do
subject.version 'v1', using: :path
subject.get 'version' do
api.version
end
# version v2
subject.version 'v2', using: :path
subject.prefix 'p'
subject.namespace 'n1' do
namespace 'n2' do
get 'version' do
api.version
end
end
end
end
it 'returns the latest version set' do
expect(subject.version).to eq('v2')
end
it 'returns versions' do
expect(subject.versions).to eq(%w[v1 v2])
end
it 'sets route paths' do
expect(subject.routes.size).to be >= 2
expect(subject.routes[0].path).to eq('/:version/version(.:format)')
expect(subject.routes[1].path).to eq('/p/:version/n1/n2/version(.:format)')
end
it 'sets route versions' do
expect(subject.routes[0].version).to eq('v1')
expect(subject.routes[1].version).to eq('v2')
end
it 'sets a nested namespace' do
expect(subject.routes[1].namespace).to eq('/n1/n2')
end
it 'sets prefix' do
expect(subject.routes[1].prefix).to eq('p')
end
end
describe 'api structure with additional parameters' do
before(:each) do
subject.params do
requires :token, desc: 'a token'
optional :limit, desc: 'the limit'
end
subject.get 'split/:string' do
params[:string].split(params[:token], (params[:limit] || 0).to_i)
end
end
it 'splits a string' do
get '/split/a,b,c.json', token: ','
expect(last_response.body).to eq('["a","b","c"]')
end
it 'splits a string with limit' do
get '/split/a,b,c.json', token: ',', limit: '2'
expect(last_response.body).to eq('["a","b,c"]')
end
it 'sets params' do
expect(subject.routes.map do |route|
{ params: route.params }
end).to eq [
{
params: {
'string' => '',
'token' => { required: true, desc: 'a token' },
'limit' => { required: false, desc: 'the limit' }
}
}
]
end
end
describe 'api structure with multiple apis' do
before(:each) do
subject.params do
requires :one, desc: 'a token'
optional :two, desc: 'the limit'
end
subject.get 'one' do
end
subject.params do
requires :three, desc: 'a token'
optional :four, desc: 'the limit'
end
subject.get 'two' do
end
end
it 'sets params' do
expect(subject.routes.map do |route|
{ params: route.params }
end).to eq [
{
params: {
'one' => { required: true, desc: 'a token' },
'two' => { required: false, desc: 'the limit' }
}
},
{
params: {
'three' => { required: true, desc: 'a token' },
'four' => { required: false, desc: 'the limit' }
}
}
]
end
end
describe 'api structure with an api without params' do
before(:each) do
subject.params do
requires :one, desc: 'a token'
optional :two, desc: 'the limit'
end
subject.get 'one' do
end
subject.get 'two' do
end
end
it 'sets params' do
expect(subject.routes.map do |route|
{ params: route.params }
end).to eq [
{
params: {
'one' => { required: true, desc: 'a token' },
'two' => { required: false, desc: 'the limit' }
}
},
{
params: {}
}
]
end
end
describe 'api with a custom route setting' do
before(:each) do
subject.route_setting :custom, key: 'value'
subject.get 'one'
end
it 'exposed' do
expect(subject.routes.count).to eq 1
route = subject.routes.first
expect(route.settings[:custom]).to eq(key: 'value')
end
end
describe 'status' do
it 'can be set to arbitrary Integer value' do
subject.get '/foo' do
status 210
end
get '/foo'
expect(last_response.status).to eq 210
end
it 'can be set with a status code symbol' do
subject.get '/foo' do
status :see_other
end
get '/foo'
expect(last_response.status).to eq 303
end
end
end
context 'desc' do
it 'empty array of routes' do
expect(subject.routes).to eq([])
end
it 'empty array of routes' do
subject.desc 'grape api'
expect(subject.routes).to eq([])
end
it 'describes a method' do
subject.desc 'first method'
subject.get :first
expect(subject.routes.length).to eq(1)
route = subject.routes.first
expect(route.description).to eq('first method')
expect(route.route_foo).to be_nil
expect(route.params).to eq({})
expect(route.options).to be_a_kind_of(Hash)
end
it 'has params which does not include format and version as named captures' do
subject.version :v1, using: :path
subject.get :first
param_keys = subject.routes.first.params.keys
expect(param_keys).not_to include('format')
expect(param_keys).not_to include('version')
end
it 'describes methods separately' do
subject.desc 'first method'
subject.get :first
subject.desc 'second method'
subject.get :second
expect(subject.routes.count).to eq(2)
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: 'first method', params: {} },
{ description: 'second method', params: {} }
]
end
it 'resets desc' do
subject.desc 'first method'
subject.get :first
subject.get :second
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: 'first method', params: {} },
{ description: nil, params: {} }
]
end
it 'namespaces and describe arbitrary parameters' do
subject.namespace 'ns' do
desc 'ns second', foo: 'bar'
get 'second'
end
expect(subject.routes.map do |route|
{ description: route.description, foo: route.route_foo, params: route.params }
end).to eq [
{ description: 'ns second', foo: 'bar', params: {} }
]
end
it 'includes details' do
subject.desc 'method', details: 'method details'
subject.get 'method'
expect(subject.routes.map do |route|
{ description: route.description, details: route.details, params: route.params }
end).to eq [
{ description: 'method', details: 'method details', params: {} }
]
end
it 'describes a method with parameters' do
subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } }
subject.get 'reverse' do
params[:s].reverse
end
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } }
]
end
it 'does not inherit param descriptions in consequent namespaces' do
subject.desc 'global description'
subject.params do
requires :param1
optional :param2
end
subject.namespace 'ns1' do
get { ; }
end
subject.params do
optional :param2
end
subject.namespace 'ns2' do
get { ; }
end
routes_doc = subject.routes.map do |route|
{ description: route.description, params: route.params }
end
expect(routes_doc).to eq [
{ description: 'global description',
params: {
'param1' => { required: true },
'param2' => { required: false }
} },
{ description: 'global description',
params: {
'param2' => { required: false }
} }
]
end
it 'merges the parameters of the namespace with the parameters of the method' do
subject.desc 'namespace'
subject.params do
requires :ns_param, desc: 'namespace parameter'
end
subject.namespace 'ns' do
desc 'method'
params do
optional :method_param, desc: 'method parameter'
end
get 'method'
end
routes_doc = subject.routes.map do |route|
{ description: route.description, params: route.params }
end
expect(routes_doc).to eq [
{ description: 'method',
params: {
'ns_param' => { required: true, desc: 'namespace parameter' },
'method_param' => { required: false, desc: 'method parameter' }
} }
]
end
it 'merges the parameters of nested namespaces' do
subject.desc 'ns1'
subject.params do
optional :ns_param, desc: 'ns param 1'
requires :ns1_param, desc: 'ns1 param'
end
subject.namespace 'ns1' do
desc 'ns2'
params do
requires :ns_param, desc: 'ns param 2'
requires :ns2_param, desc: 'ns2 param'
end
namespace 'ns2' do
desc 'method'
params do
optional :method_param, desc: 'method param'
end
get 'method'
end
end
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: 'method',
params: {
'ns_param' => { required: true, desc: 'ns param 2' },
'ns1_param' => { required: true, desc: 'ns1 param' },
'ns2_param' => { required: true, desc: 'ns2 param' },
'method_param' => { required: false, desc: 'method param' }
} }
]
end
it 'groups nested params and prevents overwriting of params with same name in different groups' do
subject.desc 'method'
subject.params do
group :group1, type: Array do
optional :param1, desc: 'group1 param1 desc'
requires :param2, desc: 'group1 param2 desc'
end
group :group2, type: Array do
optional :param1, desc: 'group2 param1 desc'
requires :param2, desc: 'group2 param2 desc'
end
end
subject.get 'method'
expect(subject.routes.map(&:params)).to eq [{
'group1' => { required: true, type: 'Array' },
'group1[param1]' => { required: false, desc: 'group1 param1 desc' },
'group1[param2]' => { required: true, desc: 'group1 param2 desc' },
'group2' => { required: true, type: 'Array' },
'group2[param1]' => { required: false, desc: 'group2 param1 desc' },
'group2[param2]' => { required: true, desc: 'group2 param2 desc' }
}]
end
it 'uses full name of parameters in nested groups' do
subject.desc 'nesting'
subject.params do
requires :root_param, desc: 'root param'
group :nested, type: Array do
requires :nested_param, desc: 'nested param'
end
end
subject.get 'method'
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: 'nesting',
params: {
'root_param' => { required: true, desc: 'root param' },
'nested' => { required: true, type: 'Array' },
'nested[nested_param]' => { required: true, desc: 'nested param' }
} }
]
end
it 'allows to set the type attribute on :group element' do
subject.params do
group :foo, type: Array do
optional :bar
end
end
end
it 'parses parameters when no description is given' do
subject.params do
requires :one_param, desc: 'one param'
end
subject.get 'method'
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: nil, params: { 'one_param' => { required: true, desc: 'one param' } } }
]
end
it 'does not symbolize params' do
subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } }
subject.get 'reverse/:s' do
params[:s].reverse
end
expect(subject.routes.map do |route|
{ description: route.description, params: route.params }
end).to eq [
{ description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } }
]
end
end
describe '.mount' do
let(:mounted_app) { ->(_env) { [200, {}, ['MOUNTED']] } }
context 'with a bare rack app' do
before do
subject.mount mounted_app => '/mounty'
end
it 'makes a bare Rack app available at the endpoint' do
get '/mounty'
expect(last_response.body).to eq('MOUNTED')
end
it 'anchors the routes, passing all subroutes to it' do
get '/mounty/awesome'
expect(last_response.body).to eq('MOUNTED')
end
it 'is able to cascade' do
subject.mount lambda { |env|
headers = {}
headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
[200, headers, ['Farfegnugen']]
} => '/'
get '/boo'
expect(last_response.body).to eq('Farfegnugen')
get '/mounty'
expect(last_response.body).to eq('MOUNTED')
end
end
context 'without a hash' do
it 'calls through setting the route to "/"' do
subject.mount mounted_app
get '/'
expect(last_response.body).to eq('MOUNTED')
end
end
context 'mounting an API' do
it 'applies the settings of the mounting api' do
subject.version 'v1', using: :path
subject.namespace :cool do
app = Class.new(Grape::API)
app.get('/awesome') do
'yo'
end
mount app
end
get '/v1/cool/awesome'
expect(last_response.body).to eq('yo')
end
it 'applies the settings to nested mounted apis' do
subject.version 'v1', using: :path
subject.namespace :cool do
inner_app = Class.new(Grape::API)
inner_app.get('/awesome') do
'yo'
end
app = Class.new(Grape::API)
app.mount inner_app
mount app
end
get '/v1/cool/awesome'
expect(last_response.body).to eq('yo')
end
context 'when some rescues are defined by mounted' do
it 'inherits parent rescues' do
subject.rescue_from :all do |e|
rack_response("rescued from #{e.message}", 202)
end
app = Class.new(Grape::API)
subject.namespace :mounted do
app.rescue_from ArgumentError
app.get('/fail') { raise 'doh!' }
mount app
end
get '/mounted/fail'
expect(last_response.status).to eql 202
expect(last_response.body).to eq('rescued from doh!')
end
it 'prefers rescues defined by mounted if they rescue similar error class' do
subject.rescue_from StandardError do
rack_response('outer rescue')
end
app = Class.new(Grape::API)
subject.namespace :mounted do
rescue_from StandardError do
rack_response('inner rescue')
end
app.get('/fail') { raise 'doh!' }
mount app
end
get '/mounted/fail'
expect(last_response.body).to eq('inner rescue')
end
it 'prefers rescues defined by mounted even if outer is more specific' do
subject.rescue_from ArgumentError do
rack_response('outer rescue')
end
app = Class.new(Grape::API)
subject.namespace :mounted do
rescue_from StandardError do
rack_response('inner rescue')
end
app.get('/fail') { raise ArgumentError.new }
mount app
end
get '/mounted/fail'
expect(last_response.body).to eq('inner rescue')
end
it 'prefers more specific rescues defined by mounted' do
subject.rescue_from StandardError do
rack_response('outer rescue')
end
app = Class.new(Grape::API)
subject.namespace :mounted do
rescue_from ArgumentError do
rack_response('inner rescue')
end
app.get('/fail') { raise ArgumentError.new }
mount app
end
get '/mounted/fail'
expect(last_response.body).to eq('inner rescue')
end
end
it 'collects the routes of the mounted api' do
subject.namespace :cool do
app = Class.new(Grape::API)
app.get('/awesome') {}
app.post('/sauce') {}
mount app
end
expect(subject.routes.size).to eq(2)
expect(subject.routes.first.path).to match(%r{\/cool\/awesome})
expect(subject.routes.last.path).to match(%r{\/cool\/sauce})
end
it 'mounts on a path' do
subject.namespace :cool do
app = Class.new(Grape::API)
app.get '/awesome' do
'sauce'
end
mount app => '/mounted'
end
get '/mounted/cool/awesome'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('sauce')
end
it 'mounts on a nested path' do
APP1 = Class.new(Grape::API)
APP2 = Class.new(Grape::API)
APP2.get '/nice' do
'play'
end
# note that the reverse won't work, mount from outside-in
APP3 = subject
APP3.mount APP1 => '/app1'
APP1.mount APP2 => '/app2'
get '/app1/app2/nice'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('play')
options '/app1/app2/nice'
expect(last_response.status).to eq(204)
end
it 'responds to options' do
app = Class.new(Grape::API)
app.get '/colour' do
'red'
end
app.namespace :pears do
get '/colour' do
'green'
end
end
subject.namespace :apples do
mount app
end
get '/apples/colour'
expect(last_response.status).to eql 200
expect(last_response.body).to eq('red')
options '/apples/colour'
expect(last_response.status).to eql 204
get '/apples/pears/colour'
expect(last_response.status).to eql 200
expect(last_response.body).to eq('green')
options '/apples/pears/colour'
expect(last_response.status).to eql 204
end
it 'responds to options with path versioning' do
subject.version 'v1', using: :path
subject.namespace :apples do
app = Class.new(Grape::API)
app.get('/colour') do
'red'
end
mount app
end
get '/v1/apples/colour'
expect(last_response.status).to eql 200
expect(last_response.body).to eq('red')
options '/v1/apples/colour'
expect(last_response.status).to eql 204
end
it 'mounts a versioned API with nested resources' do
api = Class.new(Grape::API) do
version 'v1'
resources :users do
get :hello do
'hello users'
end
end
end
subject.mount api
get '/v1/users/hello'
expect(last_response.body).to eq('hello users')
end
it 'mounts a prefixed API with nested resources' do
api = Class.new(Grape::API) do
prefix 'api'
resources :users do
get :hello do
'hello users'
end
end
end
subject.mount api
get '/api/users/hello'
expect(last_response.body).to eq('hello users')
end
it 'applies format to a mounted API with nested resources' do
api = Class.new(Grape::API) do
format :json
resources :users do
get do
{ users: true }
end
end
end
subject.mount api
get '/users'
expect(last_response.body).to eq({ users: true }.to_json)
end
it 'applies auth to a mounted API with nested resources' do
api = Class.new(Grape::API) do
format :json
http_basic do |username, password|
username == 'username' && password == 'password'
end
resources :users do
get do
{ users: true }
end
end
end
subject.mount api
get '/users'
expect(last_response.status).to eq(401)
get '/users', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('username', 'password')
expect(last_response.body).to eq({ users: true }.to_json)
end
it 'mounts multiple versioned APIs with nested resources' do
api1 = Class.new(Grape::API) do
version 'one', using: :header, vendor: 'test'
resources :users do
get :hello do
'one'
end
end
end
api2 = Class.new(Grape::API) do
version 'two', using: :header, vendor: 'test'
resources :users do
get :hello do
'two'
end
end
end
subject.mount api1
subject.mount api2
versioned_get '/users/hello', 'one', using: :header, vendor: 'test'
expect(last_response.body).to eq('one')
versioned_get '/users/hello', 'two', using: :header, vendor: 'test'
expect(last_response.body).to eq('two')
end
it 'recognizes potential versions with mounted path' do
a = Class.new(Grape::API) do
version :v1, using: :path
get '/hello' do
'hello'
end
end
b = Class.new(Grape::API) do
version :v1, using: :path
get '/world' do
'world'
end
end
subject.mount a => '/one'
subject.mount b => '/two'
get '/one/v1/hello'
expect(last_response.status).to eq 200
get '/two/v1/world'
expect(last_response.status).to eq 200
end
context 'when mounting class extends a subclass of Grape::API' do
it 'mounts APIs with the same superclass' do
base_api = Class.new(Grape::API)
a = Class.new(base_api)
b = Class.new(base_api)
expect { a.mount b }.to_not raise_error
end
end
end
end
describe '.endpoints' do
it 'adds one for each route created' do
subject.get '/'
subject.post '/'
expect(subject.endpoints.size).to eq(2)
end
end
describe '.compile' do
it 'sets the instance' do
expect(subject.instance).to be_nil
subject.compile
expect(subject.instance).to be_kind_of(subject)
end
end
describe '.change!' do
it 'invalidates any compiled instance' do
subject.compile
subject.change!
expect(subject.instance).to be_nil
end
end
describe '.endpoint' do
before(:each) do
subject.format :json
subject.get '/endpoint/options' do
{
path: options[:path],
source_location: source.source_location
}
end
end
it 'path' do
get '/endpoint/options'
options = ::Grape::Json.load(last_response.body)
expect(options['path']).to eq(['/endpoint/options'])
expect(options['source_location'][0]).to include 'api_spec.rb'
expect(options['source_location'][1].to_i).to be > 0
end
end
describe '.route' do
context 'plain' do
before(:each) do
subject.get '/' do
route.path
end
subject.get '/path' do
route.path
end
end
it 'provides access to route info' do
get '/'
expect(last_response.body).to eq('/(.:format)')
get '/path'
expect(last_response.body).to eq('/path(.:format)')
end
end
context 'with desc' do
before(:each) do
subject.desc 'returns description'
subject.get '/description' do
route.description
end
subject.desc 'returns parameters', params: { 'x' => 'y' }
subject.get '/params/:id' do
route.params[params[:id]]
end
end
it 'returns route description' do
get '/description'
expect(last_response.body).to eq('returns description')
end
it 'returns route parameters' do
get '/params/x'
expect(last_response.body).to eq('y')
end
end
end
describe '.format' do
context ':txt' do
before(:each) do
subject.format :txt
subject.content_type :json, 'application/json'
subject.get '/meaning_of_life' do
{ meaning_of_life: 42 }
end
end
it 'forces txt without an extension' do
get '/meaning_of_life'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
it 'does not force txt with an extension' do
get '/meaning_of_life.json'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
end
it 'forces txt from a non-accepting header' do
get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
end
context ':txt only' do
before(:each) do
subject.format :txt
subject.get '/meaning_of_life' do
{ meaning_of_life: 42 }
end
end
it 'forces txt without an extension' do
get '/meaning_of_life'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
it 'accepts specified extension' do
get '/meaning_of_life.txt'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
it 'does not accept extensions other than specified' do
get '/meaning_of_life.json'
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'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
end
context ':json' do
before(:each) do
subject.format :json
subject.content_type :txt, 'text/plain'
subject.get '/meaning_of_life' do
{ meaning_of_life: 42 }
end
end
it 'forces json without an extension' do
get '/meaning_of_life'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
end
it 'does not force json with an extension' do
get '/meaning_of_life.txt'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
it 'forces json from a non-accepting header' do
get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
end
it 'can be overwritten with an explicit content type' do
subject.get '/meaning_of_life_with_content_type' do
content_type 'text/plain'
{ meaning_of_life: 42 }.to_s
end
get '/meaning_of_life_with_content_type'
expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
end
it 'raised :error from middleware' do
middleware = Class.new(Grape::Middleware::Base) do
def before
throw :error, message: 'Unauthorized', status: 42
end
end
subject.use middleware
subject.get do
end
get '/'
expect(last_response.status).to eq(42)
expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json)
end
end
context ':serializable_hash' do
before(:each) do
class SerializableHashExample
def serializable_hash
{ abc: 'def' }
end
end
subject.format :serializable_hash
end
it 'instance' do
subject.get '/example' do
SerializableHashExample.new
end
get '/example'
expect(last_response.body).to eq('{"abc":"def"}')
end
it 'root' do
subject.get '/example' do
{ 'root' => SerializableHashExample.new }
end
get '/example'
expect(last_response.body).to eq('{"root":{"abc":"def"}}')
end
it 'array' do
subject.get '/examples' do
[SerializableHashExample.new, SerializableHashExample.new]
end
get '/examples'
expect(last_response.body).to eq('[{"abc":"def"},{"abc":"def"}]')
end
end
context ':xml' do
before(:each) do
subject.format :xml
end
it 'string' do
subject.get '/example' do
'example'
end
get '/example'
expect(last_response.status).to eq(500)
expect(last_response.body).to eq <<-XML
cannot convert String to xml
XML
end
it 'hash' do
subject.get '/example' do
{
example1: 'example1',
example2: 'example2'
}
end
get '/example'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq <<-XML
example1example2
XML
end
it 'array' do
subject.get '/example' do
%w[example1 example2]
end
get '/example'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq <<-XML
example1example2
XML
end
it 'raised :error from middleware' do
middleware = Class.new(Grape::Middleware::Base) do
def before
throw :error, message: 'Unauthorized', status: 42
end
end
subject.use middleware
subject.get do
end
get '/'
expect(last_response.status).to eq(42)
expect(last_response.body).to eq <<-XML
Unauthorized
XML
end
end
end
context 'catch-all' do
before do
api1 = Class.new(Grape::API)
api1.version 'v1', using: :path
api1.get 'hello' do
'v1'
end
api2 = Class.new(Grape::API)
api2.version 'v2', using: :path
api2.get 'hello' do
'v2'
end
subject.mount api1
subject.mount api2
end
[true, false].each do |anchor|
it "anchor=#{anchor}" do
subject.route :any, '*path', anchor: anchor do
error!("Unrecognized request path: #{params[:path]} - #{env['PATH_INFO']}#{env['SCRIPT_NAME']}", 404)
end
get '/v1/hello'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v1')
get '/v2/hello'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v2')
options '/v2/hello'
expect(last_response.status).to eq(204)
expect(last_response.body).to be_blank
head '/v2/hello'
expect(last_response.status).to eq(200)
expect(last_response.body).to be_blank
get '/foobar'
expect(last_response.status).to eq(404)
expect(last_response.body).to eq('Unrecognized request path: foobar - /foobar')
end
end
end
context 'cascading' do
context 'via version' do
it 'cascades' do
subject.version 'v1', using: :path, cascade: true
get '/v1/hello'
expect(last_response.status).to eq(404)
expect(last_response.headers['X-Cascade']).to eq('pass')
end
it 'does not cascade' do
subject.version 'v2', using: :path, cascade: false
get '/v2/hello'
expect(last_response.status).to eq(404)
expect(last_response.headers.keys).not_to include 'X-Cascade'
end
end
context 'via endpoint' do
it 'cascades' do
subject.cascade true
get '/hello'
expect(last_response.status).to eq(404)
expect(last_response.headers['X-Cascade']).to eq('pass')
end
it 'does not cascade' do
subject.cascade false
get '/hello'
expect(last_response.status).to eq(404)
expect(last_response.headers.keys).not_to include 'X-Cascade'
end
end
end
context 'with json default_error_formatter' do
it 'returns json error' do
subject.content_type :json, 'application/json'
subject.default_error_formatter :json
subject.get '/something' do
'foo'
end
get '/something'
expect(last_response.status).to eq(406)
expect(last_response.body).to eq("{\"error\":\"The requested format 'txt' is not supported.\"}")
end
end
context 'body' do
context 'false' do
before do
subject.get '/blank' do
body false
end
end
it 'returns blank body' do
get '/blank'
expect(last_response.status).to eq(204)
expect(last_response.body).to be_blank
end
end
context 'plain text' do
before do
subject.get '/text' do
content_type 'text/plain'
body 'Hello World'
'ignored'
end
end
it 'returns blank body' do
get '/text'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'Hello World'
end
end
end
end
grape-1.0.2/spec/grape/endpoint_spec.rb 0000644 0000041 0000041 00000134712 13231337007 020056 0 ustar www-data www-data require 'spec_helper'
describe Grape::Endpoint do
subject { Class.new(Grape::API) }
def app
subject
end
describe '.before_each' do
after { Grape::Endpoint.before_each(nil) }
it 'is settable via block' do
block = ->(_endpoint) { 'noop' }
Grape::Endpoint.before_each(&block)
expect(Grape::Endpoint.before_each.first).to eq(block)
end
it 'is settable via reference' do
block = ->(_endpoint) { 'noop' }
Grape::Endpoint.before_each block
expect(Grape::Endpoint.before_each.first).to eq(block)
end
it 'is able to override a helper' do
subject.get('/') { current_user }
expect { get '/' }.to raise_error(NameError)
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:current_user).and_return('Bob')
end
get '/'
expect(last_response.body).to eq('Bob')
Grape::Endpoint.before_each(nil)
expect { get '/' }.to raise_error(NameError)
end
it 'is able to stack helper' do
subject.get('/') do
authenticate_user!
current_user
end
expect { get '/' }.to raise_error(NameError)
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:current_user).and_return('Bob')
end
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:authenticate_user!).and_return(true)
end
get '/'
expect(last_response.body).to eq('Bob')
Grape::Endpoint.before_each(nil)
expect { get '/' }.to raise_error(NameError)
end
end
describe '#initialize' do
it 'takes a settings stack, options, and a block' do
p = proc {}
expect do
Grape::Endpoint.new(Grape::Util::InheritableSetting.new, {
path: '/',
method: :get
}, &p)
end.not_to raise_error
end
end
it 'sets itself in the env upon call' do
subject.get('/') { 'Hello world.' }
get '/'
expect(last_request.env['api.endpoint']).to be_kind_of(Grape::Endpoint)
end
describe '#status' do
it 'is callable from within a block' do
subject.get('/home') do
status 206
'Hello'
end
get '/home'
expect(last_response.status).to eq(206)
expect(last_response.body).to eq('Hello')
end
it 'is set as default to 200 for get' do
memoized_status = nil
subject.get('/home') do
memoized_status = status
'Hello'
end
get '/home'
expect(last_response.status).to eq(200)
expect(memoized_status).to eq(200)
expect(last_response.body).to eq('Hello')
end
it 'is set as default to 201 for post' do
memoized_status = nil
subject.post('/home') do
memoized_status = status
'Hello'
end
post '/home'
expect(last_response.status).to eq(201)
expect(memoized_status).to eq(201)
expect(last_response.body).to eq('Hello')
end
end
describe '#header' do
it 'is callable from within a block' do
subject.get('/hey') do
header 'X-Awesome', 'true'
'Awesome'
end
get '/hey'
expect(last_response.headers['X-Awesome']).to eq('true')
end
end
describe '#headers' do
before do
subject.get('/headers') do
headers.to_json
end
end
it 'includes request headers' do
get '/headers'
expect(JSON.parse(last_response.body)).to eq(
'Host' => 'example.org',
'Cookie' => ''
)
end
it 'includes additional request headers' do
get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1'
expect(JSON.parse(last_response.body)['X-Grape-Client']).to eq('1')
end
it 'includes headers passed as symbols' do
env = Rack::MockRequest.env_for('/headers')
env['HTTP_SYMBOL_HEADER'.to_sym] = 'Goliath passes symbols'
body = subject.call(env)[2].body.first
expect(JSON.parse(body)['Symbol-Header']).to eq('Goliath passes symbols')
end
end
describe '#cookies' do
it 'is callable from within a block' do
subject.get('/get/cookies') do
cookies['my-awesome-cookie1'] = 'is cool'
cookies['my-awesome-cookie2'] = {
value: 'is cool too',
domain: 'my.example.com',
path: '/',
secure: true
}
cookies[:cookie3] = 'symbol'
cookies['cookie4'] = 'secret code here'
end
get('/get/cookies')
expect(last_response.headers['Set-Cookie'].split("\n").sort).to eql [
'cookie3=symbol',
'cookie4=secret+code+here',
'my-awesome-cookie1=is+cool',
'my-awesome-cookie2=is+cool+too; domain=my.example.com; path=/; secure'
]
end
it 'sets browser cookies and does not set response cookies' do
subject.get('/username') do
cookies[:username]
end
get('/username', {}, 'HTTP_COOKIE' => 'username=mrplum; sandbox=true')
expect(last_response.body).to eq('mrplum')
expect(last_response.headers['Set-Cookie']).to be_nil
end
it 'sets and update browser cookies' do
subject.get('/username') do
cookies[:sandbox] = true if cookies[:sandbox] == 'false'
cookies[:username] += '_test'
end
get('/username', {}, 'HTTP_COOKIE' => 'username=user; sandbox=false')
expect(last_response.body).to eq('user_test')
expect(last_response.headers['Set-Cookie']).to match(/username=user_test/)
expect(last_response.headers['Set-Cookie']).to match(/sandbox=true/)
end
it 'deletes cookie' do
subject.get('/test') do
sum = 0
cookies.each do |name, val|
sum += val.to_i
cookies.delete name
end
sum
end
get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2'
expect(last_response.body).to eq('3')
cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie|
cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie
[cookie.name, cookie]
end]
expect(cookies.size).to eq(2)
%w[and_this delete_this_cookie].each do |cookie_name|
cookie = cookies[cookie_name]
expect(cookie).not_to be_nil
expect(cookie.value).to eq('deleted')
expect(cookie.expired?).to be true
end
end
it 'deletes cookies with path' do
subject.get('/test') do
sum = 0
cookies.each do |name, val|
sum += val.to_i
cookies.delete name, path: '/test'
end
sum
end
get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2')
expect(last_response.body).to eq('3')
cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie|
cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie
[cookie.name, cookie]
end]
expect(cookies.size).to eq(2)
%w[and_this delete_this_cookie].each do |cookie_name|
cookie = cookies[cookie_name]
expect(cookie).not_to be_nil
expect(cookie.value).to eq('deleted')
expect(cookie.path).to eq('/test')
expect(cookie.expired?).to be true
end
end
end
describe '#params' do
context 'default class' do
it 'should be a ActiveSupport::HashWithIndifferentAccess' do
subject.get '/foo' do
params.class
end
get '/foo'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess')
end
end
context 'sets a value to params' do
it 'params' do
subject.params do
requires :a, type: String
end
subject.get '/foo' do
params[:a] = 'bar'
end
get '/foo', a: 'foo'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('bar')
end
end
end
describe '#declared' do
before do
subject.format :json
subject.params do
requires :first
optional :second
optional :third, default: 'third-default'
optional :nested, type: Hash do
optional :fourth
optional :fifth
optional :nested_two, type: Hash do
optional :sixth
optional :nested_three, type: Hash do
optional :seventh
end
end
end
optional :nested_arr, type: Array do
optional :eighth
end
end
end
context 'when params are not built with default class' do
it 'returns an object that corresponds with the params class - hash with indifferent access' do
subject.params do
build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
end
subject.get '/declared' do
d = declared(params, include_missing: true)
{ declared_class: d.class.to_s }
end
get '/declared?first=present'
expect(JSON.parse(last_response.body)['declared_class']).to eq('ActiveSupport::HashWithIndifferentAccess')
end
it 'returns an object that corresponds with the params class - hashie mash' do
subject.params do
build_with Grape::Extensions::Hashie::Mash::ParamBuilder
end
subject.get '/declared' do
d = declared(params, include_missing: true)
{ declared_class: d.class.to_s }
end
get '/declared?first=present'
expect(JSON.parse(last_response.body)['declared_class']).to eq('Hashie::Mash')
end
it 'returns an object that corresponds with the params class - hash' do
subject.params do
build_with Grape::Extensions::Hash::ParamBuilder
end
subject.get '/declared' do
d = declared(params, include_missing: true)
{ declared_class: d.class.to_s }
end
get '/declared?first=present'
expect(JSON.parse(last_response.body)['declared_class']).to eq('Hash')
end
end
it 'should show nil for nested params if include_missing is true' do
subject.get '/declared' do
declared(params, include_missing: true)
end
get '/declared?first=present'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil
end
it 'does not work in a before filter' do
subject.before do
declared(params)
end
subject.get('/declared') { declared(params) }
expect { get('/declared') }.to raise_error(
Grape::DSL::InsideRoute::MethodNotYetAvailable
)
end
it 'has as many keys as there are declared params' do
subject.get '/declared' do
declared(params)
end
get '/declared?first=present'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body).keys.size).to eq(5)
end
it 'has a optional param with default value all the time' do
subject.get '/declared' do
declared(params)
end
get '/declared?first=one'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['third']).to eql('third-default')
end
it 'builds nested params' do
subject.get '/declared' do
declared(params)
end
get '/declared?first=present&nested[fourth]=1'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 3
end
it 'builds nested params when given array' do
subject.get '/dummy' do
end
subject.params do
requires :first
optional :second
optional :third, default: 'third-default'
optional :nested, type: Array do
optional :fourth
end
end
subject.get '/declared' do
declared(params)
end
get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['nested'].size).to eq 2
end
context 'sets nested objects when the param is missing' do
it 'to be a hash when include_missing is true' do
subject.get '/declared' do
declared(params, include_missing: true)
end
get '/declared?first=present'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['nested']).to be_a(Hash)
end
it 'to be an array when include_missing is true' do
subject.get '/declared' do
declared(params, include_missing: true)
end
get '/declared?first=present'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['nested_arr']).to be_a(Array)
end
it 'to be nil when include_missing is false' do
subject.get '/declared' do
declared(params, include_missing: false)
end
get '/declared?first=present'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['nested']).to be_nil
end
end
it 'filters out any additional params that are given' do
subject.get '/declared' do
declared(params)
end
get '/declared?first=one&other=two'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body).key?(:other)).to eq false
end
it 'stringifies if that option is passed' do
subject.get '/declared' do
declared(params, stringify: true)
end
get '/declared?first=one&other=two'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['first']).to eq 'one'
end
it 'does not include missing attributes if that option is passed' do
subject.get '/declared' do
error! 400, 'expected nil' if declared(params, include_missing: false)[:second]
''
end
get '/declared?first=one&other=two'
expect(last_response.status).to eq(200)
end
it 'includes attributes with value that evaluates to false' do
subject.params do
requires :first
optional :boolean
end
subject.post '/declared' do
error!('expected false', 400) if declared(params, include_missing: false)[:boolean] != false
''
end
post '/declared', ::Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
end
it 'includes attributes with value that evaluates to nil' do
subject.params do
requires :first
optional :second
end
subject.post '/declared' do
error!('expected nil', 400) unless declared(params, include_missing: false)[:second].nil?
''
end
post '/declared', ::Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
end
it 'includes missing attributes with defaults when there are nested hashes' do
subject.get '/dummy' do
end
subject.params do
requires :first
optional :second
optional :third, default: nil
optional :nested, type: Hash do
optional :fourth, default: nil
optional :fifth, default: nil
requires :nested_nested, type: Hash do
optional :sixth, default: 'sixth-default'
optional :seven, default: nil
end
end
end
subject.get '/declared' do
declared(params, include_missing: false)
end
get '/declared?first=present&nested[fourth]=&nested[nested_nested][sixth]=sixth'
json = JSON.parse(last_response.body)
expect(last_response.status).to eq(200)
expect(json['first']).to eq 'present'
expect(json['nested'].keys).to eq %w[fourth fifth nested_nested]
expect(json['nested']['fourth']).to eq ''
expect(json['nested']['nested_nested'].keys).to eq %w[sixth seven]
expect(json['nested']['nested_nested']['sixth']).to eq 'sixth'
end
it 'does not include missing attributes when there are nested hashes' do
subject.get '/dummy' do
end
subject.params do
requires :first
optional :second
optional :third
optional :nested, type: Hash do
optional :fourth
optional :fifth
end
end
subject.get '/declared' do
declared(params, include_missing: false)
end
get '/declared?first=present&nested[fourth]=4'
json = JSON.parse(last_response.body)
expect(last_response.status).to eq(200)
expect(json['first']).to eq 'present'
expect(json['nested'].keys).to eq %w[fourth]
expect(json['nested']['fourth']).to eq '4'
end
end
describe '#declared; call from child namespace' do
before do
subject.format :json
subject.namespace :parent do
params do
requires :parent_name, type: String
end
namespace ':parent_name' do
params do
requires :child_name, type: String
requires :child_age, type: Integer
end
namespace ':child_name' do
params do
requires :grandchild_name, type: String
end
get ':grandchild_name' do
{
'params' => params,
'without_parent_namespaces' => declared(params, include_parent_namespaces: false),
'with_parent_namespaces' => declared(params, include_parent_namespaces: true)
}
end
end
end
end
get '/parent/foo/bar/baz', child_age: 5, extra: 'hello'
end
let(:parsed_response) { JSON.parse(last_response.body, symbolize_names: true) }
it { expect(last_response.status).to eq 200 }
context 'with include_parent_namespaces: false' do
it 'returns declared parameters only from current namespace' do
expect(parsed_response[:without_parent_namespaces]).to eq(
grandchild_name: 'baz'
)
end
end
context 'with include_parent_namespaces: true' do
it 'returns declared parameters from every parent namespace' do
expect(parsed_response[:with_parent_namespaces]).to eq(
parent_name: 'foo',
child_name: 'bar',
grandchild_name: 'baz',
child_age: 5
)
end
end
context 'without declaration' do
it 'returns all requested parameters' do
expect(parsed_response[:params]).to eq(
parent_name: 'foo',
child_name: 'bar',
grandchild_name: 'baz',
child_age: 5,
extra: 'hello'
)
end
end
end
describe '#declared; from a nested mounted endpoint' do
before do
doubly_mounted = Class.new(Grape::API)
doubly_mounted.namespace :more do
params do
requires :y, type: Integer
end
route_param :y do
get do
{
params: params,
declared_params: declared(params)
}
end
end
end
mounted = Class.new(Grape::API)
mounted.namespace :another do
params do
requires :mount_space, type: Integer
end
route_param :mount_space do
mount doubly_mounted
end
end
subject.format :json
subject.namespace :something do
params do
requires :id, type: Integer
end
resource ':id' do
mount mounted
end
end
end
it 'can access parent attributes' do
get '/something/123/another/456/more/789'
expect(last_response.status).to eq 200
json = JSON.parse(last_response.body, symbolize_names: true)
# test all three levels of params
expect(json[:declared_params][:y]).to eq 789
expect(json[:declared_params][:mount_space]).to eq 456
expect(json[:declared_params][:id]).to eq 123
end
end
describe '#declared; mixed nesting' do
before do
subject.format :json
subject.resource :users do
route_param :id, type: Integer, desc: 'ID desc' do
# Adding this causes route_setting(:declared_params) to be nil for the
# get block in namespace 'foo' below
get do
end
namespace 'foo' do
get do
{
params: params,
declared_params: declared(params),
declared_params_no_parent: declared(params, include_parent_namespaces: false)
}
end
end
end
end
end
it 'can access parent route_param' do
get '/users/123/foo', bar: 'bar'
expect(last_response.status).to eq 200
json = JSON.parse(last_response.body, symbolize_names: true)
expect(json[:declared_params][:id]).to eq 123
expect(json[:declared_params_no_parent][:id]).to eq nil
end
end
describe '#declared; with multiple route_param' do
before do
mounted = Class.new(Grape::API)
mounted.namespace :albums do
get do
declared(params)
end
end
subject.format :json
subject.namespace :artists do
route_param :id, type: Integer do
get do
declared(params)
end
params do
requires :filter, type: String
end
get :some_route do
declared(params)
end
end
route_param :artist_id, type: Integer do
namespace :compositions do
get do
declared(params)
end
end
end
route_param :compositor_id, type: Integer do
mount mounted
end
end
end
it 'return only :id without :artist_id' do
get '/artists/1'
json = JSON.parse(last_response.body, symbolize_names: true)
expect(json.key?(:id)).to be_truthy
expect(json.key?(:artist_id)).not_to be_truthy
end
it 'return only :artist_id without :id' do
get '/artists/1/compositions'
json = JSON.parse(last_response.body, symbolize_names: true)
expect(json.key?(:artist_id)).to be_truthy
expect(json.key?(:id)).not_to be_truthy
end
it 'return :filter and :id parameters in declared for second enpoint inside route_param' do
get '/artists/1/some_route', filter: 'some_filter'
json = JSON.parse(last_response.body, symbolize_names: true)
expect(json.key?(:filter)).to be_truthy
expect(json.key?(:id)).to be_truthy
expect(json.key?(:artist_id)).not_to be_truthy
end
it 'return :compositor_id for mounter in route_param' do
get '/artists/1/albums'
json = JSON.parse(last_response.body, symbolize_names: true)
expect(json.key?(:compositor_id)).to be_truthy
expect(json.key?(:id)).not_to be_truthy
expect(json.key?(:artist_id)).not_to be_truthy
end
end
describe '#params' do
it 'is available to the caller' do
subject.get('/hey') do
params[:howdy]
end
get '/hey?howdy=hey'
expect(last_response.body).to eq('hey')
end
it 'parses from path segments' do
subject.get('/hey/:id') do
params[:id]
end
get '/hey/12'
expect(last_response.body).to eq('12')
end
it 'deeply converts nested params' do
subject.get '/location' do
params[:location][:city]
end
get '/location?location[city]=Dallas'
expect(last_response.body).to eq('Dallas')
end
context 'with special requirements' do
it 'parses email param with provided requirements for params' do
subject.get('/:person_email', requirements: { person_email: /.*/ }) do
params[:person_email]
end
get '/someone@example.com'
expect(last_response.body).to eq('someone@example.com')
get 'someone@example.com.pl'
expect(last_response.body).to eq('someone@example.com.pl')
end
it 'parses many params with provided regexps' do
subject.get('/:person_email/test/:number', requirements: { person_email: /someone@(.*).com/, number: /[0-9]/ }) do
params[:person_email] << params[:number]
end
get '/someone@example.com/test/1'
expect(last_response.body).to eq('someone@example.com1')
get '/someone@testing.wrong/test/1'
expect(last_response.status).to eq(404)
get 'someone@test.com/test/wrong_number'
expect(last_response.status).to eq(404)
get 'someone@test.com/wrong_middle/1'
expect(last_response.status).to eq(404)
end
context 'namespace requirements' do
before :each do
subject.namespace :outer, requirements: { person_email: /abc@(.*).com/ } do
get('/:person_email') do
params[:person_email]
end
namespace :inner, requirements: { number: /[0-9]/, person_email: /someone@(.*).com/ } do
get '/:person_email/test/:number' do
params[:person_email] << params[:number]
end
end
end
end
it 'parse email param with provided requirements for params' do
get '/outer/abc@example.com'
expect(last_response.body).to eq('abc@example.com')
end
it "should override outer namespace's requirements" do
get '/outer/inner/someone@testing.wrong/test/1'
expect(last_response.status).to eq(404)
get '/outer/inner/someone@testing.com/test/1'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('someone@testing.com1')
end
end
end
context 'from body parameters' do
before(:each) do
subject.post '/request_body' do
params[:user]
end
subject.put '/request_body' do
params[:user]
end
end
it 'converts JSON bodies to params' do
post '/request_body', ::Grape::Json.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json'
expect(last_response.body).to eq('Bobby T.')
end
it 'does not convert empty JSON bodies to params' do
put '/request_body', '', 'CONTENT_TYPE' => 'application/json'
expect(last_response.body).to eq('')
end
if Object.const_defined? :MultiXml
it 'converts XML bodies to params' do
post '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.body).to eq('Bobby T.')
end
it 'converts XML bodies to params' do
put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.body).to eq('Bobby T.')
end
else
it 'converts XML bodies to params' do
post '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.body).to eq('{"__content__"=>"Bobby T."}')
end
it 'converts XML bodies to params' do
put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.body).to eq('{"__content__"=>"Bobby T."}')
end
end
it 'does not include parameters not defined by the body' do
subject.post '/omitted_params' do
error! 400, 'expected nil' if params[:version]
params[:user]
end
post '/omitted_params', ::Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq('Bob')
end
end
it 'responds with a 406 for an unsupported content-type' do
subject.format :json
# subject.content_type :json, "application/json"
subject.put '/request_body' do
params[:user]
end
put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml'
expect(last_response.status).to eq(406)
expect(last_response.body).to eq('{"error":"The requested content-type \'application/xml\' is not supported."}')
end
it 'does not accept text/plain in JSON format if application/json is specified as content type' do
subject.format :json
subject.default_format :json
subject.put '/request_body' do
params[:user]
end
put '/request_body', ::Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'text/plain'
expect(last_response.status).to eq(406)
expect(last_response.body).to eq('{"error":"The requested content-type \'text/plain\' is not supported."}')
end
context 'content type with params' do
before do
subject.format :json
subject.content_type :json, 'application/json; charset=utf-8'
subject.post do
params[:data]
end
post '/', ::Grape::Json.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json'
end
it 'should not response with 406 for same type without params' do
expect(last_response.status).not_to be 406
end
it 'should response with given content type in headers' do
expect(last_response.headers['Content-Type']).to eq 'application/json; charset=utf-8'
end
end
context 'precedence' do
before do
subject.format :json
subject.namespace '/:id' do
get do
{
params: params[:id]
}
end
post do
{
params: params[:id]
}
end
put do
{
params: params[:id]
}
end
end
end
it 'route string params have higher precedence than body params' do
post '/123', { id: 456 }.to_json
expect(JSON.parse(last_response.body)['params']).to eq '123'
put '/123', { id: 456 }.to_json
expect(JSON.parse(last_response.body)['params']).to eq '123'
end
it 'route string params have higher precedence than URL params' do
get '/123?id=456'
expect(JSON.parse(last_response.body)['params']).to eq '123'
post '/123?id=456'
expect(JSON.parse(last_response.body)['params']).to eq '123'
end
end
context 'sets a value to params' do
it 'params' do
subject.params do
requires :a, type: String
end
subject.get '/foo' do
params[:a] = 'bar'
end
get '/foo', a: 'foo'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('bar')
end
end
end
describe '#error!' do
it 'accepts a message' do
subject.get('/hey') do
error! 'This is not valid.'
'This is valid.'
end
get '/hey'
expect(last_response.status).to eq(500)
expect(last_response.body).to eq('This is not valid.')
end
it 'accepts a code' do
subject.get('/hey') do
error! 'Unauthorized.', 401
end
get '/hey'
expect(last_response.status).to eq(401)
expect(last_response.body).to eq('Unauthorized.')
end
it 'accepts an object and render it in format' do
subject.get '/hey' do
error!({ 'dude' => 'rad' }, 403)
end
get '/hey.json'
expect(last_response.status).to eq(403)
expect(last_response.body).to eq('{"dude":"rad"}')
end
it 'accepts a frozen object' do
subject.get '/hey' do
error!({ 'dude' => 'rad' }.freeze, 403)
end
get '/hey.json'
expect(last_response.status).to eq(403)
expect(last_response.body).to eq('{"dude":"rad"}')
end
it 'can specifiy headers' do
subject.get '/hey' do
error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value')
end
get '/hey.json'
expect(last_response.status).to eq(403)
expect(last_response.headers['X-Custom']).to eq('value')
end
it 'sets the status code for the endpoint' do
memoized_endpoint = nil
subject.get '/hey' do
memoized_endpoint = self
error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value')
end
get '/hey.json'
expect(memoized_endpoint.status).to eq(403)
end
end
describe '#redirect' do
it 'redirects to a url with status 302' do
subject.get('/hey') do
redirect '/ha'
end
get '/hey'
expect(last_response.status).to eq 302
expect(last_response.headers['Location']).to eq '/ha'
expect(last_response.body).to eq 'This resource has been moved temporarily to /ha.'
end
it 'has status code 303 if it is not get request and it is http 1.1' do
subject.post('/hey') do
redirect '/ha'
end
post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1'
expect(last_response.status).to eq 303
expect(last_response.headers['Location']).to eq '/ha'
expect(last_response.body).to eq 'An alternate resource is located at /ha.'
end
it 'support permanent redirect' do
subject.get('/hey') do
redirect '/ha', permanent: true
end
get '/hey'
expect(last_response.status).to eq 301
expect(last_response.headers['Location']).to eq '/ha'
expect(last_response.body).to eq 'This resource has been moved permanently to /ha.'
end
it 'allows for an optional redirect body override' do
subject.get('/hey') do
redirect '/ha', body: 'test body'
end
get '/hey'
expect(last_response.body).to eq 'test body'
end
end
it 'does not persist params between calls' do
subject.post('/new') do
params[:text]
end
post '/new', text: 'abc'
expect(last_response.body).to eq('abc')
post '/new', text: 'def'
expect(last_response.body).to eq('def')
end
it 'resets all instance variables (except block) between calls' do
subject.helpers do
def memoized
@memoized ||= params[:howdy]
end
end
subject.get('/hello') do
memoized
end
get '/hello?howdy=hey'
expect(last_response.body).to eq('hey')
get '/hello?howdy=yo'
expect(last_response.body).to eq('yo')
end
it 'allows explicit return calls' do
subject.get('/home') do
return 'Hello'
end
get '/home'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello')
end
describe '.generate_api_method' do
it 'raises NameError if the method name is already in use' do
expect do
Grape::Endpoint.generate_api_method('version', &proc {})
end.to raise_error(NameError)
end
it 'raises ArgumentError if a block is not given' do
expect do
Grape::Endpoint.generate_api_method('GET without a block method')
end.to raise_error(ArgumentError)
end
it 'returns a Proc' do
expect(Grape::Endpoint.generate_api_method('GET test for a proc', &proc {})).to be_a Proc
end
end
context 'filters' do
describe 'before filters' do
it 'runs the before filter if set' do
subject.before { env['before_test'] = 'OK' }
subject.get('/before_test') { env['before_test'] }
get '/before_test'
expect(last_response.body).to eq('OK')
end
end
describe 'after filters' do
it 'overrides the response body if it sets it' do
subject.after { body 'after' }
subject.get('/after_test') { 'during' }
get '/after_test'
expect(last_response.body).to eq('after')
end
it 'does not override the response body with its return' do
subject.after { 'after' }
subject.get('/after_test') { 'body' }
get '/after_test'
expect(last_response.body).to eq('body')
end
end
it 'allows adding to response with present' do
subject.format :json
subject.before { present :before, 'before' }
subject.before_validation { present :before_validation, 'before_validation' }
subject.after_validation { present :after_validation, 'after_validation' }
subject.after { present :after, 'after' }
subject.get :all_filters do
present :endpoint, 'endpoint'
end
get '/all_filters'
json = JSON.parse(last_response.body)
expect(json.keys).to match_array %w[before before_validation after_validation endpoint after]
end
context 'when terminating the response with error!' do
it 'breaks normal call chain' do
called = []
subject.before { called << 'before' }
subject.before_validation { called << 'before_validation' }
subject.after_validation { error! :oops, 500 }
subject.after { called << 'after' }
subject.get :error_filters do
called << 'endpoint'
''
end
get '/error_filters'
expect(last_response.status).to eql 500
expect(called).to match_array %w[before before_validation]
end
it 'allows prior and parent filters of same type to run' do
called = []
subject.before { called << 'parent' }
subject.namespace :parent do
before { called << 'prior' }
before { error! :oops, 500 }
before { called << 'subsequent' }
get :hello do
called << :endpoint
'Hello!'
end
end
get '/parent/hello'
expect(last_response.status).to eql 500
expect(called).to match_array %w[parent prior]
end
end
end
context 'anchoring' do
describe 'delete 204' do
it 'allows for the anchoring option with a delete method' do
subject.send(:delete, '/example', anchor: true) {}
send(:delete, '/example/and/some/more')
expect(last_response.status).to eql 404
end
it 'anchors paths by default for the delete method' do
subject.send(:delete, '/example') {}
send(:delete, '/example/and/some/more')
expect(last_response.status).to eql 404
end
it 'responds to /example/and/some/more for the non-anchored delete method' do
subject.send(:delete, '/example', anchor: false) {}
send(:delete, '/example/and/some/more')
expect(last_response.status).to eql 204
expect(last_response.body).to be_empty
end
end
describe 'delete 200, with response body' do
it 'responds to /example/and/some/more for the non-anchored delete method' do
subject.send(:delete, '/example', anchor: false) do
status 200
body 'deleted'
end
send(:delete, '/example/and/some/more')
expect(last_response.status).to eql 200
expect(last_response.body).not_to be_empty
end
end
describe 'delete 200, with a return value (no explicit body)' do
it 'responds to /example delete method' do
subject.delete(:example) { 'deleted' }
delete '/example'
expect(last_response.status).to eql 200
expect(last_response.body).not_to be_empty
end
end
describe 'delete 204, with nil has return value (no explicit body)' do
it 'responds to /example delete method' do
subject.delete(:example) { nil }
delete '/example'
expect(last_response.status).to eql 204
expect(last_response.body).to be_empty
end
end
describe 'delete 204, with empty array has return value (no explicit body)' do
it 'responds to /example delete method' do
subject.delete(:example) { '' }
delete '/example'
expect(last_response.status).to eql 204
expect(last_response.body).to be_empty
end
end
describe 'all other' do
%w[post get head put options patch].each do |verb|
it "allows for the anchoring option with a #{verb.upcase} method" do
subject.send(verb, '/example', anchor: true) do
verb
end
send(verb, '/example/and/some/more')
expect(last_response.status).to eql 404
end
it "anchors paths by default for the #{verb.upcase} method" do
subject.send(verb, '/example') do
verb
end
send(verb, '/example/and/some/more')
expect(last_response.status).to eql 404
end
it "responds to /example/and/some/more for the non-anchored #{verb.upcase} method" do
subject.send(verb, '/example', anchor: false) do
verb
end
send(verb, '/example/and/some/more')
expect(last_response.status).to eql verb == 'post' ? 201 : 200
expect(last_response.body).to eql verb == 'head' ? '' : verb
end
end
end
end
context 'request' do
it 'is set to the url requested' do
subject.get('/url') do
request.url
end
get '/url'
expect(last_response.body).to eq('http://example.org/url')
end
['v1', :v1].each do |version|
it "should include version #{version}" do
subject.version version, using: :path
subject.get('/url') do
request.url
end
get "/#{version}/url"
expect(last_response.body).to eq("http://example.org/#{version}/url")
end
end
it 'should include prefix' do
subject.version 'v1', using: :path
subject.prefix 'api'
subject.get('/url') do
request.url
end
get '/api/v1/url'
expect(last_response.body).to eq('http://example.org/api/v1/url')
end
end
context 'version headers' do
before do
# NOTE: a 404 is returned instead of the 406 if cascade: false is not set.
subject.version 'v1', using: :header, vendor: 'ohanapi', cascade: false
subject.get '/test' do
'Hello!'
end
end
it 'result in a 406 response if they are invalid' do
get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json'
expect(last_response.status).to eq(406)
end
it 'result in a 406 response if they cannot be parsed by rack-accept' do
get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1'
expect(last_response.status).to eq(406)
end
end
context 'binary' do
before do
subject.get do
file FileStreamer.new(__FILE__)
end
end
it 'suports stream objects in response' do
get '/'
expect(last_response.status).to eq 200
expect(last_response.body).to eq File.read(__FILE__)
end
end
context 'validation errors' do
before do
subject.before do
header['Access-Control-Allow-Origin'] = '*'
end
subject.params do
requires :id, type: String
end
subject.get do
'should not get here'
end
end
it 'returns the errors, and passes headers' do
get '/'
expect(last_response.status).to eq 400
expect(last_response.body).to eq 'id is missing'
expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*')
end
end
context 'instrumentation' do
before do
subject.before do
# Placeholder
end
subject.get do
'hello'
end
@events = []
@subscriber = ActiveSupport::Notifications.subscribe(/grape/) do |*args|
@events << ActiveSupport::Notifications::Event.new(*args)
end
end
after do
ActiveSupport::Notifications.unsubscribe(@subscriber)
end
it 'notifies AS::N' do
get '/'
# In order that the events finalized (time each block ended)
expect(@events).to contain_exactly(
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: a_collection_containing_exactly(an_instance_of(Proc)),
type: :before }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: [],
type: :before_validation }),
have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
validators: [],
request: a_kind_of(Grape::Request) }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: [],
type: :after_validation }),
have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(Grape::Endpoint) }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: [],
type: :after }),
have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
env: an_instance_of(Hash) })
)
# In order that events were initialized
expect(@events.sort_by(&:time)).to contain_exactly(
have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
env: an_instance_of(Hash) }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: a_collection_containing_exactly(an_instance_of(Proc)),
type: :before }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: [],
type: :before_validation }),
have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
validators: [],
request: a_kind_of(Grape::Request) }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: [],
type: :after_validation }),
have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(Grape::Endpoint) }),
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint),
filters: [],
type: :after })
)
end
end
end
grape-1.0.2/spec/grape/validations/ 0000755 0000041 0000041 00000000000 13231337007 017204 5 ustar www-data www-data grape-1.0.2/spec/grape/validations/types_spec.rb 0000644 0000041 0000041 00000005632 13231337007 021715 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::Types do
module TypesSpec
class FooType
def self.parse(_); end
end
class BarType
def self.parse; end
end
end
VirtusA = Virtus::Attribute.build(String)
module VirtusModule
include Virtus.module
end
class VirtusB
include VirtusModule
end
class VirtusC
include Virtus.model
end
MyAxiom = Axiom::Types::String.new do
minimum_length 1
maximum_length 30
end
describe '::primitive?' do
[
Integer, Float, Numeric, BigDecimal,
Virtus::Attribute::Boolean, String, Symbol,
Date, DateTime, Time, Rack::Multipart::UploadedFile
].each do |type|
it "recognizes #{type} as a primitive" do
expect(described_class.primitive?(type)).to be_truthy
end
end
it 'identifies unknown types' do
expect(described_class.primitive?(Object)).to be_falsy
expect(described_class.primitive?(TypesSpec::FooType)).to be_falsy
end
end
describe '::structure?' do
[
Hash, Array, Set
].each do |type|
it "recognizes #{type} as a structure" do
expect(described_class.structure?(type)).to be_truthy
end
end
end
describe '::recognized?' do
[
VirtusA, VirtusB, VirtusC, MyAxiom
].each do |type|
it "recognizes #{type}" do
expect(described_class.recognized?(type)).to be_truthy
end
end
end
describe '::special?' do
[
JSON, Array[JSON], File, Rack::Multipart::UploadedFile
].each do |type|
it "provides special handling for #{type.inspect}" do
expect(described_class.special?(type)).to be_truthy
end
end
end
describe '::custom?' do
it 'returns false if the type does not respond to :parse' do
expect(described_class.custom?(Object)).to be_falsy
end
it 'returns true if the type responds to :parse with one argument' do
expect(described_class.custom?(TypesSpec::FooType)).to be_truthy
end
it 'returns false if the type\'s #parse method takes other than one argument' do
expect(described_class.custom?(TypesSpec::BarType)).to be_falsy
end
end
describe '::build_coercer' do
it 'has internal cache variables' do
expect(described_class.instance_variable_get(:@__cache)).to be_a(Hash)
expect(described_class.instance_variable_get(:@__cache_write_lock)).to be_a(Mutex)
end
it 'caches the result of the Virtus::Attribute.build method' do
original_cache = described_class.instance_variable_get(:@__cache)
described_class.instance_variable_set(:@__cache, {})
coercer = 'TestCoercer'
expect(Virtus::Attribute).to receive(:build).once.and_return(coercer)
expect(described_class.build_coercer(Array[String])).to eq(coercer)
expect(described_class.build_coercer(Array[String])).to eq(coercer)
described_class.instance_variable_set(:@__cache, original_cache)
end
end
end
grape-1.0.2/spec/grape/validations/instance_behaivour_spec.rb 0000644 0000041 0000041 00000002177 13231337007 024422 0 ustar www-data www-data require 'spec_helper'
describe 'Validator with instance variables' do
let(:validator_type) do
Class.new(Grape::Validations::Base) do
def validate_param!(_attr_name, _params)
if @instance_variable
raise Grape::Exceptions::Validation, params: ['params'],
message: 'This should never happen'
end
@instance_variable = true
end
end
end
before do
Grape::Validations.register_validator('instance_validator', validator_type)
end
after do
Grape::Validations.deregister_validator('instance_validator')
end
let(:app) do
Class.new(Grape::API) do
params do
optional :param_to_validate, instance_validator: true
optional :another_param_to_validate, instance_validator: true
end
get do
'noop'
end
end
end
it 'passes validation every time' do
expect(validator_type).to receive(:new).exactly(4).times.and_call_original
2.times do
get '/', param_to_validate: 'value', another_param_to_validate: 'value'
expect(last_response.status).to eq 200
end
end
end
grape-1.0.2/spec/grape/validations/params_scope_spec.rb 0000644 0000041 0000041 00000065207 13231337007 023231 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::ParamsScope do
subject do
Class.new(Grape::API)
end
def app
subject
end
context 'setting a default' do
let(:documentation) { subject.routes.first.params }
context 'when the default value is truthy' do
before do
subject.params do
optional :int, type: Integer, default: 42
end
subject.get
end
it 'adds documentation about the default value' do
expect(documentation).to have_key('int')
expect(documentation['int']).to have_key(:default)
expect(documentation['int'][:default]).to eq(42)
end
end
context 'when the default value is false' do
before do
subject.params do
optional :bool, type: Virtus::Attribute::Boolean, default: false
end
subject.get
end
it 'adds documentation about the default value' do
expect(documentation).to have_key('bool')
expect(documentation['bool']).to have_key(:default)
expect(documentation['bool'][:default]).to eq(false)
end
end
context 'when the default value is nil' do
before do
subject.params do
optional :object, type: Object, default: nil
end
subject.get
end
it 'adds documentation about the default value' do
expect(documentation).to have_key('object')
expect(documentation['object']).to have_key(:default)
expect(documentation['object'][:default]).to eq(nil)
end
end
end
context 'without a default' do
before do
subject.params do
optional :object, type: Object
end
subject.get
end
it 'does not add documentation for the default value' do
documentation = subject.routes.first.params
expect(documentation).to have_key('object')
expect(documentation['object']).not_to have_key(:default)
end
end
context 'setting description' do
%i[desc description].each do |description_type|
it "allows setting #{description_type}" do
subject.params do
requires :int, type: Integer, description_type => 'My very nice integer'
end
subject.get '/single' do
'int works'
end
get '/single', int: 420
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('int works')
end
end
end
context 'when using custom types' do
module ParamsScopeSpec
class CustomType
attr_reader :value
def self.parse(value)
raise if value == 'invalid'
new(value)
end
def initialize(value)
@value = value
end
end
end
it 'coerces the parameter via the type\'s parse method' do
subject.params do
requires :foo, type: ParamsScopeSpec::CustomType
end
subject.get('/types') { params[:foo].value }
get '/types', foo: 'valid'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('valid')
get '/types', foo: 'invalid'
expect(last_response.status).to eq(400)
expect(last_response.body).to match(/foo is invalid/)
end
end
context 'param alias' do
it do
subject.params do
requires :foo, as: :bar
optional :super, as: :hiper
end
subject.get('/alias') { "#{declared(params)['bar']}-#{declared(params)['hiper']}" }
get '/alias', foo: 'any', super: 'any2'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('any-any2')
end
it do
subject.params do
requires :foo, as: :bar, type: String, coerce_with: ->(c) { c.strip }
end
subject.get('/alias-coerced') { "#{params['bar']}-#{params['foo']}" }
get '/alias-coerced', foo: ' there we go '
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('there we go-')
end
it do
subject.params do
requires :foo, as: :bar, allow_blank: false
end
subject.get('/alias-not-blank') {}
get '/alias-not-blank', foo: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('foo is empty')
end
end
context 'array without coerce type explicitly given' do
it 'sets the type based on first element' do
subject.params do
requires :periods, type: Array, values: -> { %w[day month] }
end
subject.get('/required') { 'required works' }
get '/required', periods: %w[day month]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
it 'fails to call API without Array type' do
subject.params do
requires :periods, type: Array, values: -> { %w[day month] }
end
subject.get('/required') { 'required works' }
get '/required', periods: 'day'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('periods is invalid')
end
it 'raises exception when values are of different type' do
expect do
subject.params { requires :numbers, type: Array, values: [1, 'definitely not a number', 3] }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'raises exception when range values have different endpoint types' do
expect do
subject.params { requires :numbers, type: Array, values: 0.0..10 }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
end
context 'coercing values validation with proc' do
it 'allows the proc to pass validation without checking' do
subject.params { requires :numbers, type: Integer, values: -> { [0, 1, 2] } }
subject.post('/required') { 'coercion with proc works' }
post '/required', numbers: '1'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq('coercion with proc works')
end
it 'allows the proc to pass validation without checking in value' do
subject.params { requires :numbers, type: Integer, values: { value: -> { [0, 1, 2] } } }
subject.post('/required') { 'coercion with proc works' }
post '/required', numbers: '1'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq('coercion with proc works')
end
it 'allows the proc to pass validation without checking in except' do
subject.params { requires :numbers, type: Integer, values: { except: -> { [0, 1, 2] } } }
subject.post('/required') { 'coercion with proc works' }
post '/required', numbers: '10'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq('coercion with proc works')
end
end
context 'with range values' do
context "when left range endpoint isn't #kind_of? the type" do
it 'raises exception' do
expect do
subject.params { requires :latitude, type: Integer, values: -90.0..90 }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
end
context "when right range endpoint isn't #kind_of? the type" do
it 'raises exception' do
expect do
subject.params { requires :latitude, type: Integer, values: -90..90.0 }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
end
context 'when the default is an array' do
context 'and is the entire range of allowed values' do
it 'does not raise an exception' do
expect do
subject.params { optional :numbers, type: Array[Integer], values: 0..2, default: 0..2 }
end.to_not raise_error
end
end
context 'and is a subset of allowed values' do
it 'does not raise an exception' do
expect do
subject.params { optional :numbers, type: Array[Integer], values: [0, 1, 2], default: [1, 0] }
end.to_not raise_error
end
end
end
context 'when both range endpoints are #kind_of? the type' do
it 'accepts values in the range' do
subject.params do
requires :letter, type: String, values: 'a'..'z'
end
subject.get('/letter') { params[:letter] }
get '/letter', letter: 'j'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('j')
end
it 'rejects values outside the range' do
subject.params do
requires :letter, type: String, values: 'a'..'z'
end
subject.get('/letter') { params[:letter] }
get '/letter', letter: 'J'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('letter does not have a valid value')
end
end
end
context 'parameters in group' do
it 'errors when no type is provided' do
expect do
subject.params do
group :a do
requires :b
end
end
end.to raise_error Grape::Exceptions::MissingGroupTypeError
expect do
subject.params do
optional :a do
requires :b
end
end
end.to raise_error Grape::Exceptions::MissingGroupTypeError
end
it 'allows Hash as type' do
subject.params do
group :a, type: Hash do
requires :b
end
end
subject.get('/group') { 'group works' }
get '/group', a: { b: true }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('group works')
subject.params do
optional :a, type: Hash do
requires :b
end
end
get '/optional_type_hash'
end
it 'allows Array as type' do
subject.params do
group :a, type: Array do
requires :b
end
end
subject.get('/group') { 'group works' }
get '/group', a: [{ b: true }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('group works')
subject.params do
optional :a, type: Array do
requires :b
end
end
get '/optional_type_array'
end
it 'handles missing optional Array type' do
subject.params do
optional :a, type: Array do
requires :b
end
end
subject.get('/test') { declared(params).to_json }
get '/test'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('{"a":[]}')
end
it 'errors with an unsupported type' do
expect do
subject.params do
group :a, type: Set do
requires :b
end
end
end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError
expect do
subject.params do
optional :a, type: Set do
requires :b
end
end
end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError
end
end
context 'when validations are dependent on a parameter' do
before do
subject.params do
optional :a
given :a do
requires :b
end
end
subject.get('/test') { declared(params).to_json }
end
it 'applies the validations only if the parameter is present' do
get '/test'
expect(last_response.status).to eq(200)
get '/test', a: true
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('b is missing')
get '/test', a: true, b: true
expect(last_response.status).to eq(200)
end
it 'applies the validations of multiple parameters' do
subject.params do
optional :a, :b
given :a, :b do
requires :c
end
end
subject.get('/multiple') { declared(params).to_json }
get '/multiple'
expect(last_response.status).to eq(200)
get '/multiple', a: true
expect(last_response.status).to eq(200)
get '/multiple', b: true
expect(last_response.status).to eq(200)
get '/multiple', a: true, b: true
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('c is missing')
get '/multiple', a: true, b: true, c: true
expect(last_response.status).to eq(200)
end
it 'applies only the appropriate validation' do
subject.params do
optional :a
optional :b
mutually_exclusive :a, :b
given :a do
requires :c, type: String
end
given :b do
requires :c, type: Integer
end
end
subject.get('/multiple') { declared(params).to_json }
get '/multiple'
expect(last_response.status).to eq(200)
get '/multiple', a: true, c: 'test'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body).symbolize_keys).to eq a: 'true', b: nil, c: 'test'
get '/multiple', b: true, c: '3'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body).symbolize_keys).to eq a: nil, b: 'true', c: 3
get '/multiple', a: true
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('c is missing')
get '/multiple', b: true
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('c is missing')
get '/multiple', a: true, b: true, c: 'test'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a, b are mutually exclusive, c is invalid')
end
it 'raises an error if the dependent parameter was never specified' do
expect do
subject.params do
given :c do
end
end
end.to raise_error(Grape::Exceptions::UnknownParameter)
end
it 'does not validate nested requires when given is false' do
subject.params do
requires :a, type: String, allow_blank: false, values: %w[x y z]
given a: ->(val) { val == 'x' } do
requires :inner1, type: Hash, allow_blank: false do
requires :foo, type: Integer, allow_blank: false
end
end
given a: ->(val) { val == 'y' } do
requires :inner2, type: Hash, allow_blank: false do
requires :bar, type: Integer, allow_blank: false
requires :baz, type: Array, allow_blank: false do
requires :baz_category, type: String, allow_blank: false
end
end
end
given a: ->(val) { val == 'z' } do
requires :inner3, type: Array, allow_blank: false do
requires :bar, type: Integer, allow_blank: false
requires :baz, type: Array, allow_blank: false do
requires :baz_category, type: String, allow_blank: false
end
end
end
end
subject.get('/varying') { declared(params).to_json }
get '/varying', a: 'x', inner1: { foo: 1 }
expect(last_response.status).to eq(200)
get '/varying', a: 'y', inner2: { bar: 2, baz: [{ baz_category: 'barstools' }] }
expect(last_response.status).to eq(200)
get '/varying', a: 'y', inner2: { bar: 2, baz: [{ unrelated: 'yep' }] }
expect(last_response.status).to eq(400)
get '/varying', a: 'z', inner3: [{ bar: 3, baz: [{ baz_category: 'barstools' }] }]
expect(last_response.status).to eq(200)
end
it 'includes the parameter within #declared(params)' do
get '/test', a: true, b: true
expect(JSON.parse(last_response.body)).to eq('a' => 'true', 'b' => 'true')
end
it 'returns a sensible error message within a nested context' do
subject.params do
requires :bar, type: Hash do
optional :a
given :a do
requires :b
end
end
end
subject.get('/nested') { 'worked' }
get '/nested', bar: { a: true }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('bar[b] is missing')
end
it 'includes the nested parameter within #declared(params)' do
subject.params do
requires :bar, type: Hash do
optional :a
given :a do
requires :b
end
end
end
subject.get('/nested') { declared(params).to_json }
get '/nested', bar: { a: true, b: 'yes' }
expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'b' => 'yes' })
end
it 'includes level 2 nested parameters outside the given within #declared(params)' do
subject.params do
requires :bar, type: Hash do
optional :a
given :a do
requires :c, type: Hash do
requires :b
end
end
end
end
subject.get('/nested') { declared(params).to_json }
get '/nested', bar: { a: true, c: { b: 'yes' } }
expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } })
end
end
context 'when validations are dependent on a parameter within an array param' do
before do
subject.params do
requires :foos, type: Array do
optional :foo_type, :baz_type
given :foo_type do
requires :bar
end
end
end
subject.post('/test') { declared(params).to_json }
end
it 'applies the constraint within each value' do
post '/test',
{ foos: [{ foo_type: 'a' }, { baz_type: 'c' }] }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('foos[0][bar] is missing')
end
end
context 'when validations are dependent on a parameter with specific value' do
# build test cases from all combinations of declarations and options
a_decls = %i[optional requires]
a_options = [{}, { values: %w[x y z] }]
b_options = [{}, { type: String }, { allow_blank: false }, { type: String, allow_blank: false }]
combinations = a_decls.product(a_options, b_options)
combinations.each_with_index do |combination, i|
a_decl, a_opts, b_opts = combination
context "(case #{i})" do
before do
# puts "a_decl: #{a_decl}, a_opts: #{a_opts}, b_opts: #{b_opts}"
subject.params do
send a_decl, :a, **a_opts
given(a: ->(val) { val == 'x' }) { requires :b, **b_opts }
given(a: ->(val) { val == 'y' }) { requires :c, **b_opts }
end
subject.get('/test') { declared(params).to_json }
end
if a_decl == :optional
it 'skips validation when base param is missing' do
get '/test'
expect(last_response.status).to eq(200)
end
end
it 'skips validation when base param does not have a specified value' do
get '/test', a: 'z'
expect(last_response.status).to eq(200)
get '/test', a: 'z', b: ''
expect(last_response.status).to eq(200)
end
it 'applies the validation when base param has the specific value' do
get '/test', a: 'x'
expect(last_response.status).to eq(400)
expect(last_response.body).to include('b is missing')
get '/test', a: 'x', b: true
expect(last_response.status).to eq(200)
get '/test', a: 'x', b: true, c: ''
expect(last_response.status).to eq(200)
end
it 'includes the parameter within #declared(params)' do
get '/test', a: 'x', b: true
expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => 'true', 'c' => nil)
end
end
end
end
it 'raises an error if the dependent parameter was never specified' do
expect do
subject.params do
given :c do
end
end
end.to raise_error(Grape::Exceptions::UnknownParameter)
end
it 'returns a sensible error message within a nested context' do
subject.params do
requires :bar, type: Hash do
optional :a
given a: ->(val) { val == 'x' } do
requires :b
end
end
end
subject.get('/nested') { 'worked' }
get '/nested', bar: { a: 'x' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('bar[b] is missing')
end
it 'includes the nested parameter within #declared(params)' do
subject.params do
requires :bar, type: Hash do
optional :a
given a: ->(val) { val == 'x' } do
requires :b
end
end
end
subject.get('/nested') { declared(params).to_json }
get '/nested', bar: { a: 'x', b: 'yes' }
expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'b' => 'yes' })
end
it 'includes level 2 nested parameters outside the given within #declared(params)' do
subject.params do
requires :bar, type: Hash do
optional :a
given a: ->(val) { val == 'x' } do
requires :c, type: Hash do
requires :b
end
end
end
end
subject.get('/nested') { declared(params).to_json }
get '/nested', bar: { a: 'x', c: { b: 'yes' } }
expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'c' => { 'b' => 'yes' } })
end
it 'includes deeply nested parameters within #declared(params)' do
subject.params do
requires :arr1, type: Array do
requires :hash1, type: Hash do
requires :arr2, type: Array do
requires :hash2, type: Hash do
requires :something, type: String
end
end
end
end
end
subject.get('/nested_deep') { declared(params).to_json }
get '/nested_deep', arr1: [{ hash1: { arr2: [{ hash2: { something: 'value' } }] } }]
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq('arr1' => [{ 'hash1' => { 'arr2' => [{ 'hash2' => { 'something' => 'value' } }] } }])
end
context 'failing fast' do
context 'when fail_fast is not defined' do
it 'does not stop validation' do
subject.params do
requires :one
requires :two
requires :three
end
subject.get('/fail-fast') { declared(params).to_json }
get '/fail-fast'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('one is missing, two is missing, three is missing')
end
end
context 'when fail_fast is defined it stops the validation' do
it 'of other params' do
subject.params do
requires :one, fail_fast: true
requires :two
end
subject.get('/fail-fast') { declared(params).to_json }
get '/fail-fast'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('one is missing')
end
it 'for a single param' do
subject.params do
requires :one, allow_blank: false, regexp: /[0-9]+/, fail_fast: true
end
subject.get('/fail-fast') { declared(params).to_json }
get '/fail-fast', one: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('one is empty')
end
end
end
context 'when params have group attributes' do
context 'with validations' do
before do
subject.params do
with(allow_blank: false) do
requires :id
optional :name
optional :address, allow_blank: true
end
end
subject.get('test')
end
context 'when data is invalid' do
before do
get 'test', id: '', name: ''
end
it 'returns a validation error' do
expect(last_response.status).to eq(400)
end
it 'applies group validations for every parameter' do
expect(last_response.body).to eq('id is empty, name is empty')
end
end
context 'when parameter has the same validator as a group' do
before do
get 'test', id: 'id', address: ''
end
it 'returns a successful response' do
expect(last_response.status).to eq(200)
end
it 'prioritizes parameter validation over group validation' do
expect(last_response.body).to_not include('address is empty')
end
end
end
context 'with types' do
before do
subject.params do
with(type: Date) do
requires :created_at
end
end
subject.get('test') { params[:created_at] }
end
context 'when invalid date provided' do
before do
get 'test', created_at: 'not_a_date'
end
it 'responds with HTTP error' do
expect(last_response.status).to eq(400)
end
it 'returns a validation error' do
expect(last_response.body).to eq('created_at is invalid')
end
end
context 'when created_at receives a valid date' do
before do
get 'test', created_at: '2016-01-01'
end
it 'returns a successful response' do
expect(last_response.status).to eq(200)
end
it 'returns a date' do
expect(last_response.body).to eq('2016-01-01')
end
end
end
context 'with several group attributes' do
before do
subject.params do
with(values: [1]) do
requires :id, type: Integer
end
with(allow_blank: false) do
optional :address, type: String
end
requires :name
end
subject.get('test')
end
context 'when data is invalid' do
before do
get 'test', id: 2, address: ''
end
it 'responds with HTTP error' do
expect(last_response.status).to eq(400)
end
it 'returns a validation error' do
expect(last_response.body).to eq('id does not have a valid value, address is empty, name is missing')
end
end
context 'when correct data is provided' do
before do
get 'test', id: 1, address: 'Some street', name: 'John'
end
it 'returns a successful response' do
expect(last_response.status).to eq(200)
end
end
end
context 'with nested groups' do
before do
subject.params do
with(type: Integer) do
requires :id
with(type: Date) do
requires :created_at
optional :updated_at
end
end
end
subject.get('test')
end
context 'when data is invalid' do
before do
get 'test', id: 'wrong', created_at: 'not_a_date', updated_at: '2016-01-01'
end
it 'responds with HTTP error' do
expect(last_response.status).to eq(400)
end
it 'returns a validation error' do
expect(last_response.body).to eq('id is invalid, created_at is invalid')
end
end
context 'when correct data is provided' do
before do
get 'test', id: 1, created_at: '2016-01-01'
end
it 'returns a successful response' do
expect(last_response.status).to eq(200)
end
end
end
end
end
grape-1.0.2/spec/grape/validations/attributes_iterator_spec.rb 0000644 0000041 0000041 00000000116 13231337007 024640 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::AttributesIterator do
end
grape-1.0.2/spec/grape/validations/validators/ 0000755 0000041 0000041 00000000000 13231337007 021354 5 ustar www-data www-data grape-1.0.2/spec/grape/validations/validators/exactly_one_of_spec.rb 0000644 0000041 0000041 00000004163 13231337007 025715 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::ExactlyOneOfValidator do
describe '#validate!' do
let(:scope) do
Struct.new(:opts) do
def params(arg)
arg
end
def required?; end
end
end
let(:exactly_one_of_params) { %i[beer wine grapefruit] }
let(:validator) { described_class.new(exactly_one_of_params, {}, false, scope.new) }
context 'when all restricted params are present' do
let(:params) { { beer: true, wine: true, grapefruit: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
context 'mixed with other params' do
let(:mixed_params) { params.merge!(other: true, andanother: true) }
it 'still raises a validation exception' do
expect do
validator.validate! mixed_params
end.to raise_error(Grape::Exceptions::Validation)
end
end
end
context 'when a subset of restricted params are present' do
let(:params) { { beer: true, grapefruit: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
context 'when params keys come as strings' do
let(:params) { { 'beer' => true, 'grapefruit' => true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
context 'when none of the restricted params is selected' do
let(:params) { { somethingelse: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
context 'when exactly one of the restricted params is selected' do
let(:params) { { beer: true, somethingelse: true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/coerce_spec.rb 0000644 0000041 0000041 00000071246 13231337007 024165 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::CoerceValidator do
subject do
Class.new(Grape::API)
end
def app
subject
end
describe 'coerce' do
module CoerceValidatorSpec
class User
include Virtus.model
attribute :id, Integer
attribute :name, String
end
end
context 'i18n' do
after :each do
I18n.locale = :en
end
it 'i18n error on malformed input' do
I18n.load_path << File.expand_path('../zh-CN.yml', __FILE__)
I18n.reload!
I18n.locale = 'zh-CN'.to_sym
subject.params do
requires :age, type: Integer
end
subject.get '/single' do
'int works'
end
get '/single', age: '43a'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('å¹´é¾„æ ¼å¼ä¸æ£ç¡®')
end
it 'gives an english fallback error when default locale message is blank' do
I18n.locale = 'pt-BR'.to_sym
subject.params do
requires :age, type: Integer
end
subject.get '/single' do
'int works'
end
get '/single', age: '43a'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('age is invalid')
end
end
context 'with a custom validation message' do
it 'errors on malformed input' do
subject.params do
requires :int, type: { value: Integer, message: 'type cast is invalid' }
end
subject.get '/single' do
'int works'
end
get '/single', int: '43a'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('int type cast is invalid')
get '/single', int: '43'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('int works')
end
context 'on custom coercion rules' do
before do
subject.params do
requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val|
if val == 'yup'
true
elsif val == 'false'
0
else
val
end
end)
end
subject.get '/' do
params[:a].class.to_s
end
end
it 'respects :coerce_with' do
get '/', a: 'yup'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
end
it 'still validates type' do
get '/', a: 'false'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a type cast is invalid')
end
it 'performs no additional coercion' do
get '/', a: 'true'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('String')
end
end
end
it 'error on malformed input' do
subject.params do
requires :int, type: Integer
end
subject.get '/single' do
'int works'
end
get '/single', int: '43a'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('int is invalid')
get '/single', int: '43'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('int works')
end
it 'error on malformed input (Array)' do
subject.params do
requires :ids, type: Array[Integer]
end
subject.get '/array' do
'array int works'
end
get 'array', ids: %w[1 2 az]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('ids is invalid')
get 'array', ids: %w[1 2 890]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('array int works')
end
context 'complex objects' do
it 'error on malformed input for complex objects' do
subject.params do
requires :user, type: CoerceValidatorSpec::User
end
subject.get '/user' do
'complex works'
end
get '/user', user: '32'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('user is invalid')
get '/user', user: { id: 32, name: 'Bob' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('complex works')
end
end
context 'coerces' do
it 'Integer' do
subject.params do
requires :int, coerce: Integer
end
subject.get '/int' do
params[:int].class
end
get '/int', int: '45'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq(integer_class_name)
end
context 'Array' do
it 'Array of Integers' do
subject.params do
requires :arry, coerce: Array[Integer]
end
subject.get '/array' do
params[:arry][0].class
end
get '/array', arry: %w[1 2 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq(integer_class_name)
end
it 'Array of Bools' do
subject.params do
requires :arry, coerce: Array[Virtus::Attribute::Boolean]
end
subject.get '/array' do
params[:arry][0].class
end
get 'array', arry: [1, 0]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
end
it 'Array of Complex' do
subject.params do
requires :arry, coerce: Array[CoerceValidatorSpec::User]
end
subject.get '/array' do
params[:arry].size
end
get 'array', arry: [31]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('arry is invalid')
get 'array', arry: { id: 31, name: 'Alice' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('arry is invalid')
get 'array', arry: [{ id: 31, name: 'Alice' }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('1')
end
it 'Array of type implementing parse' do
subject.params do
requires :uri, type: Array[URI]
end
subject.get '/uri_array' do
params[:uri][0].class
end
get 'uri_array', uri: ['http://www.google.com']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('URI::HTTP')
end
it 'Set of type implementing parse' do
subject.params do
requires :uri, type: Set[URI]
end
subject.get '/uri_array' do
"#{params[:uri].class},#{params[:uri].first.class},#{params[:uri].size}"
end
get 'uri_array', uri: Array.new(2) { 'http://www.example.com' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Set,URI::HTTP,1')
end
it 'Array of class implementing parse and parsed?' do
class SecureURIOnly
def self.parse(value)
URI.parse(value)
end
def self.parsed?(value)
value.is_a? URI::HTTPS
end
end
subject.params do
requires :uri, type: Array[SecureURIOnly]
end
subject.get '/secure_uris' do
params[:uri].first.class
end
get 'secure_uris', uri: ['https://www.example.com']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('URI::HTTPS')
get 'secure_uris', uri: ['https://www.example.com', 'http://www.example.com']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('uri is invalid')
end
end
context 'Set' do
it 'Set of Integers' do
subject.params do
requires :set, coerce: Set[Integer]
end
subject.get '/set' do
params[:set].first.class
end
get '/set', set: Set.new([1, 2, 3, 4]).to_a
expect(last_response.status).to eq(200)
expect(last_response.body).to eq(integer_class_name)
end
it 'Set of Bools' do
subject.params do
requires :set, coerce: Set[Virtus::Attribute::Boolean]
end
subject.get '/set' do
params[:set].first.class
end
get '/set', set: Set.new([1, 0]).to_a
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
end
end
it 'Bool' do
subject.params do
requires :bool, coerce: Virtus::Attribute::Boolean
end
subject.get '/bool' do
params[:bool].class
end
get '/bool', bool: 1
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
get '/bool', bool: 0
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('FalseClass')
get '/bool', bool: 'false'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('FalseClass')
get '/bool', bool: 'true'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
end
it 'Boolean' do
subject.params do
optional :boolean, type: Boolean, default: true
end
subject.get '/boolean' do
params[:boolean].class
end
get '/boolean'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
get '/boolean', boolean: true
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
get '/boolean', boolean: false
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('FalseClass')
get '/boolean', boolean: 'true'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
get '/boolean', boolean: 'false'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('FalseClass')
get '/boolean', boolean: 123
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('boolean is invalid')
get '/boolean', boolean: '123'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('boolean is invalid')
end
it 'Rack::Multipart::UploadedFile' do
subject.params do
requires :file, type: Rack::Multipart::UploadedFile
end
subject.post '/upload' do
params[:file][:filename]
end
post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
expect(last_response.status).to eq(201)
expect(last_response.body).to eq(File.basename(__FILE__).to_s)
post '/upload', file: 'not a file'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('file is invalid')
end
it 'File' do
subject.params do
requires :file, coerce: File
end
subject.post '/upload' do
params[:file][:filename]
end
post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
expect(last_response.status).to eq(201)
expect(last_response.body).to eq(File.basename(__FILE__).to_s)
post '/upload', file: 'not a file'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('file is invalid')
end
it 'Nests integers' do
subject.params do
requires :integers, type: Hash do
requires :int, coerce: Integer
end
end
subject.get '/int' do
params[:integers][:int].class
end
get '/int', integers: { int: '45' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq(integer_class_name)
end
end
context 'using coerce_with' do
it 'parses parameters with Array type' do
subject.params do
requires :values, type: Array, coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: '1 2 3 4'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4])
get '/ints', values: 'a b c d'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0])
end
it 'parses parameters with Array[String] type' do
subject.params do
requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: '1 2 3 4'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4])
get '/ints', values: 'a b c d'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq(%w[0 0 0 0])
end
it 'parses parameters with Array[Integer] type' do
subject.params do
requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: '1 2 3 4'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4])
get '/ints', values: 'a b c d'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0])
end
it 'parses parameters even if type is valid' do
subject.params do
requires :values, type: Array, coerce_with: ->(array) { array.map { |val| val.to_i + 1 } }
end
subject.get '/ints' do
params[:values]
end
get '/ints', values: [1, 2, 3, 4]
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([2, 3, 4, 5])
get '/ints', values: %w[a b c d]
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1])
end
it 'uses parse where available' do
subject.params do
requires :ints, type: Array, coerce_with: JSON do
requires :i, type: Integer
requires :j
end
end
subject.get '/ints' do
ints = params[:ints].first
'coercion works' if ints[:i] == 1 && ints[:j] == '2'
end
get '/ints', ints: [{ i: 1, j: '2' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('ints is invalid')
get '/ints', ints: '{"i":1,"j":"2"}'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('ints is invalid')
get '/ints', ints: '[{"i":"1","j":"2"}]'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('coercion works')
end
it 'accepts any callable' do
subject.params do
requires :ints, type: Hash, coerce_with: JSON.method(:parse) do
requires :int, type: Integer, coerce_with: ->(val) { val == 'three' ? 3 : val }
end
end
subject.get '/ints' do
params[:ints][:int]
end
get '/ints', ints: '{"int":"3"}'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('ints[int] is invalid')
get '/ints', ints: '{"int":"three"}'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('3')
get '/ints', ints: '{"int":3}'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('3')
end
it 'must be supplied with :type or :coerce' do
expect do
subject.params do
requires :ints, coerce_with: JSON
end
end.to raise_error(ArgumentError)
end
end
context 'first-class JSON' do
it 'parses objects, hashes, and arrays' do
subject.params do
requires :splines, type: JSON do
requires :x, type: Integer, values: [1, 2, 3]
optional :ints, type: Array[Integer]
optional :obj, type: Hash do
optional :y
end
end
end
subject.get '/' do
if params[:splines].is_a? Hash
params[:splines][:obj][:y]
elsif params[:splines].any? { |s| s.key? :obj }
'arrays work'
end
end
get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('woof')
get '/', splines: { x: 1, ints: [1, 2, 3], obj: { y: 'woof' } }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('woof')
get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('arrays work')
get '/', splines: [{ x: 2, ints: [] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('arrays work')
get '/', splines: '{"x":4,"ints":[2]}'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: { x: 4, ints: [2] }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: [{ x: 1, ints: [] }, { x: 4, ints: [] }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
end
it 'works when declared optional' do
subject.params do
optional :splines, type: JSON do
requires :x, type: Integer, values: [1, 2, 3]
optional :ints, type: Array[Integer]
optional :obj, type: Hash do
optional :y
end
end
end
subject.get '/' do
if params[:splines].is_a? Hash
params[:splines][:obj][:y]
elsif params[:splines].any? { |s| s.key? :obj }
'arrays work'
end
end
get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('woof')
get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('arrays work')
get '/', splines: '{"x":4,"ints":[2]}'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
end
it 'accepts Array[JSON] shorthand' do
subject.params do
requires :splines, type: Array[JSON] do
requires :x, type: Integer, values: [1, 2, 3]
requires :y
end
end
subject.get '/' do
params[:splines].first[:y].class.to_s
spline = params[:splines].first
"#{spline[:x].class}.#{spline[:y].class}"
end
get '/', splines: '{"x":"1","y":"woof"}'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq("#{integer_class_name}.String")
get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq("#{integer_class_name}.#{integer_class_name}")
get '/', splines: '{"x":"4","y":"woof"}'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
get '/', splines: '[{"x":"4","y":"woof"}]'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('splines[x] does not have a valid value')
end
it "doesn't make sense using coerce_with" do
expect do
subject.params do
requires :bad, type: JSON, coerce_with: JSON do
requires :x
end
end
end.to raise_error(ArgumentError)
expect do
subject.params do
requires :bad, type: Array[JSON], coerce_with: JSON do
requires :x
end
end
end.to raise_error(ArgumentError)
end
end
context 'multiple types' do
Boolean = Grape::API::Boolean
it 'coerces to first possible type' do
subject.params do
requires :a, types: [Boolean, Integer, String]
end
subject.get '/' do
params[:a].class.to_s
end
get '/', a: 'true'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
get '/', a: '5'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq(integer_class_name)
get '/', a: 'anything else'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('String')
end
it 'fails when no coercion is possible' do
subject.params do
requires :a, types: [Boolean, Integer]
end
subject.get '/' do
params[:a].class.to_s
end
get '/', a: true
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
get '/', a: 'not good'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a is invalid')
end
context 'for primitive collections' do
before do
subject.params do
optional :a, types: [String, Array[String]]
optional :b, types: [Array[Integer], Array[String]]
optional :c, type: Array[Integer, String]
optional :d, types: [Integer, String, Set[Integer, String]]
end
subject.get '/' do
(
params[:a] ||
params[:b] ||
params[:c] ||
params[:d]
).inspect
end
end
it 'allows singular form declaration' do
get '/', a: 'one way'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('"one way"')
get '/', a: %w[the other]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["the", "other"]')
get '/', a: { a: 1, b: 2 }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a is invalid')
get '/', a: [1, 2, 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["1", "2", "3"]')
end
it 'allows multiple collection types' do
get '/', b: [1, 2, 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('[1, 2, 3]')
get '/', b: %w[1 2 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('[1, 2, 3]')
get '/', b: [1, true, 'three']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["1", "true", "three"]')
end
it 'allows collections with multiple types' do
get '/', c: [1, '2', true, 'three']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('[1, 2, "true", "three"]')
get '/', d: '1'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('1')
get '/', d: 'one'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('"one"')
get '/', d: %w[1 two]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
end
end
context 'when params is Hashie::Mash' do
context 'for primitive collections' do
before do
subject.params do
build_with Grape::Extensions::Hashie::Mash::ParamBuilder
optional :a, types: [String, Array[String]]
optional :b, types: [Array[Integer], Array[String]]
optional :c, type: Array[Integer, String]
optional :d, types: [Integer, String, Set[Integer, String]]
end
subject.get '/' do
(
params.a ||
params.b ||
params.c ||
params.d
).inspect
end
end
it 'allows singular form declaration' do
get '/', a: 'one way'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('"one way"')
get '/', a: %w[the other]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
get '/', a: { a: 1, b: 2 }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a is invalid')
get '/', a: [1, 2, 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
end
it 'allows multiple collection types' do
get '/', b: [1, 2, 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
get '/', b: %w[1 2 3]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
get '/', b: [1, true, 'three']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
end
it 'allows collections with multiple types' do
get '/', c: [1, '2', true, 'three']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
get '/', d: '1'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('1')
get '/', d: 'one'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('"one"')
get '/', d: %w[1 two]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('#')
end
end
end
context 'custom coercion rules' do
before do
subject.params do
requires :a, types: [Boolean, String], coerce_with: (lambda do |val|
if val == 'yup'
true
elsif val == 'false'
0
else
val
end
end)
end
subject.get '/' do
params[:a].class.to_s
end
end
it 'respects :coerce_with' do
get '/', a: 'yup'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('TrueClass')
end
it 'still validates type' do
get '/', a: 'false'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a is invalid')
end
it 'performs no additional coercion' do
get '/', a: 'true'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('String')
end
end
it 'may not be supplied together with a single type' do
expect do
subject.params do
requires :a, type: Integer, types: [Integer, String]
end
end.to raise_exception ArgumentError
end
end
context 'converter' do
it 'does not build Virtus::Attribute multiple times' do
subject.params do
requires :something, type: Array[String]
end
subject.get do
end
expect(Virtus::Attribute).to receive(:build).at_most(2).times.and_call_original
10.times { get '/' }
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/except_values_spec.rb 0000644 0000041 0000041 00000017622 13231337007 025572 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::ExceptValuesValidator do
module ValidationsSpec
class ExceptValuesModel
DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
class << self
attr_accessor :excepts
def excepts
@excepts ||= []
[DEFAULT_EXCEPTS + @excepts].flatten.uniq
end
end
end
TEST_CASES = {
req_except: {
requires: { except_values: ExceptValuesModel.excepts },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }
]
},
req_except_hash: {
requires: { except_values: { value: ExceptValuesModel.excepts } },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }
]
},
req_except_custom_message: {
requires: { except_values: { value: ExceptValuesModel.excepts, message: 'is not allowed' } },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json },
{ value: 'invalid-type3', rc: 400, body: { error: 'type is not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }
]
},
req_except_no_value: {
requires: { except_values: { message: 'is not allowed' } },
tests: [
{ value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json }
]
},
req_except_empty: {
requires: { except_values: [] },
tests: [
{ value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json }
]
},
req_except_lambda: {
requires: { except_values: -> { ExceptValuesModel.excepts } },
add_excepts: ['invalid-type4'],
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'invalid-type4', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }
]
},
req_except_lambda_custom_message: {
requires: { except_values: { value: -> { ExceptValuesModel.excepts }, message: 'is not allowed' } },
add_excepts: ['invalid-type4'],
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json },
{ value: 'invalid-type4', rc: 400, body: { error: 'type is not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }
]
},
opt_except_default: {
optional: { except_values: ExceptValuesModel.excepts, default: 'valid-type2' },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json },
{ rc: 200, body: { type: 'valid-type2' }.to_json }
]
},
opt_except_lambda_default: {
optional: { except_values: -> { ExceptValuesModel.excepts }, default: 'valid-type2' },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json },
{ rc: 200, body: { type: 'valid-type2' }.to_json }
]
},
req_except_type_coerce: {
requires: { type: Integer, except_values: [10, 11] },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json },
{ value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: '11', rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: '3', rc: 200, body: { type: 3 }.to_json },
{ value: 3, rc: 200, body: { type: 3 }.to_json }
]
},
opt_except_type_coerce_default: {
optional: { type: Integer, except_values: [10, 11], default: 12 },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json },
{ value: 10, rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: '3', rc: 200, body: { type: 3 }.to_json },
{ value: 3, rc: 200, body: { type: 3 }.to_json },
{ rc: 200, body: { type: 12 }.to_json }
]
},
opt_except_array_type_coerce_default: {
optional: { type: Array[Integer], except_values: [10, 11], default: 12 },
tests: [
{ value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json },
{ value: 10, rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: [10], rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: ['3'], rc: 200, body: { type: [3] }.to_json },
{ value: [3], rc: 200, body: { type: [3] }.to_json },
{ rc: 200, body: { type: 12 }.to_json }
]
},
req_except_range: {
optional: { type: Integer, except_values: 10..12 },
tests: [
{ value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json },
{ value: 13, rc: 200, body: { type: 13 }.to_json }
]
}
}.freeze
module ExceptValidatorSpec
class API < Grape::API
default_format :json
TEST_CASES.each_with_index do |(k, v), _i|
params do
requires :type, v[:requires] if v.key? :requires
optional :type, v[:optional] if v.key? :optional
end
get k do
{ type: params[:type] }
end
end
end
end
end
it 'raises IncompatibleOptionValues on a default value in exclude' do
subject = Class.new(Grape::API)
expect do
subject.params do
optional :type, except_values: ValidationsSpec::ExceptValuesModel.excepts,
default: ValidationsSpec::ExceptValuesModel.excepts.sample
end
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'raises IncompatibleOptionValues when a default array has excluded values' do
subject = Class.new(Grape::API)
expect do
subject.params do
optional :type, type: Array[Integer],
except_values: 10..12,
default: [8, 9, 10]
end
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'raises IncompatibleOptionValues when type is incompatible with values array' do
subject = Class.new(Grape::API)
expect do
subject.params { optional :type, except_values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
def app
ValidationsSpec::ExceptValidatorSpec::API
end
ValidationsSpec::TEST_CASES.each_with_index do |(k, v), i|
v[:tests].each do |t|
it "#{i}: #{k} - #{t[:value]}" do
ValidationsSpec::ExceptValuesModel.excepts = v[:add_excepts] if v.key? :add_excepts
body = {}
body[:type] = t[:value] if t.key? :value
get k.to_s, **body
expect(last_response.status).to eq t[:rc]
expect(last_response.body).to eq t[:body]
ValidationsSpec::ExceptValuesModel.excepts = nil
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/default_spec.rb 0000644 0000041 0000041 00000024216 13231337007 024344 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::DefaultValidator do
module ValidationsSpec
module DefaultValidatorSpec
class API < Grape::API
default_format :json
params do
optional :id
optional :type, default: 'default-type'
end
get '/' do
{ id: params[:id], type: params[:type] }
end
params do
optional :type1, default: 'default-type1'
optional :type2, default: 'default-type2'
end
get '/user' do
{ type1: params[:type1], type2: params[:type2] }
end
params do
requires :id
optional :type1, default: 'default-type1'
optional :type2, default: 'default-type2'
end
get '/message' do
{ id: params[:id], type1: params[:type1], type2: params[:type2] }
end
params do
optional :random, default: -> { Random.rand }
optional :not_random, default: Random.rand
end
get '/numbers' do
{ random_number: params[:random], non_random_number: params[:non_random_number] }
end
params do
optional :array, type: Array do
requires :name
optional :with_default, default: 'default'
end
end
get '/array' do
{ array: params[:array] }
end
params do
requires :thing1
optional :more_things, type: Array do
requires :nested_thing
requires :other_thing, default: 1
end
end
get '/optional_array' do
{ thing1: params[:thing1] }
end
params do
requires :root, type: Hash do
optional :some_things, type: Array do
requires :foo
optional :options, type: Array do
requires :name, type: String
requires :value, type: String
end
end
end
end
get '/nested_optional_array' do
{ root: params[:root] }
end
params do
requires :root, type: Hash do
optional :some_things, type: Array do
requires :foo
optional :options, type: Array do
optional :name, type: String
optional :value, type: String
end
end
end
end
get '/another_nested_optional_array' do
{ root: params[:root] }
end
end
end
end
def app
ValidationsSpec::DefaultValidatorSpec::API
end
it 'lets you leave required values nested inside an optional blank' do
get '/optional_array', thing1: 'stuff'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ thing1: 'stuff' }.to_json)
end
it 'allows optional arrays to be omitted' do
params = { some_things:
[{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] },
{ foo: 'two' },
{ foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] }
get '/nested_optional_array', root: params
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ root: params }.to_json)
end
it 'does not allows faulty optional arrays' do
params = { some_things:
[
{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] },
{ foo: 'two', options: [{ name: 'wat' }] },
{ foo: 'three' }
] }
error = { error: 'root[some_things][1][options][0][value] is missing' }
get '/nested_optional_array', root: params
expect(last_response.status).to eq(400)
expect(last_response.body).to eq(error.to_json)
end
it 'allows optional arrays with optional params' do
params = { some_things:
[
{ foo: 'one', options: [{ value: 'nope' }] },
{ foo: 'two', options: [{ name: 'wat' }] },
{ foo: 'three' }
] }
get '/another_nested_optional_array', root: params
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ root: params }.to_json)
end
it 'set default value for optional param' do
get('/')
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json)
end
it 'set default values for optional params' do
get('/user')
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json)
end
it 'set default values for missing params in the request' do
get('/user?type2=value2')
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json)
end
it 'set default values for optional params and allow to use required fields in the same time' do
get('/message?id=1')
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json)
end
it 'sets lambda based defaults at the time of call' do
get('/numbers')
expect(last_response.status).to eq(200)
before = JSON.parse(last_response.body)
get('/numbers')
expect(last_response.status).to eq(200)
after = JSON.parse(last_response.body)
expect(before['non_random_number']).to eq(after['non_random_number'])
expect(before['random_number']).not_to eq(after['random_number'])
end
it 'sets default values for grouped arrays' do
get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2')
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json)
end
context 'optional group with defaults' do
subject do
Class.new(Grape::API) do
default_format :json
end
end
def app
subject
end
context 'optional array without default value includes optional param with default value' do
before do
subject.params do
optional :optional_array, type: Array do
optional :foo_in_optional_array, default: 'bar'
end
end
subject.post '/optional_array' do
{ optional_array: params[:optional_array] }
end
end
it 'returns nil for optional array if param is not provided' do
post '/optional_array'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq({ optional_array: nil }.to_json)
end
end
context 'optional array with default value includes optional param with default value' do
before do
subject.params do
optional :optional_array_with_default, type: Array, default: [] do
optional :foo_in_optional_array, default: 'bar'
end
end
subject.post '/optional_array_with_default' do
{ optional_array_with_default: params[:optional_array_with_default] }
end
end
it 'sets default value for optional array if param is not provided' do
post '/optional_array_with_default'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq({ optional_array_with_default: [] }.to_json)
end
end
context 'optional hash without default value includes optional param with default value' do
before do
subject.params do
optional :optional_hash_without_default, type: Hash do
optional :foo_in_optional_hash, default: 'bar'
end
end
subject.post '/optional_hash_without_default' do
{ optional_hash_without_default: params[:optional_hash_without_default] }
end
end
it 'returns nil for optional hash if param is not provided' do
post '/optional_hash_without_default'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq({ optional_hash_without_default: nil }.to_json)
end
it 'does not fail even if invalid params is passed to default validator' do
expect { post '/optional_hash_without_default', optional_hash_without_default: '5678' }.not_to raise_error
expect(last_response.status).to eq(400)
expect(last_response.body).to eq({ error: 'optional_hash_without_default is invalid' }.to_json)
end
end
context 'optional hash with default value includes optional param with default value' do
before do
subject.params do
optional :optional_hash_with_default, type: Hash, default: {} do
optional :foo_in_optional_hash, default: 'bar'
end
end
subject.post '/optional_hash_with_default_empty_hash' do
{ optional_hash_with_default: params[:optional_hash_with_default] }
end
subject.params do
optional :optional_hash_with_default, type: Hash, default: { foo_in_optional_hash: 'parent_default' } do
optional :some_param
optional :foo_in_optional_hash, default: 'own_default'
end
end
subject.post '/optional_hash_with_default_inner_params' do
{ foo_in_optional_hash: params[:optional_hash_with_default][:foo_in_optional_hash] }
end
end
it 'sets default value for optional hash if param is not provided' do
post '/optional_hash_with_default_empty_hash'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq({ optional_hash_with_default: {} }.to_json)
end
it 'sets default value from parent defaults for inner param if parent param is not provided' do
post '/optional_hash_with_default_inner_params'
expect(last_response.status).to eq(201)
expect(last_response.body).to eq({ foo_in_optional_hash: 'parent_default' }.to_json)
end
it 'sets own default value for inner param if parent param is provided' do
post '/optional_hash_with_default_inner_params', optional_hash_with_default: { some_param: 'param' }
expect(last_response.status).to eq(201)
expect(last_response.body).to eq({ foo_in_optional_hash: 'own_default' }.to_json)
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/at_least_one_of_spec.rb 0000644 0000041 0000041 00000003665 13231337007 026046 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::AtLeastOneOfValidator do
describe '#validate!' do
let(:scope) do
Struct.new(:opts) do
def params(arg)
arg
end
def required?; end
end
end
let(:at_least_one_of_params) { %i[beer wine grapefruit] }
let(:validator) { described_class.new(at_least_one_of_params, {}, false, scope.new) }
context 'when all restricted params are present' do
let(:params) { { beer: true, wine: true, grapefruit: true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
context 'mixed with other params' do
let(:mixed_params) { params.merge!(other: true, andanother: true) }
it 'does not raise a validation exception' do
expect(validator.validate!(mixed_params)).to eql mixed_params
end
end
end
context 'when a subset of restricted params are present' do
let(:params) { { beer: true, grapefruit: true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
end
context 'when params keys come as strings' do
let(:params) { { 'beer' => true, 'grapefruit' => true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
end
context 'when none of the restricted params is selected' do
let(:params) { { somethingelse: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
context 'when exactly one of the restricted params is selected' do
let(:params) { { beer: true, somethingelse: true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/zh-CN.yml 0000644 0000041 0000041 00000000336 13231337007 023020 0 ustar www-data www-data zh-CN:
grape:
errors:
format: ! '%{attributes}%{message}'
attributes:
age: 年龄
messages:
coerce: 'æ ¼å¼ä¸æ£ç¡®'
presence: '请填写'
regexp: 'æ ¼å¼ä¸æ£ç¡®'
grape-1.0.2/spec/grape/validations/validators/presence_spec.rb 0000644 0000041 0000041 00000020617 13231337007 024525 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::PresenceValidator do
subject do
Class.new(Grape::API) do
format :json
end
end
def app
subject
end
context 'without validation' do
before do
subject.resource :bacons do
get do
'All the bacon'
end
end
end
it 'does not validate for any params' do
get '/bacons'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('All the bacon'.to_json)
end
end
context 'with a custom validation message' do
before do
subject.resource :requires do
params do
requires :email, type: String, allow_blank: { value: false, message: 'has no value' }, regexp: { value: /^\S+$/, message: 'format is invalid' }, message: 'is required'
end
get do
'Hello'
end
end
end
it 'requires when missing' do
get '/requires'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"email is required, email has no value"}')
end
it 'requires when empty' do
get '/requires', email: ''
expect(last_response.body).to eq('{"error":"email has no value, email format is invalid"}')
end
it 'valid when set' do
get '/requires', email: 'bob@example.com'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello'.to_json)
end
end
context 'with a required regexp parameter supplied in the POST body' do
before do
subject.format :json
subject.params do
requires :id, regexp: /^[0-9]+$/
end
subject.post do
{ ret: params[:id] }
end
end
it 'validates id' do
post '/'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"id is missing"}')
io = StringIO.new('{"id" : "a56b"}')
post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length
expect(last_response.body).to eq('{"error":"id is invalid"}')
expect(last_response.status).to eq(400)
io = StringIO.new('{"id" : 56}')
post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length
expect(last_response.body).to eq('{"ret":56}')
expect(last_response.status).to eq(201)
end
end
context 'with a required non-empty string' do
before do
subject.params do
requires :email, type: String, allow_blank: false, regexp: /^\S+$/
end
subject.get do
'Hello'
end
end
it 'requires when missing' do
get '/'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"email is missing, email is empty"}')
end
it 'requires when empty' do
get '/', email: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"email is empty, email is invalid"}')
end
it 'valid when set' do
get '/', email: 'bob@example.com'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello'.to_json)
end
end
context 'with multiple parameters per requires' do
before do
subject.params do
requires :one, :two
end
subject.get '/single-requires' do
'Hello'
end
subject.params do
requires :one
requires :two
end
subject.get '/multiple-requires' do
'Hello'
end
end
it 'validates for all defined params' do
get '/single-requires'
expect(last_response.status).to eq(400)
single_requires_error = last_response.body
get '/multiple-requires'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq(single_requires_error)
end
end
context 'with required parameters and no type' do
before do
subject.params do
requires :name, :company
end
subject.get do
'Hello'
end
end
it 'validates name, company' do
get '/'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"name is missing, company is missing"}')
get '/', name: 'Bob'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"company is missing"}')
get '/', name: 'Bob', company: 'TestCorp'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello'.to_json)
end
end
context 'with nested parameters' do
before do
subject.params do
requires :user, type: Hash do
requires :first_name
requires :last_name
end
end
subject.get '/nested' do
'Nested'
end
end
it 'validates nested parameters' do
get '/nested'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}')
get '/nested', user: { first_name: 'Billy' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user[last_name] is missing"}')
get '/nested', user: { first_name: 'Billy', last_name: 'Bob' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Nested'.to_json)
end
end
context 'with triply nested required parameters' do
before do
subject.params do
requires :admin, type: Hash do
requires :admin_name
requires :super, type: Hash do
requires :user, type: Hash do
requires :first_name
requires :last_name
end
end
end
end
subject.get '/nested_triple' do
'Nested triple'
end
end
it 'validates triple nested parameters' do
get '/nested_triple'
expect(last_response.status).to eq(400)
expect(last_response.body).to include '{"error":"admin is missing'
get '/nested_triple', user: { first_name: 'Billy' }
expect(last_response.status).to eq(400)
expect(last_response.body).to include '{"error":"admin is missing'
get '/nested_triple', admin: { super: { first_name: 'Billy' } }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}')
get '/nested_triple', super: { user: { first_name: 'Billy', last_name: 'Bob' } }
expect(last_response.status).to eq(400)
expect(last_response.body).to include '{"error":"admin is missing'
get '/nested_triple', admin: { super: { user: { first_name: 'Billy' } } }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user][last_name] is missing"}')
get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: 'Billy' } } }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"admin[super][user][last_name] is missing"}')
get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: 'Billy', last_name: 'Bob' } } }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Nested triple'.to_json)
end
end
context 'with reused parameter documentation once required and once optional' do
before do
docs = { name: { type: String, desc: 'some name' } }
subject.params do
requires :all, using: docs
end
subject.get '/required' do
'Hello required'
end
subject.params do
optional :all, using: docs
end
subject.get '/optional' do
'Hello optional'
end
end
it 'works with required' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"name is missing"}')
get '/required', name: 'Bob'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello required'.to_json)
end
it 'works with optional' do
get '/optional'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello optional'.to_json)
get '/optional', name: 'Bob'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hello optional'.to_json)
end
end
end
grape-1.0.2/spec/grape/validations/validators/values_spec.rb 0000644 0000041 0000041 00000052407 13231337007 024222 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::ValuesValidator do
module ValidationsSpec
class ValuesModel
DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'].freeze
DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
class << self
def values
@values ||= []
[DEFAULT_VALUES + @values].flatten.uniq
end
def add_value(value)
@values ||= []
@values << value
end
def excepts
@excepts ||= []
[DEFAULT_EXCEPTS + @excepts].flatten.uniq
end
def add_except(except)
@excepts ||= []
@excepts << except
end
end
end
module ValuesValidatorSpec
class API < Grape::API
default_format :json
resources :custom_message do
params do
requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' }
end
get '/' do
{ type: params[:type] }
end
params do
optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2'
end
get '/lambda' do
{ type: params[:type] }
end
params do
requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' }
end
get '/exclude/exclude_message'
params do
requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' }
end
get '/exclude/lambda/exclude_message'
params do
requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' }
end
get '/exclude/fallback_message'
end
params do
requires :type, values: ValuesModel.values
end
get '/' do
{ type: params[:type] }
end
params do
requires :type, values: []
end
get '/empty'
params do
optional :type, values: { value: ValuesModel.values }, default: 'valid-type2'
end
get '/default/hash/valid' do
{ type: params[:type] }
end
params do
optional :type, values: ValuesModel.values, default: 'valid-type2'
end
get '/default/valid' do
{ type: params[:type] }
end
params do
optional :type, values: { except: ValuesModel.excepts }, default: 'valid-type2'
end
get '/default/except' do
{ type: params[:type] }
end
params do
optional :type, values: -> { ValuesModel.values }, default: 'valid-type2'
end
get '/lambda' do
{ type: params[:type] }
end
params do
requires :type, values: ->(v) { ValuesModel.values.include? v }
end
get '/lambda_val' do
{ type: params[:type] }
end
params do
requires :number, type: Integer, values: ->(v) { v > 0 }
end
get '/lambda_int_val' do
{ number: params[:number] }
end
params do
requires :type, values: -> { [] }
end
get '/empty_lambda'
params do
optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample }
end
get '/default_lambda' do
{ type: params[:type] }
end
params do
optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample }
end
get '/default_and_values_lambda' do
{ type: params[:type] }
end
params do
optional :type, type: Boolean, desc: 'A boolean', values: [true]
end
get '/values/optional_boolean' do
{ type: params[:type] }
end
params do
requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10
end
get '/values/coercion' do
{ type: params[:type] }
end
params do
requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10
end
get '/values/array_coercion' do
{ type: params[:type] }
end
params do
optional :optional, type: Array do
requires :type, values: %w[a b]
end
end
get '/optional_with_required_values'
params do
requires :type, values: { except: ValuesModel.excepts }
end
get '/except/exclusive' do
{ type: params[:type] }
end
params do
requires :type, type: String, values: { except: ValuesModel.excepts }
end
get '/except/exclusive/type' do
{ type: params[:type] }
end
params do
requires :type, values: { except: -> { ValuesModel.excepts } }
end
get '/except/exclusive/lambda' do
{ type: params[:type] }
end
params do
requires :type, type: String, values: { except: -> { ValuesModel.excepts } }
end
get '/except/exclusive/lambda/type' do
{ type: params[:type] }
end
params do
requires :type, type: Integer, values: { except: -> { [3, 4, 5] } }
end
get '/except/exclusive/lambda/coercion' do
{ type: params[:type] }
end
params do
requires :type, type: Integer, values: { value: 1..5, except: [3] }
end
get '/mixed/value/except' do
{ type: params[:type] }
end
params do
optional :optional, type: Array[String], values: %w[a b c]
end
put '/optional_with_array_of_string_values'
params do
requires :type, values: { proc: ->(v) { ValuesModel.values.include? v } }
end
get '/proc' do
{ type: params[:type] }
end
params do
requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' }
end
get '/proc/message'
end
end
end
def app
ValidationsSpec::ValuesValidatorSpec::API
end
context 'with a custom validation message' do
it 'allows a valid value for a parameter' do
get('/custom_message', type: 'valid-type1')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
end
it 'does not allow an invalid value for a parameter' do
get('/custom_message', type: 'invalid-type')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json)
end
it 'validates against values in a proc' do
ValidationsSpec::ValuesModel.add_value('valid-type4')
get('/custom_message/lambda', type: 'valid-type4')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type4' }.to_json)
end
it 'does not allow an invalid value for a parameter using lambda' do
get('/custom_message/lambda', type: 'invalid-type')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json)
end
end
context 'with a custom exclude validation message' do
it 'does not allow an invalid value for a parameter' do
get('/custom_message/exclude/exclude_message', type: 'invalid-type1')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json)
end
end
context 'with a custom exclude validation message' do
it 'does not allow an invalid value for a parameter' do
get('/custom_message/exclude/lambda/exclude_message', type: 'invalid-type1')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json)
end
end
context 'exclude with a standard custom validation message' do
it 'does not allow an invalid value for a parameter' do
get('/custom_message/exclude/fallback_message', type: 'invalid-type1')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type default exclude message' }.to_json)
end
end
it 'allows a valid value for a parameter' do
get('/', type: 'valid-type1')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
end
it 'does not allow an invalid value for a parameter' do
get('/', type: 'invalid-type')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'rejects all values if values is an empty array' do
get('/empty', type: 'invalid-type')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
context 'nil value for a parameter' do
it 'does not allow for root params scope' do
get('/', type: nil)
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'allows for a required param in child scope' do
get('/optional_with_required_values')
expect(last_response.status).to eq 200
end
it 'allows for an optional param with a list of values' do
put('/optional_with_array_of_string_values', optional: nil)
expect(last_response.status).to eq 200
end
end
it 'allows a valid default value' do
get('/default/valid')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
end
it 'allows a default value with except' do
get('/default/except')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
end
it 'allows a valid default value' do
get('/default/hash/valid')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
end
it 'allows a proc for values' do
get('/lambda', type: 'valid-type1')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
end
it 'does not validate updated values without proc' do
ValidationsSpec::ValuesModel.add_value('valid-type4')
get('/', type: 'valid-type4')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'validates against values in a proc' do
ValidationsSpec::ValuesModel.add_value('valid-type4')
get('/lambda', type: 'valid-type4')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type4' }.to_json)
end
it 'does not allow an invalid value for a parameter using lambda' do
get('/lambda', type: 'invalid-type')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'does not allow non-numeric string value for int value using lambda' do
get('/lambda_int_val', number: 'foo')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json)
end
it 'does not allow nil for int value using lambda' do
get('/lambda_int_val', number: nil)
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json)
end
it 'allows numeric string for int value using lambda' do
get('/lambda_int_val', number: '3')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ number: 3 }.to_json)
end
it 'allows value using lambda' do
get('/lambda_val', type: 'valid-type1')
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
end
it 'does not allow invalid value using lambda' do
get('/lambda_val', type: 'invalid-type')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'validates against an empty array in a proc' do
get('/empty_lambda', type: 'any')
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'validates default value from proc' do
get('/default_lambda')
expect(last_response.status).to eq 200
end
it 'validates default value from proc against values in a proc' do
get('/default_and_values_lambda')
expect(last_response.status).to eq 200
end
it 'raises IncompatibleOptionValues on an invalid default value from proc' do
subject = Class.new(Grape::API)
expect do
subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: ValidationsSpec::ValuesModel.values.sample + '_invalid' }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'raises IncompatibleOptionValues on an invalid default value' do
subject = Class.new(Grape::API)
expect do
subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'raises IncompatibleOptionValues when type is incompatible with values array' do
subject = Class.new(Grape::API)
expect do
subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'allows values to be true or false when setting the type to boolean' do
get('/values/optional_boolean', type: true)
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: true }.to_json)
end
it 'allows values to be a kind of the coerced type not just an instance of it' do
get('/values/coercion', type: 10)
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 10 }.to_json)
end
it 'allows values to be a kind of the coerced type in an array' do
get('/values/array_coercion', type: [10])
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: [10] }.to_json)
end
it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do
subject = Class.new(Grape::API)
expect do
subject.params { requires :type, values: [10.5, 11], type: Integer }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
it 'raises IncompatibleOptionValues when except contains a value that is not a kind of the type' do
subject = Class.new(Grape::API)
expect do
subject.params { requires :type, values: { except: [10.5, 11] }, type: Integer }
end.to raise_error Grape::Exceptions::IncompatibleOptionValues
end
context 'with a lambda values' do
subject do
Class.new(Grape::API) do
params do
optional :type, type: String, values: -> { [SecureRandom.uuid] }, default: -> { SecureRandom.uuid }
end
get '/random_values'
end
end
def app
subject
end
before do
expect(SecureRandom).to receive(:uuid).and_return('foo').once
end
it 'only evaluates values dynamically with each request' do
get '/random_values', type: 'foo'
expect(last_response.status).to eq 200
end
it 'chooses default' do
get '/random_values'
expect(last_response.status).to eq 200
end
end
context 'with a range of values' do
subject(:app) do
Class.new(Grape::API) do
params do
optional :value, type: Float, values: 0.0..10.0
end
get '/value' do
{ value: params[:value] }.to_json
end
params do
optional :values, type: Array[Float], values: 0.0..10.0
end
get '/values' do
{ values: params[:values] }.to_json
end
end
end
it 'allows a single value inside of the range' do
get('/value', value: 5.2)
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ value: 5.2 }.to_json)
end
it 'allows an array of values inside of the range' do
get('/values', values: [8.6, 7.5, 3, 0.9])
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ values: [8.6, 7.5, 3.0, 0.9] }.to_json)
end
it 'rejects a single value outside the range' do
get('/value', value: 'a')
expect(last_response.status).to eq 400
expect(last_response.body).to eq('value is invalid, value does not have a valid value')
end
it 'rejects an array of values if any of them are outside the range' do
get('/values', values: [8.6, 75, 3, 0.9])
expect(last_response.status).to eq 400
expect(last_response.body).to eq('values does not have a valid value')
end
end
context 'exclusive excepts' do
it 'allows any other value outside excepts' do
get '/except/exclusive', type: 'value'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'value' }.to_json)
end
it 'allows any other value outside excepts when type is included' do
get '/except/exclusive/type', type: 'value'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'value' }.to_json)
end
it 'rejects values that matches except' do
get '/except/exclusive', type: 'invalid-type1'
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end
it 'rejects an array of values if any of them matches except' do
get '/except/exclusive', type: %w[valid1 valid2 invalid-type1 valid4]
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end
end
context 'exclusive excepts with lambda' do
it 'allows any other value outside excepts when type is included' do
get '/except/exclusive/lambda/type', type: 'value'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'value' }.to_json)
end
it 'allows any other value outside excepts' do
get '/except/exclusive/lambda', type: 'value'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'value' }.to_json)
end
it 'rejects values that matches except' do
get '/except/exclusive/lambda', type: 'invalid-type1'
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end
end
context 'exclusive excepts with lambda and coercion' do
it 'allows any other value outside excepts' do
get '/except/exclusive/lambda/coercion', type: '10010000'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 10_010_000 }.to_json)
end
it 'rejects values that matches except' do
get '/except/exclusive/lambda/coercion', type: '3'
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end
end
context 'with mixed values and excepts' do
it 'allows value, but not in except' do
get '/mixed/value/except', type: 2
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 2 }.to_json)
end
it 'rejects except' do
get '/mixed/value/except', type: 3
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json)
end
it 'rejects outside except and outside value' do
get '/mixed/value/except', type: 10
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
end
context 'custom validation using proc' do
it 'accepts a single valid value' do
get '/proc', type: 'valid-type1'
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
end
it 'accepts multiple valid values' do
get '/proc', type: ['valid-type1', 'valid-type3']
expect(last_response.status).to eq 200
expect(last_response.body).to eq({ type: ['valid-type1', 'valid-type3'] }.to_json)
end
it 'rejects a single invalid value' do
get '/proc', type: 'invalid-type1'
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'rejects an invalid value among valid ones' do
get '/proc', type: ['valid-type1', 'invalid-type1', 'valid-type3']
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
end
it 'uses supplied message' do
get '/proc/message', type: 'invalid-type1'
expect(last_response.status).to eq 400
expect(last_response.body).to eq({ error: 'type failed check' }.to_json)
end
end
end
grape-1.0.2/spec/grape/validations/validators/allow_blank_spec.rb 0000644 0000041 0000041 00000043773 13231337007 025216 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::AllowBlankValidator do
module ValidationsSpec
module AllowBlankValidatorSpec
class API < Grape::API
default_format :json
params do
requires :name, allow_blank: false
end
get '/disallow_blank'
params do
optional :name, type: String, allow_blank: false
end
get '/opt_disallow_string_blank'
params do
optional :name, allow_blank: false
end
get '/disallow_blank_optional_param'
params do
requires :name, allow_blank: true
end
get '/allow_blank'
params do
requires :val, type: DateTime, allow_blank: true
end
get '/allow_datetime_blank'
params do
requires :val, type: DateTime, allow_blank: false
end
get '/disallow_datetime_blank'
params do
requires :val, type: DateTime
end
get '/default_allow_datetime_blank'
params do
requires :val, type: Date, allow_blank: true
end
get '/allow_date_blank'
params do
requires :val, type: Integer, allow_blank: true
end
get '/allow_integer_blank'
params do
requires :val, type: Float, allow_blank: true
end
get '/allow_float_blank'
params do
requires :val, type: Integer, allow_blank: true
end
get '/allow_integer_blank'
params do
requires :val, type: Symbol, allow_blank: true
end
get '/allow_symbol_blank'
params do
requires :val, type: Boolean, allow_blank: true
end
get '/allow_boolean_blank'
params do
requires :val, type: Boolean, allow_blank: false
end
get '/disallow_boolean_blank'
params do
optional :user, type: Hash do
requires :name, allow_blank: false
end
end
get '/disallow_blank_required_param_in_an_optional_group'
params do
optional :user, type: Hash do
requires :name, type: Date, allow_blank: true
end
end
get '/allow_blank_date_param_in_an_optional_group'
params do
optional :user, type: Hash do
optional :name, allow_blank: false
requires :age
end
end
get '/disallow_blank_optional_param_in_an_optional_group'
params do
requires :user, type: Hash do
requires :name, allow_blank: false
end
end
get '/disallow_blank_required_param_in_a_required_group'
params do
requires :user, type: Hash do
requires :name, allow_blank: false
end
end
get '/disallow_string_value_in_a_required_hash_group'
params do
requires :user, type: Hash do
optional :name, allow_blank: false
end
end
get '/disallow_blank_optional_param_in_a_required_group'
params do
optional :user, type: Hash do
optional :name, allow_blank: false
end
end
get '/disallow_string_value_in_an_optional_hash_group'
resources :custom_message do
params do
requires :name, allow_blank: { value: false, message: 'has no value' }
end
get
params do
optional :name, allow_blank: { value: false, message: 'has no value' }
end
get '/disallow_blank_optional_param'
params do
requires :name, allow_blank: true
end
get '/allow_blank'
params do
requires :val, type: DateTime, allow_blank: true
end
get '/allow_datetime_blank'
params do
requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' }
end
get '/disallow_datetime_blank'
params do
requires :val, type: DateTime
end
get '/default_allow_datetime_blank'
params do
requires :val, type: Date, allow_blank: true
end
get '/allow_date_blank'
params do
requires :val, type: Integer, allow_blank: true
end
get '/allow_integer_blank'
params do
requires :val, type: Float, allow_blank: true
end
get '/allow_float_blank'
params do
requires :val, type: Integer, allow_blank: true
end
get '/allow_integer_blank'
params do
requires :val, type: Symbol, allow_blank: true
end
get '/allow_symbol_blank'
params do
requires :val, type: Boolean, allow_blank: true
end
get '/allow_boolean_blank'
params do
requires :val, type: Boolean, allow_blank: { value: false, message: 'has no value' }
end
get '/disallow_boolean_blank'
params do
optional :user, type: Hash do
requires :name, allow_blank: { value: false, message: 'has no value' }
end
end
get '/disallow_blank_required_param_in_an_optional_group'
params do
optional :user, type: Hash do
requires :name, type: Date, allow_blank: true
end
end
get '/allow_blank_date_param_in_an_optional_group'
params do
optional :user, type: Hash do
optional :name, allow_blank: { value: false, message: 'has no value' }
requires :age
end
end
get '/disallow_blank_optional_param_in_an_optional_group'
params do
requires :user, type: Hash do
requires :name, allow_blank: { value: false, message: 'has no value' }
end
end
get '/disallow_blank_required_param_in_a_required_group'
params do
requires :user, type: Hash do
requires :name, allow_blank: { value: false, message: 'has no value' }
end
end
get '/disallow_string_value_in_a_required_hash_group'
params do
requires :user, type: Hash do
optional :name, allow_blank: { value: false, message: 'has no value' }
end
end
get '/disallow_blank_optional_param_in_a_required_group'
params do
optional :user, type: Hash do
optional :name, allow_blank: { value: false, message: 'has no value' }
end
end
get '/disallow_string_value_in_an_optional_hash_group'
end
end
end
end
def app
ValidationsSpec::AllowBlankValidatorSpec::API
end
context 'invalid input' do
it 'refuses empty string' do
get '/disallow_blank', name: ''
expect(last_response.status).to eq(400)
get '/disallow_datetime_blank', val: ''
expect(last_response.status).to eq(400)
end
it 'refuses only whitespaces' do
get '/disallow_blank', name: ' '
expect(last_response.status).to eq(400)
get '/disallow_blank', name: " \n "
expect(last_response.status).to eq(400)
get '/disallow_blank', name: "\n"
expect(last_response.status).to eq(400)
end
it 'refuses nil' do
get '/disallow_blank', name: nil
expect(last_response.status).to eq(400)
end
it 'refuses missing' do
get '/disallow_blank'
expect(last_response.status).to eq(400)
end
end
context 'custom validation message' do
context 'with invalid input' do
it 'refuses empty string' do
get '/custom_message', name: ''
expect(last_response.body).to eq('{"error":"name has no value"}')
end
it 'refuses empty string for an optional param' do
get '/custom_message/disallow_blank_optional_param', name: ''
expect(last_response.body).to eq('{"error":"name has no value"}')
end
it 'refuses only whitespaces' do
get '/custom_message', name: ' '
expect(last_response.body).to eq('{"error":"name has no value"}')
get '/custom_message', name: " \n "
expect(last_response.body).to eq('{"error":"name has no value"}')
get '/custom_message', name: "\n"
expect(last_response.body).to eq('{"error":"name has no value"}')
end
it 'refuses nil' do
get '/custom_message', name: nil
expect(last_response.body).to eq('{"error":"name has no value"}')
end
end
context 'with valid input' do
it 'accepts valid input' do
get '/custom_message', name: 'bob'
expect(last_response.status).to eq(200)
end
it 'accepts empty input when allow_blank is false' do
get '/custom_message/allow_blank', name: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty input' do
get '/custom_message/default_allow_datetime_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when datetime allow_blank' do
get '/custom_message/allow_datetime_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when date allow_blank' do
get '/custom_message/allow_date_blank', val: ''
expect(last_response.status).to eq(200)
end
context 'allow_blank when Numeric' do
it 'accepts empty when integer allow_blank' do
get '/custom_message/allow_integer_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when float allow_blank' do
get '/custom_message/allow_float_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when integer allow_blank' do
get '/custom_message/allow_integer_blank', val: ''
expect(last_response.status).to eq(200)
end
end
it 'accepts empty when symbol allow_blank' do
get '/custom_message/allow_symbol_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when boolean allow_blank' do
get '/custom_message/allow_boolean_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts false when boolean allow_blank' do
get '/custom_message/disallow_boolean_blank', val: false
expect(last_response.status).to eq(200)
end
end
context 'in an optional group' do
context 'as a required param' do
it 'accepts a missing group, even with a disallwed blank param' do
get '/custom_message/disallow_blank_required_param_in_an_optional_group'
expect(last_response.status).to eq(200)
end
it 'accepts a nested missing date value' do
get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' }
expect(last_response.status).to eq(200)
end
it 'refuses a blank value in an existing group' do
get '/custom_message/disallow_blank_required_param_in_an_optional_group', user: { name: '' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user[name] has no value"}')
end
end
context 'as an optional param' do
it 'accepts a missing group, even with a disallwed blank param' do
get '/custom_message/disallow_blank_optional_param_in_an_optional_group'
expect(last_response.status).to eq(200)
end
it 'accepts a nested missing optional value' do
get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' }
expect(last_response.status).to eq(200)
end
it 'refuses a blank existing value in an existing scope' do
get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user[name] has no value"}')
end
end
end
context 'in a required group' do
context 'as a required param' do
it 'refuses a blank value in a required existing group' do
get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user[name] has no value"}')
end
it 'refuses a string value in a required hash group' do
get '/custom_message/disallow_string_value_in_a_required_hash_group', user: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user is invalid, user[name] is missing"}')
end
end
context 'as an optional param' do
it 'accepts a nested missing value' do
get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' }
expect(last_response.status).to eq(200)
end
it 'refuses a blank existing value in an existing scope' do
get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user[name] has no value"}')
end
it 'refuses a string value in an optional hash group' do
get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"user is invalid"}')
end
end
end
end
context 'valid input' do
it 'allows missing optional strings' do
get 'opt_disallow_string_blank'
expect(last_response.status).to eq(200)
end
it 'accepts valid input' do
get '/disallow_blank', name: 'bob'
expect(last_response.status).to eq(200)
end
it 'accepts empty input when allow_blank is false' do
get '/allow_blank', name: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty input' do
get '/default_allow_datetime_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when datetime allow_blank' do
get '/allow_datetime_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when date allow_blank' do
get '/allow_date_blank', val: ''
expect(last_response.status).to eq(200)
end
context 'allow_blank when Numeric' do
it 'accepts empty when integer allow_blank' do
get '/allow_integer_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when float allow_blank' do
get '/allow_float_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when integer allow_blank' do
get '/allow_integer_blank', val: ''
expect(last_response.status).to eq(200)
end
end
it 'accepts empty when symbol allow_blank' do
get '/allow_symbol_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts empty when boolean allow_blank' do
get '/allow_boolean_blank', val: ''
expect(last_response.status).to eq(200)
end
it 'accepts false when boolean allow_blank' do
get '/disallow_boolean_blank', val: false
expect(last_response.status).to eq(200)
end
it 'accepts value when time allow_blank' do
get '/disallow_datetime_blank', val: Time.now
expect(last_response.status).to eq(200)
end
end
context 'in an optional group' do
context 'as a required param' do
it 'accepts a missing group, even with a disallwed blank param' do
get '/disallow_blank_required_param_in_an_optional_group'
expect(last_response.status).to eq(200)
end
it 'accepts a nested missing date value' do
get '/allow_blank_date_param_in_an_optional_group', user: { name: '' }
expect(last_response.status).to eq(200)
end
it 'refuses a blank value in an existing group' do
get '/disallow_blank_required_param_in_an_optional_group', user: { name: '' }
expect(last_response.status).to eq(400)
end
end
context 'as an optional param' do
it 'accepts a missing group, even with a disallwed blank param' do
get '/disallow_blank_optional_param_in_an_optional_group'
expect(last_response.status).to eq(200)
end
it 'accepts a nested missing optional value' do
get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' }
expect(last_response.status).to eq(200)
end
it 'refuses a blank existing value in an existing scope' do
get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' }
expect(last_response.status).to eq(400)
end
end
end
context 'in a required group' do
context 'as a required param' do
it 'refuses a blank value in a required existing group' do
get '/disallow_blank_required_param_in_a_required_group', user: { name: '' }
expect(last_response.status).to eq(400)
end
it 'refuses a string value in a required hash group' do
get '/disallow_string_value_in_a_required_hash_group', user: ''
expect(last_response.status).to eq(400)
end
end
context 'as an optional param' do
it 'accepts a nested missing value' do
get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29' }
expect(last_response.status).to eq(200)
end
it 'refuses a blank existing value in an existing scope' do
get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' }
expect(last_response.status).to eq(400)
end
it 'refuses a string value in an optional hash group' do
get '/disallow_string_value_in_an_optional_hash_group', user: ''
expect(last_response.status).to eq(400)
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/regexp_spec.rb 0000644 0000041 0000041 00000011475 13231337007 024215 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::RegexpValidator do
module ValidationsSpec
module RegexpValidatorSpec
class API < Grape::API
default_format :json
resources :custom_message do
params do
requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' }
end
get do
end
params do
requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' }
end
get 'regexp_with_array' do
end
end
params do
requires :name, regexp: /^[a-z]+$/
end
get do
end
params do
requires :names, type: Array[String], regexp: /^[a-z]+$/
end
get 'regexp_with_array' do
end
params do
requires :people, type: Hash do
requires :names, type: Array[String], regexp: /^[a-z]+$/
end
end
get 'nested_regexp_with_array' do
end
end
end
end
def app
ValidationsSpec::RegexpValidatorSpec::API
end
context 'custom validation message' do
context 'with invalid input' do
it 'refuses inapppopriate' do
get '/custom_message', name: 'invalid name'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"name format is invalid"}')
end
it 'refuses empty' do
get '/custom_message', name: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"name format is invalid"}')
end
end
it 'accepts nil' do
get '/custom_message', name: nil
expect(last_response.status).to eq(200)
end
it 'accepts valid input' do
get '/custom_message', name: 'bob'
expect(last_response.status).to eq(200)
end
context 'regexp with array' do
it 'refuses inapppopriate items' do
get '/custom_message/regexp_with_array', names: ['invalid name', 'abc']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"names format is invalid"}')
end
it 'refuses empty items' do
get '/custom_message/regexp_with_array', names: ['', 'abc']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"names format is invalid"}')
end
it 'refuses nil items' do
get '/custom_message/regexp_with_array', names: [nil, 'abc']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"names can\'t be nil"}')
end
it 'accepts valid items' do
get '/custom_message/regexp_with_array', names: ['bob']
expect(last_response.status).to eq(200)
end
it 'accepts nil instead of array' do
get '/custom_message/regexp_with_array', names: nil
expect(last_response.status).to eq(200)
end
end
end
context 'invalid input' do
it 'refuses inapppopriate' do
get '/', name: 'invalid name'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"name is invalid"}')
end
it 'refuses empty' do
get '/', name: ''
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"name is invalid"}')
end
end
it 'accepts nil' do
get '/', name: nil
expect(last_response.status).to eq(200)
end
it 'accepts valid input' do
get '/', name: 'bob'
expect(last_response.status).to eq(200)
end
context 'regexp with array' do
it 'refuses inapppopriate items' do
get '/regexp_with_array', names: ['invalid name', 'abc']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"names is invalid"}')
end
it 'refuses empty items' do
get '/regexp_with_array', names: ['', 'abc']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"names is invalid"}')
end
it 'refuses nil items' do
get '/regexp_with_array', names: [nil, 'abc']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"names is invalid"}')
end
it 'accepts valid items' do
get '/regexp_with_array', names: ['bob']
expect(last_response.status).to eq(200)
end
it 'accepts nil instead of array' do
get '/regexp_with_array', names: nil
expect(last_response.status).to eq(200)
end
end
context 'nested regexp with array' do
it 'refuses inapppopriate' do
get '/nested_regexp_with_array', people: 'invalid name'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}')
end
end
end
grape-1.0.2/spec/grape/validations/validators/mutual_exclusion_spec.rb 0000644 0000041 0000041 00000003453 13231337007 026320 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::MutualExclusionValidator do
describe '#validate!' do
let(:scope) do
Struct.new(:opts) do
def params(arg)
arg
end
end
end
let(:mutually_exclusive_params) { %i[beer wine grapefruit] }
let(:validator) { described_class.new(mutually_exclusive_params, {}, false, scope.new) }
context 'when all mutually exclusive params are present' do
let(:params) { { beer: true, wine: true, grapefruit: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
context 'mixed with other params' do
let(:mixed_params) { params.merge!(other: true, andanother: true) }
it 'still raises a validation exception' do
expect do
validator.validate! mixed_params
end.to raise_error(Grape::Exceptions::Validation)
end
end
end
context 'when a subset of mutually exclusive params are present' do
let(:params) { { beer: true, grapefruit: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
context 'when params keys come as strings' do
let(:params) { { 'beer' => true, 'grapefruit' => true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
context 'when no mutually exclusive params are present' do
let(:params) { { beer: true, somethingelse: true } }
it 'params' do
expect(validator.validate!(params)).to eql params
end
end
end
end
grape-1.0.2/spec/grape/validations/validators/all_or_none_spec.rb 0000644 0000041 0000041 00000003353 13231337007 025206 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations::AllOrNoneOfValidator do
describe '#validate!' do
let(:scope) do
Struct.new(:opts) do
def params(arg)
arg
end
def required?; end
end
end
let(:all_or_none_params) { %i[beer wine grapefruit] }
let(:validator) { described_class.new(all_or_none_params, {}, false, scope.new) }
context 'when all restricted params are present' do
let(:params) { { beer: true, wine: true, grapefruit: true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
context 'mixed with other params' do
let(:mixed_params) { params.merge!(other: true, andanother: true) }
it 'does not raise a validation exception' do
expect(validator.validate!(mixed_params)).to eql mixed_params
end
end
end
context 'when none of the restricted params is selected' do
let(:params) { { somethingelse: true } }
it 'does not raise a validation exception' do
expect(validator.validate!(params)).to eql params
end
end
context 'when only a subset of restricted params are present' do
let(:params) { { beer: true, grapefruit: true } }
it 'raises a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
context 'mixed with other params' do
let(:mixed_params) { params.merge!(other: true, andanother: true) }
it 'raise a validation exception' do
expect do
validator.validate! params
end.to raise_error(Grape::Exceptions::Validation)
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/ 0000755 0000041 0000041 00000000000 13231337007 015451 5 ustar www-data www-data grape-1.0.2/spec/grape/dsl/desc_spec.rb 0000644 0000041 0000041 00000005102 13231337007 017724 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module DescSpec
class Dummy
extend Grape::DSL::Desc
end
end
describe Desc do
subject { Class.new(DescSpec::Dummy) }
describe '.desc' do
it 'sets a description' do
desc_text = 'The description'
options = { message: 'none' }
subject.desc desc_text, options
expect(subject.namespace_setting(:description)).to eq(options.merge(description: desc_text))
expect(subject.route_setting(:description)).to eq(options.merge(description: desc_text))
end
it 'can be set with a block' do
expected_options = {
description: 'The description',
detail: 'more details',
params: { first: :param },
entity: Object,
http_codes: [[401, 'Unauthorized', 'Entities::Error']],
named: 'My named route',
headers: [XAuthToken: {
description: 'Valdates your identity',
required: true
},
XOptionalHeader: {
description: 'Not really needed',
required: false
}]
}
subject.desc 'The description' do
detail 'more details'
params(first: :param)
success Object
failure [[401, 'Unauthorized', 'Entities::Error']]
named 'My named route'
headers [XAuthToken: {
description: 'Valdates your identity',
required: true
},
XOptionalHeader: {
description: 'Not really needed',
required: false
}]
end
expect(subject.namespace_setting(:description)).to eq(expected_options)
expect(subject.route_setting(:description)).to eq(expected_options)
end
it 'can be set with options and a block' do
expect(subject).to receive(:warn).with('[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.')
desc_text = 'The description'
detail_text = 'more details'
options = { message: 'none' }
subject.desc desc_text, options do
detail detail_text
end
expect(subject.namespace_setting(:description)).to eq(description: desc_text, detail: detail_text)
expect(subject.route_setting(:description)).to eq(description: desc_text, detail: detail_text)
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/request_response_spec.rb 0000644 0000041 0000041 00000020543 13231337007 022422 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module RequestResponseSpec
class Dummy
include Grape::DSL::RequestResponse
def self.set(key, value)
settings[key.to_sym] = value
end
def self.imbue(key, value)
settings.imbue(key, value)
end
end
end
describe RequestResponse do
subject { Class.new(RequestResponseSpec::Dummy) }
let(:c_type) { 'application/json' }
let(:format) { 'txt' }
describe '.default_format' do
it 'sets the default format' do
expect(subject).to receive(:namespace_inheritable).with(:default_format, :format)
subject.default_format :format
end
it 'returns the format without paramter' do
subject.default_format :format
expect(subject.default_format).to eq :format
end
end
describe '.format' do
it 'sets a new format' do
expect(subject).to receive(:namespace_inheritable).with(:format, format.to_sym)
expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Txt)
subject.format format
end
end
describe '.formatter' do
it 'sets the formatter for a content type' do
expect(subject).to receive(:namespace_stackable).with(:formatters, c_type.to_sym => :formatter)
subject.formatter c_type, :formatter
end
end
describe '.parser' do
it 'sets a parser for a content type' do
expect(subject).to receive(:namespace_stackable).with(:parsers, c_type.to_sym => :parser)
subject.parser c_type, :parser
end
end
describe '.default_error_formatter' do
it 'sets a new error formatter' do
expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Json)
subject.default_error_formatter :json
end
end
describe '.error_formatter' do
it 'sets a error_formatter' do
format = 'txt'
expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter)
subject.error_formatter format, :error_formatter
end
it 'understands syntactic sugar' do
expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter)
subject.error_formatter format, with: :error_formatter
end
end
describe '.content_type' do
it 'sets a content type for a format' do
expect(subject).to receive(:namespace_stackable).with(:content_types, format.to_sym => c_type)
subject.content_type format, c_type
end
end
describe '.content_types' do
it 'returns all content types' do
expect(subject.content_types).to eq(xml: 'application/xml',
serializable_hash: 'application/json',
json: 'application/json',
txt: 'text/plain',
binary: 'application/octet-stream')
end
end
describe '.default_error_status' do
it 'sets a default error status' do
expect(subject).to receive(:namespace_inheritable).with(:default_error_status, 500)
subject.default_error_status 500
end
end
describe '.rescue_from' do
describe ':all' do
it 'sets rescue all to true' do
expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true)
expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, nil)
subject.rescue_from :all
end
it 'sets given proc as rescue handler' do
rescue_handler_proc = proc {}
expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true)
expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc)
subject.rescue_from :all, rescue_handler_proc
end
it 'sets given block as rescue handler' do
rescue_handler_proc = proc {}
expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true)
expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc)
subject.rescue_from :all, &rescue_handler_proc
end
it 'sets a rescue handler declared through :with option' do
with_block = -> { 'hello' }
expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true)
expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, an_instance_of(Proc))
subject.rescue_from :all, with: with_block
end
it 'abort if :with option value is not Symbol, String or Proc' do
expect { subject.rescue_from :all, with: 1234 }.to raise_error(ArgumentError, "with: #{integer_class_name}, expected Symbol, String or Proc")
end
it 'abort if both :with option and block are passed' do
expect do
subject.rescue_from :all, with: -> { 'hello' } do
error!('bye')
end
end.to raise_error(ArgumentError, 'both :with option and block cannot be passed')
end
end
describe ':grape_exceptions' do
it 'sets rescue all to true' do
expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true)
expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true)
subject.rescue_from :grape_exceptions
end
it 'sets rescue_grape_exceptions to true' do
expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true)
expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true)
subject.rescue_from :grape_exceptions
end
end
describe 'list of exceptions is passed' do
it 'sets hash of exceptions as rescue handlers' do
expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => nil)
expect(subject).to receive(:namespace_stackable).with(:rescue_options, {})
subject.rescue_from StandardError
end
it 'rescues only base handlers if rescue_subclasses: false option is passed' do
expect(subject).to receive(:namespace_reverse_stackable).with(:base_only_rescue_handlers, StandardError => nil)
expect(subject).to receive(:namespace_stackable).with(:rescue_options, rescue_subclasses: false)
subject.rescue_from StandardError, rescue_subclasses: false
end
it 'sets given proc as rescue handler for each key in hash' do
rescue_handler_proc = proc {}
expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => rescue_handler_proc)
expect(subject).to receive(:namespace_stackable).with(:rescue_options, {})
subject.rescue_from StandardError, rescue_handler_proc
end
it 'sets given block as rescue handler for each key in hash' do
rescue_handler_proc = proc {}
expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => rescue_handler_proc)
expect(subject).to receive(:namespace_stackable).with(:rescue_options, {})
subject.rescue_from StandardError, &rescue_handler_proc
end
it 'sets a rescue handler declared through :with option for each key in hash' do
with_block = -> { 'hello' }
expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => an_instance_of(Proc))
expect(subject).to receive(:namespace_stackable).with(:rescue_options, {})
subject.rescue_from StandardError, with: with_block
end
end
end
describe '.represent' do
it 'sets a presenter for a class' do
presenter = Class.new
expect(subject).to receive(:namespace_stackable).with(:representations, ThisClass: presenter)
subject.represent :ThisClass, with: presenter
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/parameters_spec.rb 0000644 0000041 0000041 00000013137 13231337007 021160 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module ParametersSpec
class Dummy
include Grape::DSL::Parameters
attr_accessor :api, :element, :parent
def validate_attributes(*args)
@validate_attributes = *args
end
def validate_attributes_reader
@validate_attributes
end
def push_declared_params(args, **_opts)
@push_declared_params = args
end
def push_declared_params_reader
@push_declared_params
end
def validates(*args)
@validates = *args
end
def validates_reader
@validates
end
def new_group_scope(args)
@group = args.clone.first
yield
end
def extract_message_option(attrs)
return nil unless attrs.is_a?(Array)
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
end
end
end
describe Parameters do
subject { ParametersSpec::Dummy.new }
describe '#use' do
before do
allow_message_expectations_on_nil
allow(subject.api).to receive(:namespace_stackable).with(:named_params)
end
let(:options) { { option: 'value' } }
let(:named_params) { { params_group: proc {} } }
it 'calls processes associated with named params' do
allow(subject.api).to receive(:namespace_stackable_with_hash).and_return(named_params)
expect(subject).to receive(:instance_exec).with(options).and_yield
subject.use :params_group, options
end
it 'raises error when non-existent named param is called' do
allow(subject.api).to receive(:namespace_stackable_with_hash).and_return({})
expect { subject.use :params_group }.to raise_error('Params :params_group not found!')
end
end
describe '#use_scope' do
it 'is alias to #use' do
expect(subject.method(:use_scope)).to eq subject.method(:use)
end
end
describe '#includes' do
it 'is alias to #use' do
expect(subject.method(:includes)).to eq subject.method(:use)
end
end
describe '#requires' do
it 'adds a required parameter' do
subject.requires :id, type: Integer, desc: 'Identity.'
expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }])
expect(subject.push_declared_params_reader).to eq([:id])
end
end
describe '#optional' do
it 'adds an optional parameter' do
subject.optional :id, type: Integer, desc: 'Identity.'
expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }])
expect(subject.push_declared_params_reader).to eq([:id])
end
end
describe '#with' do
it 'creates a scope with group attributes' do
subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' }
expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }])
expect(subject.push_declared_params_reader).to eq([:id])
end
end
describe '#mutually_exclusive' do
it 'adds an mutally exclusive parameter validation' do
subject.mutually_exclusive :media, :audio
expect(subject.validates_reader).to eq([%i[media audio], { mutual_exclusion: { value: true, message: nil } }])
end
end
describe '#exactly_one_of' do
it 'adds an exactly of one parameter validation' do
subject.exactly_one_of :media, :audio
expect(subject.validates_reader).to eq([%i[media audio], { exactly_one_of: { value: true, message: nil } }])
end
end
describe '#at_least_one_of' do
it 'adds an at least one of parameter validation' do
subject.at_least_one_of :media, :audio
expect(subject.validates_reader).to eq([%i[media audio], { at_least_one_of: { value: true, message: nil } }])
end
end
describe '#all_or_none_of' do
it 'adds an all or none of parameter validation' do
subject.all_or_none_of :media, :audio
expect(subject.validates_reader).to eq([%i[media audio], { all_or_none_of: { value: true, message: nil } }])
end
end
describe '#group' do
it 'is alias to #requires' do
expect(subject.method(:group)).to eq subject.method(:requires)
end
end
describe '#params' do
it 'inherits params from parent' do
parent_params = { foo: 'bar' }
subject.parent = Object.new
allow(subject.parent).to receive(:params).and_return(parent_params)
expect(subject.params({})).to eq parent_params
end
describe 'when params argument is an array of hashes' do
it 'returns values of each hash for @element key' do
subject.element = :foo
expect(subject.params([{ foo: 'bar' }, { foo: 'baz' }])).to eq(%w[bar baz])
end
end
describe 'when params argument is a hash' do
it 'returns value for @element key' do
subject.element = :foo
expect(subject.params(foo: 'bar')).to eq('bar')
end
end
describe 'when params argument is not a array or a hash' do
it 'returns empty hash' do
subject.element = Object.new
expect(subject.params(Object.new)).to eq({})
end
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/routing_spec.rb 0000644 0000041 0000041 00000020516 13231337007 020503 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module RoutingSpec
class Dummy
include Grape::DSL::Routing
end
end
describe Routing do
subject { Class.new(RoutingSpec::Dummy) }
let(:proc) { ->() {} }
let(:options) { { a: :b } }
let(:path) { '/dummy' }
describe '.version' do
it 'sets a version for route' do
version = 'v1'
expect(subject).to receive(:namespace_inheritable).with(:version, [version])
expect(subject).to receive(:namespace_inheritable).with(:version_options, using: :path)
expect(subject.version(version)).to eq(version)
end
end
describe '.prefix' do
it 'sets a prefix for route' do
prefix = '/api'
expect(subject).to receive(:namespace_inheritable).with(:root_prefix, prefix)
subject.prefix prefix
end
end
describe '.scope' do
it 'create a scope without affecting the URL' do
expect(subject).to receive(:within_namespace)
subject.scope {}
end
end
describe '.do_not_route_head!' do
it 'sets do not route head option' do
expect(subject).to receive(:namespace_inheritable).with(:do_not_route_head, true)
subject.do_not_route_head!
end
end
describe '.do_not_route_options!' do
it 'sets do not route options option' do
expect(subject).to receive(:namespace_inheritable).with(:do_not_route_options, true)
subject.do_not_route_options!
end
end
describe '.mount' do
it 'mounts on a nested path' do
subject = Class.new(Grape::API)
app1 = Class.new(Grape::API)
app2 = Class.new(Grape::API)
app2.get '/nice' do
'play'
end
subject.mount app1 => '/app1'
app1.mount app2 => '/app2'
expect(subject.inheritable_setting.to_hash[:namespace]).to eq({})
expect(subject.inheritable_setting.to_hash[:namespace_inheritable]).to eq({})
expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1'])
expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1', '/app2'])
end
end
describe '.route' do
before do
allow(subject).to receive(:endpoints).and_return([])
allow(subject).to receive(:route_end)
allow(subject).to receive(:reset_validations!)
end
it 'marks end of the route' do
expect(subject).to receive(:route_end)
subject.route(:any)
end
it 'resets validations' do
expect(subject).to receive(:reset_validations!)
subject.route(:any)
end
it 'defines a new endpoint' do
expect { subject.route(:any) }
.to change { subject.endpoints.count }.from(0).to(1)
end
it 'does not duplicate identical endpoints' do
subject.route(:any)
expect { subject.route(:any) }
.to_not change(subject.endpoints, :count)
end
it 'generates correct endpoint options' do
allow(subject).to receive(:route_setting).with(:description).and_return(fiz: 'baz')
allow(subject).to receive(:namespace_stackable_with_hash).and_return(nuz: 'naz')
expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options|
expect(endpoint_options[:method]).to eq :get
expect(endpoint_options[:path]).to eq '/foo'
expect(endpoint_options[:for]).to eq subject
expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz', params: { nuz: 'naz' })
end.and_yield
subject.route(:get, '/foo', { foo: 'bar' }, &proc {})
end
end
describe '.get' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('GET', path, options)
subject.get path, options, &proc
end
end
describe '.post' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('POST', path, options)
subject.post path, options, &proc
end
end
describe '.put' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('PUT', path, options)
subject.put path, options, &proc
end
end
describe '.head' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('HEAD', path, options)
subject.head path, options, &proc
end
end
describe '.delete' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('DELETE', path, options)
subject.delete path, options, &proc
end
end
describe '.options' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('OPTIONS', path, options)
subject.options path, options, &proc
end
end
describe '.patch' do
it 'delegates to .route' do
expect(subject).to receive(:route).with('PATCH', path, options)
subject.patch path, options, &proc
end
end
describe '.namespace' do
let(:new_namespace) { Object.new }
it 'creates a new namespace with given name and options' do
expect(subject).to receive(:within_namespace).and_yield
expect(subject).to receive(:nest).and_yield
expect(Namespace).to receive(:new).with(:foo, foo: 'bar').and_return(new_namespace)
expect(subject).to receive(:namespace_stackable).with(:namespace, new_namespace)
subject.namespace :foo, foo: 'bar', &proc {}
end
it 'calls #joined_space_path on Namespace' do
result_of_namspace_stackable = Object.new
allow(subject).to receive(:namespace_stackable).and_return(result_of_namspace_stackable)
expect(Namespace).to receive(:joined_space_path).with(result_of_namspace_stackable)
subject.namespace
end
end
describe '.group' do
it 'is alias to #namespace' do
expect(subject.method(:group)).to eq subject.method(:namespace)
end
end
describe '.resource' do
it 'is alias to #namespace' do
expect(subject.method(:resource)).to eq subject.method(:namespace)
end
end
describe '.resources' do
it 'is alias to #namespace' do
expect(subject.method(:resources)).to eq subject.method(:namespace)
end
end
describe '.segment' do
it 'is alias to #namespace' do
expect(subject.method(:segment)).to eq subject.method(:namespace)
end
end
describe '.routes' do
let(:routes) { Object.new }
it 'returns value received from #prepare_routes' do
expect(subject).to receive(:prepare_routes).and_return(routes)
expect(subject.routes).to eq routes
end
context 'when #routes was already called once' do
before do
allow(subject).to receive(:prepare_routes).and_return(routes)
subject.routes
end
it 'it does not call prepare_routes again' do
expect(subject).to_not receive(:prepare_routes)
expect(subject.routes).to eq routes
end
end
end
describe '.route_param' do
it 'calls #namespace with given params' do
expect(subject).to receive(:namespace).with(':foo', {}).and_yield
subject.route_param('foo', {}, &proc {})
end
let(:regex) { /(.*)/ }
let!(:options) { { requirements: regex } }
it 'nests requirements option under param name' do
expect(subject).to receive(:namespace) do |_param, options|
expect(options[:requirements][:foo]).to eq regex
end
subject.route_param('foo', options, &proc {})
end
it 'does not modify options parameter' do
allow(subject).to receive(:namespace)
expect { subject.route_param('foo', options, &proc {}) }
.to_not change { options }
end
end
describe '.versions' do
it 'returns last defined version' do
subject.version 'v1'
subject.version 'v2'
expect(subject.version).to eq('v2')
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/configuration_spec.rb 0000644 0000041 0000041 00000000402 13231337007 021653 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module ConfigurationSpec
class Dummy
include Grape::DSL::Configuration
end
end
describe Configuration do
subject { Class.new(ConfigurationSpec::Dummy) }
end
end
end
grape-1.0.2/spec/grape/dsl/logger_spec.rb 0000644 0000041 0000041 00000001017 13231337007 020266 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module LoggerSpec
class Dummy
extend Grape::DSL::Logger
end
end
describe Logger do
subject { Class.new(LoggerSpec::Dummy) }
let(:logger) { double(:logger) }
describe '.logger' do
it 'sets a logger' do
subject.logger logger
expect(subject.logger).to eq logger
end
it 'returns a logger' do
expect(subject.logger(logger)).to eq logger
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/helpers_spec.rb 0000644 0000041 0000041 00000005303 13231337007 020453 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module HelpersSpec
class Dummy
include Grape::DSL::Helpers
def self.mods
namespace_stackable(:helpers)
end
def self.first_mod
mods.first
end
end
end
module BooleanParam
extend Grape::API::Helpers
params :requires_toggle_prm do
requires :toggle_prm, type: Boolean
end
end
class Base < Grape::API
helpers BooleanParam
end
class Child < Base; end
describe Helpers do
subject { Class.new(HelpersSpec::Dummy) }
let(:proc) do
lambda do |*|
def test
:test
end
end
end
describe '.helpers' do
it 'adds a module with the given block' do
expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original
expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original
subject.helpers(&proc)
expect(subject.first_mod.instance_methods).to include(:test)
end
it 'uses provided modules' do
mod = Module.new
expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(2).times
expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original
subject.helpers(mod, &proc)
expect(subject.first_mod).to eq mod
end
it 'uses many provided modules' do
mod = Module.new
mod2 = Module.new
mod3 = Module.new
expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(4).times
expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original.exactly(3).times
subject.helpers(mod, mod2, mod3, &proc)
expect(subject.mods).to include(mod)
expect(subject.mods).to include(mod2)
expect(subject.mods).to include(mod3)
end
context 'with an external file' do
it 'sets Boolean as a Virtus::Attribute::Boolean' do
subject.helpers BooleanParam
expect(subject.first_mod::Boolean).to eq Virtus::Attribute::Boolean
end
end
context 'in child classes' do
it 'is available' do
klass = Child
expect do
klass.instance_eval do
params do
use :requires_toggle_prm
end
end
end.to_not raise_exception
end
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/validations_spec.rb 0000644 0000041 0000041 00000003751 13231337007 021333 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module ValidationsSpec
class Dummy
include Grape::DSL::Validations
end
end
describe Validations do
subject { ValidationsSpec::Dummy }
describe '.reset_validations!' do
before do
subject.namespace_stackable :declared_params, ['dummy']
subject.namespace_stackable :validations, ['dummy']
subject.namespace_stackable :params, ['dummy']
subject.route_setting :description, description: 'lol', params: ['dummy']
subject.reset_validations!
end
after do
subject.unset_route_setting :description
end
it 'resets declared params' do
expect(subject.namespace_stackable(:declared_params)).to eq []
end
it 'resets validations' do
expect(subject.namespace_stackable(:validations)).to eq []
end
it 'resets params' do
expect(subject.namespace_stackable(:params)).to eq []
end
it 'resets documentation params' do
expect(subject.route_setting(:description)[:params]).to be_nil
end
it 'does not reset documentation description' do
expect(subject.route_setting(:description)[:description]).to eq 'lol'
end
end
describe '.params' do
it 'returns a ParamsScope' do
expect(subject.params).to be_a Grape::Validations::ParamsScope
end
it 'evaluates block' do
expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo'
end
end
describe '.document_attribute' do
before do
subject.document_attribute([full_name: 'xxx'], foo: 'bar')
end
it 'creates a param documentation' do
expect(subject.namespace_stackable(:params)).to eq(['xxx' => { foo: 'bar' }])
expect(subject.route_setting(:description)).to eq(params: { 'xxx' => { foo: 'bar' } })
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/inside_route_spec.rb 0000644 0000041 0000041 00000022773 13231337007 021514 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module InsideRouteSpec
class Dummy
include Grape::DSL::InsideRoute
attr_reader :env, :request, :new_settings
def initialize
@env = {}
@header = {}
@new_settings = { namespace_inheritable: {}, namespace_stackable: {} }
end
end
end
end
end
describe Grape::Endpoint do
subject { Grape::DSL::InsideRouteSpec::Dummy.new }
describe '#version' do
it 'defaults to nil' do
expect(subject.version).to be nil
end
it 'returns env[api.version]' do
subject.env['api.version'] = 'dummy'
expect(subject.version).to eq 'dummy'
end
end
describe '#error!' do
it 'throws :error' do
expect { subject.error! 'Not Found', 404 }.to throw_symbol(:error)
end
describe 'thrown' do
before do
catch(:error) { subject.error! 'Not Found', 404 }
end
it 'sets status' do
expect(subject.status).to eq 404
end
end
describe 'default_error_status' do
before do
subject.namespace_inheritable(:default_error_status, 500)
catch(:error) { subject.error! 'Unknown' }
end
it 'sets status to default_error_status' do
expect(subject.status).to eq 500
end
end
# self.status(status || settings[:default_error_status])
# throw :error, message: message, status: self.status, headers: headers
end
describe '#redirect' do
describe 'default' do
before do
subject.redirect '/'
end
it 'sets status to 302' do
expect(subject.status).to eq 302
end
it 'sets location header' do
expect(subject.header['Location']).to eq '/'
end
end
describe 'permanent' do
before do
subject.redirect '/', permanent: true
end
it 'sets status to 301' do
expect(subject.status).to eq 301
end
it 'sets location header' do
expect(subject.header['Location']).to eq '/'
end
end
end
describe '#status' do
%w[GET PUT OPTIONS].each do |method|
it 'defaults to 200 on GET' do
request = Grape::Request.new(Rack::MockRequest.env_for('/', method: method))
expect(subject).to receive(:request).and_return(request)
expect(subject.status).to eq 200
end
end
it 'defaults to 201 on POST' do
request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'POST'))
expect(subject).to receive(:request).and_return(request)
expect(subject.status).to eq 201
end
it 'defaults to 204 on DELETE' do
request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE'))
expect(subject).to receive(:request).and_return(request)
expect(subject.status).to eq 204
end
it 'defaults to 200 on DELETE with a body present' do
request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE'))
subject.body 'content here'
expect(subject).to receive(:request).and_return(request)
expect(subject.status).to eq 200
end
it 'returns status set' do
subject.status 501
expect(subject.status).to eq 501
end
it 'accepts symbol for status' do
subject.status :see_other
expect(subject.status).to eq 303
end
it 'raises error if unknow symbol is passed' do
expect { subject.status :foo_bar }
.to raise_error(ArgumentError, 'Status code :foo_bar is invalid.')
end
it 'accepts unknown Integer status codes' do
expect { subject.status 210 }.to_not raise_error
end
it 'raises error if status is not a integer or symbol' do
expect { subject.status Object.new }
.to raise_error(ArgumentError, 'Status code must be Integer or Symbol.')
end
end
describe '#return_no_content' do
it 'sets the status code and body' do
subject.return_no_content
expect(subject.status).to eq 204
expect(subject.body).to eq ''
end
end
describe '#content_type' do
describe 'set' do
before do
subject.content_type 'text/plain'
end
it 'returns value' do
expect(subject.content_type).to eq 'text/plain'
end
end
it 'returns default' do
expect(subject.content_type).to be nil
end
end
describe '#cookies' do
it 'returns an instance of Cookies' do
expect(subject.cookies).to be_a Grape::Cookies
end
end
describe '#body' do
describe 'set' do
before do
subject.body 'body'
end
it 'returns value' do
expect(subject.body).to eq 'body'
end
end
describe 'false' do
before do
subject.body false
end
it 'sets status to 204' do
expect(subject.body).to eq ''
expect(subject.status).to eq 204
end
end
it 'returns default' do
expect(subject.body).to be nil
end
end
describe '#file' do
describe 'set' do
context 'as file path' do
let(:file_path) { '/some/file/path' }
let(:file_response) do
file_body = Grape::ServeFile::FileBody.new(file_path)
Grape::ServeFile::FileResponse.new(file_body)
end
before do
subject.file file_path
end
it 'returns value wrapped in FileResponse' do
expect(subject.file).to eq file_response
end
end
context 'as object (backward compatibility)' do
let(:file_object) { Class.new }
let(:file_response) do
Grape::ServeFile::FileResponse.new(file_object)
end
before do
subject.file file_object
end
it 'returns value wrapped in FileResponse' do
expect(subject.file).to eq file_response
end
end
end
it 'returns default' do
expect(subject.file).to be nil
end
end
describe '#stream' do
describe 'set' do
let(:file_object) { Class.new }
before do
subject.header 'Cache-Control', 'cache'
subject.header 'Content-Length', 123
subject.header 'Transfer-Encoding', 'base64'
subject.stream file_object
end
it 'returns value wrapped in FileResponse' do
expect(subject.stream).to eq Grape::ServeFile::FileResponse.new(file_object)
end
it 'also sets result of file to value wrapped in FileResponse' do
expect(subject.file).to eq Grape::ServeFile::FileResponse.new(file_object)
end
it 'sets Cache-Control header to no-cache' do
expect(subject.header['Cache-Control']).to eq 'no-cache'
end
it 'sets Content-Length header to nil' do
expect(subject.header['Content-Length']).to eq nil
end
it 'sets Transfer-Encoding header to nil' do
expect(subject.header['Transfer-Encoding']).to eq nil
end
end
it 'returns default' do
expect(subject.file).to be nil
end
end
describe '#route' do
before do
subject.env['grape.routing_args'] = {}
subject.env['grape.routing_args'][:route_info] = 'dummy'
end
it 'returns route_info' do
expect(subject.route).to eq 'dummy'
end
end
describe '#present' do
# see entity_spec.rb for entity representation spec coverage
describe 'dummy' do
before do
subject.present 'dummy'
end
it 'presents dummy object' do
expect(subject.body).to eq 'dummy'
end
end
describe 'with' do
describe 'entity' do
let(:entity_mock) do
entity_mock = Object.new
allow(entity_mock).to receive(:represent).and_return('dummy')
entity_mock
end
describe 'instance' do
before do
subject.present 'dummy', with: entity_mock
end
it 'presents dummy object' do
expect(subject.body).to eq 'dummy'
end
end
end
end
describe 'multiple entities' do
let(:entity_mock_one) do
entity_mock_one = Object.new
allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1')
entity_mock_one
end
let(:entity_mock_two) do
entity_mock_two = Object.new
allow(entity_mock_two).to receive(:represent).and_return(dummy2: 'dummy2')
entity_mock_two
end
describe 'instance' do
before do
subject.present 'dummy1', with: entity_mock_one
subject.present 'dummy2', with: entity_mock_two
end
it 'presents both dummy objects' do
expect(subject.body[:dummy1]).to eq 'dummy1'
expect(subject.body[:dummy2]).to eq 'dummy2'
end
end
end
describe 'non mergeable entity' do
let(:entity_mock_one) do
entity_mock_one = Object.new
allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1')
entity_mock_one
end
let(:entity_mock_two) do
entity_mock_two = Object.new
allow(entity_mock_two).to receive(:represent).and_return('not a hash')
entity_mock_two
end
describe 'instance' do
it 'fails' do
subject.present 'dummy1', with: entity_mock_one
expect do
subject.present 'dummy2', with: entity_mock_two
end.to raise_error ArgumentError, 'Representation of type String cannot be merged.'
end
end
end
end
describe '#declared' do
# see endpoint_spec.rb#declared for spec coverage
it 'is not available by default' do
expect { subject.declared({}) }.to raise_error(
Grape::DSL::InsideRoute::MethodNotYetAvailable
)
end
end
end
grape-1.0.2/spec/grape/dsl/middleware_spec.rb 0000644 0000041 0000041 00000003050 13231337007 021123 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module MiddlewareSpec
class Dummy
include Grape::DSL::Middleware
end
end
describe Middleware do
subject { Class.new(MiddlewareSpec::Dummy) }
let(:proc) { ->() {} }
let(:foo_middleware) { Class.new }
let(:bar_middleware) { Class.new }
describe '.use' do
it 'adds a middleware with the right operation' do
expect(subject).to receive(:namespace_stackable).with(:middleware, [:use, foo_middleware, :arg1, proc])
subject.use foo_middleware, :arg1, &proc
end
end
describe '.insert_before' do
it 'adds a middleware with the right operation' do
expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_before, foo_middleware, :arg1, proc])
subject.insert_before foo_middleware, :arg1, &proc
end
end
describe '.insert_after' do
it 'adds a middleware with the right operation' do
expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_after, foo_middleware, :arg1, proc])
subject.insert_after foo_middleware, :arg1, &proc
end
end
describe '.middleware' do
it 'returns the middleware stack' do
subject.use foo_middleware, :arg1, &proc
subject.insert_before bar_middleware, :arg1, :arg2
expect(subject.middleware).to eq [[:use, foo_middleware, :arg1, proc], [:insert_before, bar_middleware, :arg1, :arg2]]
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/callbacks_spec.rb 0000644 0000041 0000041 00000002220 13231337007 020723 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module CallbacksSpec
class Dummy
include Grape::DSL::Callbacks
end
end
describe Callbacks do
subject { Class.new(CallbacksSpec::Dummy) }
let(:proc) { ->() {} }
describe '.before' do
it 'adds a block to "before"' do
expect(subject).to receive(:namespace_stackable).with(:befores, proc)
subject.before(&proc)
end
end
describe '.before_validation' do
it 'adds a block to "before_validation"' do
expect(subject).to receive(:namespace_stackable).with(:before_validations, proc)
subject.before_validation(&proc)
end
end
describe '.after_validation' do
it 'adds a block to "after_validation"' do
expect(subject).to receive(:namespace_stackable).with(:after_validations, proc)
subject.after_validation(&proc)
end
end
describe '.after' do
it 'adds a block to "after"' do
expect(subject).to receive(:namespace_stackable).with(:afters, proc)
subject.after(&proc)
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/settings_spec.rb 0000644 0000041 0000041 00000020177 13231337007 020657 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module SettingsSpec
class Dummy
include Grape::DSL::Settings
def reset_validations!; end
end
end
describe Settings do
subject { SettingsSpec::Dummy.new }
describe '#unset' do
it 'deletes a key from settings' do
subject.namespace_setting :dummy, 1
expect(subject.inheritable_setting.namespace.keys).to include(:dummy)
subject.unset :namespace, :dummy
expect(subject.inheritable_setting.namespace.keys).not_to include(:dummy)
end
end
describe '#get_or_set' do
it 'sets a values' do
subject.get_or_set :namespace, :dummy, 1
expect(subject.namespace_setting(:dummy)).to eq 1
end
it 'returns a value when nil is new value is provided' do
subject.get_or_set :namespace, :dummy, 1
expect(subject.get_or_set(:namespace, :dummy, nil)).to eq 1
end
end
describe '#global_setting' do
it 'delegates to get_or_set' do
expect(subject).to receive(:get_or_set).with(:global, :dummy, 1)
subject.global_setting(:dummy, 1)
end
end
describe '#unset_global_setting' do
it 'delegates to unset' do
expect(subject).to receive(:unset).with(:global, :dummy)
subject.unset_global_setting(:dummy)
end
end
describe '#route_setting' do
it 'delegates to get_or_set' do
expect(subject).to receive(:get_or_set).with(:route, :dummy, 1)
subject.route_setting(:dummy, 1)
end
it 'sets a value until the next route' do
subject.route_setting :some_thing, :foo_bar
expect(subject.route_setting(:some_thing)).to eq :foo_bar
subject.route_end
expect(subject.route_setting(:some_thing)).to be_nil
end
end
describe '#unset_route_setting' do
it 'delegates to unset' do
expect(subject).to receive(:unset).with(:route, :dummy)
subject.unset_route_setting(:dummy)
end
end
describe '#namespace_setting' do
it 'delegates to get_or_set' do
expect(subject).to receive(:get_or_set).with(:namespace, :dummy, 1)
subject.namespace_setting(:dummy, 1)
end
it 'sets a value until the end of a namespace' do
subject.namespace_start
subject.namespace_setting :some_thing, :foo_bar
expect(subject.namespace_setting(:some_thing)).to eq :foo_bar
subject.namespace_end
expect(subject.namespace_setting(:some_thing)).to be_nil
end
it 'resets values after leaving nested namespaces' do
subject.namespace_start
subject.namespace_setting :some_thing, :foo_bar
expect(subject.namespace_setting(:some_thing)).to eq :foo_bar
subject.namespace_start
expect(subject.namespace_setting(:some_thing)).to be_nil
subject.namespace_end
expect(subject.namespace_setting(:some_thing)).to eq :foo_bar
subject.namespace_end
expect(subject.namespace_setting(:some_thing)).to be_nil
end
end
describe '#unset_namespace_setting' do
it 'delegates to unset' do
expect(subject).to receive(:unset).with(:namespace, :dummy)
subject.unset_namespace_setting(:dummy)
end
end
describe '#namespace_inheritable' do
it 'delegates to get_or_set' do
expect(subject).to receive(:get_or_set).with(:namespace_inheritable, :dummy, 1)
subject.namespace_inheritable(:dummy, 1)
end
it 'inherits values from surrounding namespace' do
subject.namespace_start
subject.namespace_inheritable(:some_thing, :foo_bar)
expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar
subject.namespace_start
expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar
subject.namespace_inheritable(:some_thing, :foo_bar_2)
expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar_2
subject.namespace_end
expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar
subject.namespace_end
end
end
describe '#unset_namespace_inheritable' do
it 'delegates to unset' do
expect(subject).to receive(:unset).with(:namespace_inheritable, :dummy)
subject.unset_namespace_inheritable(:dummy)
end
end
describe '#namespace_stackable' do
it 'delegates to get_or_set' do
expect(subject).to receive(:get_or_set).with(:namespace_stackable, :dummy, 1)
subject.namespace_stackable(:dummy, 1)
end
it 'stacks values from surrounding namespace' do
subject.namespace_start
subject.namespace_stackable(:some_thing, :foo_bar)
expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar]
subject.namespace_start
expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar]
subject.namespace_stackable(:some_thing, :foo_bar_2)
expect(subject.namespace_stackable(:some_thing)).to eq %i[foo_bar foo_bar_2]
subject.namespace_end
expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar]
subject.namespace_end
end
end
describe '#unset_namespace_stackable' do
it 'delegates to unset' do
expect(subject).to receive(:unset).with(:namespace_stackable, :dummy)
subject.unset_namespace_stackable(:dummy)
end
end
describe '#api_class_setting' do
it 'delegates to get_or_set' do
expect(subject).to receive(:get_or_set).with(:api_class, :dummy, 1)
subject.api_class_setting(:dummy, 1)
end
end
describe '#unset_api_class_setting' do
it 'delegates to unset' do
expect(subject).to receive(:unset).with(:api_class, :dummy)
subject.unset_api_class_setting(:dummy)
end
end
describe '#within_namespace' do
it 'calls start and end for a namespace' do
expect(subject).to receive :namespace_start
expect(subject).to receive :namespace_end
subject.within_namespace do
end
end
it 'returns the last result' do
result = subject.within_namespace do
1
end
expect(result).to eq 1
end
end
describe 'complex scenario' do
it 'plays well' do
obj1 = SettingsSpec::Dummy.new
obj2 = SettingsSpec::Dummy.new
obj3 = SettingsSpec::Dummy.new
obj1_copy = nil
obj2_copy = nil
obj3_copy = nil
obj1.within_namespace do
obj1.namespace_stackable(:some_thing, :obj1)
expect(obj1.namespace_stackable(:some_thing)).to eq [:obj1]
obj1_copy = obj1.inheritable_setting.point_in_time_copy
end
expect(obj1.namespace_stackable(:some_thing)).to eq []
expect(obj1_copy.namespace_stackable[:some_thing]).to eq [:obj1]
obj2.within_namespace do
obj2.namespace_stackable(:some_thing, :obj2)
expect(obj2.namespace_stackable(:some_thing)).to eq [:obj2]
obj2_copy = obj2.inheritable_setting.point_in_time_copy
end
expect(obj2.namespace_stackable(:some_thing)).to eq []
expect(obj2_copy.namespace_stackable[:some_thing]).to eq [:obj2]
obj3.within_namespace do
obj3.namespace_stackable(:some_thing, :obj3)
expect(obj3.namespace_stackable(:some_thing)).to eq [:obj3]
obj3_copy = obj3.inheritable_setting.point_in_time_copy
end
expect(obj3.namespace_stackable(:some_thing)).to eq []
expect(obj3_copy.namespace_stackable[:some_thing]).to eq [:obj3]
obj1.top_level_setting.inherit_from obj2_copy.point_in_time_copy
obj2.top_level_setting.inherit_from obj3_copy.point_in_time_copy
expect(obj1_copy.namespace_stackable[:some_thing]).to eq %i[obj3 obj2 obj1]
end
end
end
end
end
grape-1.0.2/spec/grape/dsl/headers_spec.rb 0000644 0000041 0000041 00000001255 13231337007 020426 0 ustar www-data www-data require 'spec_helper'
module Grape
module DSL
module HeadersSpec
class Dummy
include Grape::DSL::Headers
end
end
describe Headers do
subject { HeadersSpec::Dummy.new }
describe '#header' do
describe 'set' do
before do
subject.header 'Name', 'Value'
end
it 'returns value' do
expect(subject.header['Name']).to eq 'Value'
expect(subject.header('Name')).to eq 'Value'
end
end
it 'returns nil' do
expect(subject.header['Name']).to be nil
expect(subject.header('Name')).to be nil
end
end
end
end
end
grape-1.0.2/spec/grape/integration/ 0000755 0000041 0000041 00000000000 13231337007 017212 5 ustar www-data www-data grape-1.0.2/spec/grape/integration/global_namespace_function_spec.rb 0000644 0000041 0000041 00000000644 13231337007 025736 0 ustar www-data www-data # see https://github.com/ruby-grape/grape/issues/1348
require 'spec_helper'
def namespace
raise
end
describe Grape::API do
subject do
Class.new(Grape::API) do
format :json
get do
{ ok: true }
end
end
end
def app
subject
end
context 'with a global namespace function' do
it 'works' do
get '/'
expect(last_response.status).to eq 200
end
end
end
grape-1.0.2/spec/grape/integration/rack_sendfile_spec.rb 0000644 0000041 0000041 00000001640 13231337007 023343 0 ustar www-data www-data require 'spec_helper'
describe Rack::Sendfile do
subject do
send_file = file_streamer
app = Class.new(Grape::API) do
use Rack::Sendfile
format :json
get do
file send_file
end
end
options = {
method: 'GET',
'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
'HTTP_X_ACCEL_MAPPING' => '/accel/mapping/=/replaced/'
}
env = Rack::MockRequest.env_for('/', options)
app.call(env)
end
context do
let(:file_streamer) do
double(:file_streamer, to_path: '/accel/mapping/some/path')
end
it 'contains Sendfile headers' do
headers = subject[1]
expect(headers).to include('X-Accel-Redirect')
end
end
context do
let(:file_streamer) do
double(:file_streamer)
end
it 'not contains Sendfile headers' do
headers = subject[1]
expect(headers).to_not include('X-Accel-Redirect')
end
end
end
grape-1.0.2/spec/grape/integration/rack_spec.rb 0000644 0000041 0000041 00000001726 13231337007 021477 0 ustar www-data www-data require 'spec_helper'
describe Rack do
it 'correctly populates params from a Tempfile' do
input = Tempfile.new 'rubbish'
begin
app = Class.new(Grape::API) do
format :json
post do
{ params_keys: params.keys }
end
end
input.write({ test: '123' * 10_000 }.to_json)
input.rewind
options = {
input: input,
method: 'POST',
'CONTENT_TYPE' => 'application/json'
}
env = Rack::MockRequest.env_for('/', options)
unless RUBY_PLATFORM == 'java'
major, minor, patch = Rack.release.split('.').map(&:to_i)
patch ||= 0 # rack <= 1.5.2 does not specify patch version
pending 'Rack 1.5.3 or 1.6.1 required' unless major >= 2 || (major >= 1 && ((minor == 5 && patch >= 3) || (minor >= 6)))
end
expect(JSON.parse(app.call(env)[2].body.first)['params_keys']).to match_array('test')
ensure
input.close
input.unlink
end
end
end
grape-1.0.2/spec/grape/presenters/ 0000755 0000041 0000041 00000000000 13231337007 017061 5 ustar www-data www-data grape-1.0.2/spec/grape/presenters/presenter_spec.rb 0000644 0000041 0000041 00000003211 13231337007 022424 0 ustar www-data www-data require 'spec_helper'
module Grape
module Presenters
module PresenterSpec
class Dummy
include Grape::DSL::InsideRoute
attr_reader :env, :request, :new_settings
def initialize
@env = {}
@header = {}
@new_settings = { namespace_inheritable: {}, namespace_stackable: {} }
end
end
end
describe Presenter do
describe 'represent' do
let(:object_mock) do
Object.new
end
it 'represent object' do
expect(Presenter.represent(object_mock)).to eq object_mock
end
end
subject { PresenterSpec::Dummy.new }
describe 'present' do
let(:hash_mock) do
{ key: :value }
end
describe 'instance' do
before do
subject.present hash_mock, with: Grape::Presenters::Presenter
end
it 'presents dummy hash' do
expect(subject.body).to eq hash_mock
end
end
describe 'multiple presenter' do
let(:hash_mock1) do
{ key1: :value1 }
end
let(:hash_mock2) do
{ key2: :value2 }
end
describe 'instance' do
before do
subject.present hash_mock1, with: Grape::Presenters::Presenter
subject.present hash_mock2, with: Grape::Presenters::Presenter
end
it 'presents both dummy presenter' do
expect(subject.body[:key1]).to eq hash_mock1[:key1]
expect(subject.body[:key2]).to eq hash_mock2[:key2]
end
end
end
end
end
end
end
grape-1.0.2/spec/grape/validations_spec.rb 0000644 0000041 0000041 00000153337 13231337007 020557 0 ustar www-data www-data require 'spec_helper'
describe Grape::Validations do
subject { Class.new(Grape::API) }
def app
subject
end
describe 'params' do
context 'optional' do
it 'validates when params is present' do
subject.params do
optional :a_number, regexp: /^[0-9]+$/
end
subject.get '/optional' do
'optional works!'
end
get '/optional', a_number: 'string'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('a_number is invalid')
get '/optional', a_number: 45
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional works!')
end
it "doesn't validate when param not present" do
subject.params do
optional :a_number, regexp: /^[0-9]+$/
end
subject.get '/optional' do
'optional works!'
end
get '/optional'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional works!')
end
it 'adds to declared parameters' do
subject.params do
optional :some_param
end
expect(subject.route_setting(:declared_params)).to eq([:some_param])
end
end
context 'optional using Grape::Entity documentation' do
def define_optional_using
documentation = { field_a: { type: String }, field_b: { type: String } }
subject.params do
optional :all, using: documentation
end
end
before do
define_optional_using
subject.get '/optional' do
'optional with using works'
end
end
it 'adds entity documentation to declared params' do
define_optional_using
expect(subject.route_setting(:declared_params)).to eq(%i[field_a field_b])
end
it 'works when field_a and field_b are not present' do
get '/optional'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional with using works')
end
it 'works when field_a is present' do
get '/optional', field_a: 'woof'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional with using works')
end
it 'works when field_b is present' do
get '/optional', field_b: 'woof'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional with using works')
end
end
context 'required' do
before do
subject.params do
requires :key, type: String
end
subject.get('/required') { 'required works' }
subject.put('/required') { { key: params[:key] }.to_json }
end
it 'errors when param not present' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('key is missing')
end
it "doesn't throw a missing param when param is present" do
get '/required', key: 'cool'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
it 'adds to declared parameters' do
subject.params do
requires :some_param
end
expect(subject.route_setting(:declared_params)).to eq([:some_param])
end
it 'works when required field is present but nil' do
put '/required', { key: nil }.to_json, 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq('key' => nil)
end
end
context 'requires :all using Grape::Entity documentation' do
def define_requires_all
documentation = {
required_field: { type: String },
optional_field: { type: String }
}
subject.params do
requires :all, except: :optional_field, using: documentation
end
end
before do
define_requires_all
subject.get '/required' do
'required works'
end
end
it 'adds entity documentation to declared params' do
define_requires_all
expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field])
end
it 'errors when required_field is not present' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('required_field is missing')
end
it 'works when required_field is present' do
get '/required', required_field: 'woof'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
end
context 'requires :none using Grape::Entity documentation' do
def define_requires_none
documentation = {
required_field: { type: String },
optional_field: { type: String }
}
subject.params do
requires :none, except: :required_field, using: documentation
end
end
before do
define_requires_none
subject.get '/required' do
'required works'
end
end
it 'adds entity documentation to declared params' do
define_requires_none
expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field])
end
it 'errors when required_field is not present' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('required_field is missing')
end
it 'works when required_field is present' do
get '/required', required_field: 'woof'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
end
context 'requires :all or :none but except a non-existent field using Grape::Entity documentation' do
context 'requires :all' do
def define_requires_all
documentation = {
required_field: { type: String },
optional_field: { type: String }
}
subject.params do
requires :all, except: :non_existent_field, using: documentation
end
end
it 'adds only the entity documentation to declared params, nothing more' do
define_requires_all
expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field])
end
end
context 'requires :none' do
def define_requires_none
documentation = {
required_field: { type: String },
optional_field: { type: String }
}
subject.params do
requires :none, except: :non_existent_field, using: documentation
end
end
it 'adds only the entity documentation to declared params, nothing more' do
expect { define_requires_none }.to raise_error(ArgumentError)
end
end
end
context 'required with an Array block' do
before do
subject.params do
requires :items, type: Array do
requires :key
end
end
subject.get('/required') { 'required works' }
subject.put('/required') { { items: params[:items] }.to_json }
end
it 'errors when param not present' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is missing')
end
it 'errors when param is not an Array' do
get '/required', items: 'hello'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid')
get '/required', items: { key: 'foo' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid')
end
it "doesn't throw a missing param when param is present" do
get '/required', items: [{ key: 'hello' }, { key: 'world' }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
it "doesn't throw a missing param when param is present but empty" do
put '/required', { items: [] }.to_json, 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq('items' => [])
end
it 'adds to declared parameters' do
subject.params do
requires :items, type: Array do
requires :key
end
end
expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
end
end
# Ensure there is no leakage between declared Array types and
# subsequent Hash types
context 'required with an Array and a Hash block' do
before do
subject.params do
requires :cats, type: Array[String], default: []
requires :items, type: Hash do
requires :key
end
end
subject.get '/required' do
'required works'
end
end
it 'does not output index [0] for Hash types' do
get '/required', cats: ['Garfield'], items: { foo: 'bar' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[key] is missing')
end
end
context 'required with a Hash block' do
before do
subject.params do
requires :items, type: Hash do
requires :key
end
end
subject.get '/required' do
'required works'
end
end
it 'errors when param not present' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is missing, items[key] is missing')
end
it 'errors when nested param not present' do
get '/required', items: { foo: 'bar' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[key] is missing')
end
it 'errors when param is not a Hash' do
get '/required', items: 'hello'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid, items[key] is missing')
get '/required', items: [{ key: 'foo' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid')
end
it "doesn't throw a missing param when param is present" do
get '/required', items: { key: 'hello' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
it 'adds to declared parameters' do
subject.params do
requires :items, type: Array do
requires :key
end
end
expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
end
end
context 'hash with a required param with validation' do
before do
subject.params do
requires :items, type: Hash do
requires :key, type: String, values: %w[a b]
end
end
subject.get '/required' do
'required works'
end
end
it 'errors when param is not a Hash' do
get '/required', items: 'not a hash'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid, items[key] is missing, items[key] is invalid')
get '/required', items: [{ key: 'hash in array' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid, items[key] does not have a valid value')
end
it 'works when all params match' do
get '/required', items: { key: 'a' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
end
context 'group' do
before do
subject.params do
group :items, type: Array do
requires :key
end
end
subject.get '/required' do
'required works'
end
end
it 'errors when param not present' do
get '/required'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is missing')
end
it "doesn't throw a missing param when param is present" do
get '/required', items: [key: 'hello']
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required works')
end
it 'adds to declared parameters' do
subject.params do
group :items, type: Array do
requires :key
end
end
expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
end
end
context 'group params with nested params which has a type' do
let(:invalid_items) { { items: '' } }
before do
subject.params do
optional :items, type: Array do
optional :key1, type: String
optional :key2, type: String
end
end
subject.post '/group_with_nested' do
'group with nested works'
end
end
it 'errors when group param is invalid' do
post '/group_with_nested', items: invalid_items
expect(last_response.status).to eq(400)
end
end
context 'custom validator for a Hash' do
module ValuesSpec
module DateRangeValidations
class DateRangeValidator < Grape::Validations::Base
def validate_param!(attr_name, params)
return if params[attr_name][:from] <= params[attr_name][:to]
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'"
end
end
end
end
before do
subject.params do
optional :date_range, date_range: true, type: Hash do
requires :from, type: Integer
requires :to, type: Integer
end
end
subject.get('/optional') do
'optional works'
end
subject.params do
requires :date_range, date_range: true, type: Hash do
requires :from, type: Integer
requires :to, type: Integer
end
end
subject.get('/required') do
'required works'
end
end
context 'which is optional' do
it "doesn't throw an error if the validation passes" do
get '/optional', date_range: { from: 1, to: 2 }
expect(last_response.status).to eq(200)
end
it 'errors if the validation fails' do
get '/optional', date_range: { from: 2, to: 1 }
expect(last_response.status).to eq(400)
end
end
context 'which is required' do
it "doesn't throw an error if the validation passes" do
get '/required', date_range: { from: 1, to: 2 }
expect(last_response.status).to eq(200)
end
it 'errors if the validation fails' do
get '/required', date_range: { from: 2, to: 1 }
expect(last_response.status).to eq(400)
end
end
end
context 'validation within arrays' do
before do
subject.params do
group :children, type: Array do
requires :name
group :parents, type: Array do
requires :name, allow_blank: false
end
end
end
subject.get '/within_array' do
'within array works'
end
end
it 'can handle new scopes within child elements' do
get '/within_array', children: [
{ name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
{ name: 'Joe', parents: [{ name: 'Josie' }] }
]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('within array works')
end
it 'errors when a parameter is not present' do
get '/within_array', children: [
{ name: 'Jim', parents: [{ name: 'Joy' }] },
{ name: 'Job', parents: [{}] }
]
# NOTE: with body parameters in json or XML or similar this
# should actually fail with: children[parents][name] is missing.
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[1][parents] is missing')
end
it 'errors when a parameter is not present in array within array' do
get '/within_array', children: [
{ name: 'Jim', parents: [{ name: 'Joy' }] },
{ name: 'Job', parents: [{ name: 'Bill' }, { name: '' }] }
]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[1][parents][1][name] is empty')
end
it 'handle errors for all array elements' do
get '/within_array', children: [
{ name: 'Jim', parents: [] },
{ name: 'Job', parents: [] }
]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[0][parents] is missing, children[1][parents] is missing')
end
it 'safely handles empty arrays and blank parameters' do
# NOTE: with body parameters in json or XML or similar this
# should actually return 200, since an empty array is valid.
get '/within_array', children: []
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children is missing')
get '/within_array', children: [name: 'Jay']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[0][parents] is missing')
end
it 'errors when param is not an Array' do
get '/within_array', children: 'hello'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children is invalid')
get '/within_array', children: { name: 'foo' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children is invalid')
get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[0][parents] is invalid')
end
end
context 'with block param' do
before do
subject.params do
requires :planets, type: Array do
requires :name
end
end
subject.get '/req' do
'within array works'
end
subject.put '/req' do
''
end
subject.params do
group :stars, type: Array do
requires :name
end
end
subject.get '/grp' do
'within array works'
end
subject.put '/grp' do
''
end
subject.params do
requires :name
optional :moons, type: Array do
requires :name
end
end
subject.get '/opt' do
'within array works'
end
subject.put '/opt' do
''
end
end
it 'requires defaults to Array type' do
get '/req', planets: 'Jupiter, Saturn'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('planets is invalid')
get '/req', planets: { name: 'Jupiter' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('planets is invalid')
get '/req', planets: [{ name: 'Venus' }, { name: 'Mars' }]
expect(last_response.status).to eq(200)
put_with_json '/req', planets: []
expect(last_response.status).to eq(200)
end
it 'optional defaults to Array type' do
get '/opt', name: 'Jupiter', moons: 'Europa, Ganymede'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('moons is invalid')
get '/opt', name: 'Jupiter', moons: { name: 'Ganymede' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('moons is invalid')
get '/opt', name: 'Jupiter', moons: [{ name: 'Io' }, { name: 'Callisto' }]
expect(last_response.status).to eq(200)
put_with_json '/opt', name: 'Venus'
expect(last_response.status).to eq(200)
put_with_json '/opt', name: 'Mercury', moons: []
expect(last_response.status).to eq(200)
end
it 'group defaults to Array type' do
get '/grp', stars: 'Sun'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('stars is invalid')
get '/grp', stars: { name: 'Sun' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('stars is invalid')
get '/grp', stars: [{ name: 'Sun' }]
expect(last_response.status).to eq(200)
put_with_json '/grp', stars: []
expect(last_response.status).to eq(200)
end
end
context 'validation within arrays with JSON' do
before do
subject.params do
group :children, type: Array do
requires :name
group :parents, type: Array do
requires :name
end
end
end
subject.put '/within_array' do
'within array works'
end
end
it 'can handle new scopes within child elements' do
put_with_json '/within_array', children: [
{ name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
{ name: 'Joe', parents: [{ name: 'Josie' }] }
]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('within array works')
end
it 'errors when a parameter is not present' do
put_with_json '/within_array', children: [
{ name: 'Jim', parents: [{}] },
{ name: 'Job', parents: [{ name: 'Joy' }] }
]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[0][parents][0][name] is missing')
end
it 'safely handles empty arrays and blank parameters' do
put_with_json '/within_array', children: []
expect(last_response.status).to eq(200)
put_with_json '/within_array', children: [name: 'Jay']
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[0][parents] is missing')
end
end
context 'optional with an Array block' do
before do
subject.params do
optional :items, type: Array do
requires :key
end
end
subject.get '/optional_group' do
'optional group works'
end
end
it "doesn't throw a missing param when the group isn't present" do
get '/optional_group'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional group works')
end
it "doesn't throw a missing param when both group and param are given" do
get '/optional_group', items: [{ key: 'foo' }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional group works')
end
it 'errors when group is present, but required param is not' do
get '/optional_group', items: [{ not_key: 'foo' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[0][key] is missing')
end
it "errors when param is present but isn't an Array" do
get '/optional_group', items: 'hello'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid')
get '/optional_group', items: { key: 'foo' }
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items is invalid')
end
it 'adds to declared parameters' do
subject.params do
optional :items, type: Array do
requires :key
end
end
expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
end
end
context 'nested optional Array blocks' do
before do
subject.params do
optional :items, type: Array do
requires :key
optional(:optional_subitems, type: Array) { requires :value }
requires(:required_subitems, type: Array) { requires :value }
end
end
subject.get('/nested_optional_group') { 'nested optional group works' }
end
it 'does no internal validations if the outer group is blank' do
get '/nested_optional_group'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('nested optional group works')
end
it 'does internal validations if the outer group is present' do
get '/nested_optional_group', items: [{ key: 'foo' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[0][required_subitems] is missing')
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('nested optional group works')
end
it 'handles deep nesting' do
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[0][optional_subitems][0][value] is missing')
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ value: 'baz' }] }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('nested optional group works')
end
it 'handles validation within arrays' do
get '/nested_optional_group', items: [{ key: 'foo' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[0][required_subitems] is missing')
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('nested optional group works')
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('items[0][optional_subitems][0][value] is missing')
end
it 'adds to declared parameters' do
subject.params do
optional :items, type: Array do
requires :key
optional(:optional_subitems, type: Array) { requires :value }
requires(:required_subitems, type: Array) { requires :value }
end
end
expect(subject.route_setting(:declared_params)).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
end
end
context 'multiple validation errors' do
before do
subject.params do
requires :yolo
requires :swag
end
subject.get '/two_required' do
'two required works'
end
end
it 'throws the validation errors' do
get '/two_required'
expect(last_response.status).to eq(400)
expect(last_response.body).to match(/yolo is missing/)
expect(last_response.body).to match(/swag is missing/)
end
end
context 'custom validation' do
module CustomValidations
class Customvalidator < Grape::Validations::Base
def validate_param!(attr_name, params)
return if params[attr_name] == 'im custom'
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'is not custom!'
end
end
end
context 'when using optional with a custom validator' do
before do
subject.params do
optional :custom, customvalidator: true
end
subject.get '/optional_custom' do
'optional with custom works!'
end
end
it 'validates when param is present' do
get '/optional_custom', custom: 'im custom'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional with custom works!')
get '/optional_custom', custom: 'im wrong'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is not custom!')
end
it "skips validation when parameter isn't present" do
get '/optional_custom'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional with custom works!')
end
it 'validates with custom validator when param present and incorrect type' do
subject.params do
optional :custom, type: String, customvalidator: true
end
get '/optional_custom', custom: 123
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is not custom!')
end
end
context 'when using requires with a custom validator' do
before do
subject.params do
requires :custom, customvalidator: true
end
subject.get '/required_custom' do
'required with custom works!'
end
end
it 'validates when param is present' do
get '/required_custom', custom: 'im wrong, validate me'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is not custom!')
get '/required_custom', custom: 'im custom'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('required with custom works!')
end
it 'validates when param is not present' do
get '/required_custom'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is missing, custom is not custom!')
end
context 'nested namespaces' do
before do
subject.params do
requires :custom, customvalidator: true
end
subject.namespace 'nested' do
get 'one' do
'validation failed'
end
namespace 'nested' do
get 'two' do
'validation failed'
end
end
end
subject.namespace 'peer' do
get 'one' do
'no validation required'
end
namespace 'nested' do
get 'two' do
'no validation required'
end
end
end
subject.namespace 'unrelated' do
params do
requires :name
end
get 'one' do
'validation required'
end
namespace 'double' do
get 'two' do
'no validation required'
end
end
end
end
specify 'the parent namespace uses the validator' do
get '/nested/one', custom: 'im wrong, validate me'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is not custom!')
end
specify 'the nested namespace inherits the custom validator' do
get '/nested/nested/two', custom: 'im wrong, validate me'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is not custom!')
end
specify 'peer namespaces does not have the validator' do
get '/peer/one', custom: 'im not validated'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('no validation required')
end
specify 'namespaces nested in peers should also not have the validator' do
get '/peer/nested/two', custom: 'im not validated'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('no validation required')
end
specify 'when nested, specifying a route should clear out the validations for deeper nested params' do
get '/unrelated/one'
expect(last_response.status).to eq(400)
get '/unrelated/double/two'
expect(last_response.status).to eq(200)
end
end
end
context 'when using options on param' do
module CustomValidations
class CustomvalidatorWithOptions < Grape::Validations::Base
def validate_param!(attr_name, params)
return if params[attr_name] == @option[:text]
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message
end
end
end
before do
subject.params do
optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' }
end
subject.get '/optional_custom' do
'optional with custom works!'
end
end
it 'validates param with custom validator with options' do
get '/optional_custom', custom: 'im custom with options'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('optional with custom works!')
get '/optional_custom', custom: 'im wrong'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('custom is not custom with options!')
end
end
end # end custom validation
context 'named' do
context 'can be defined' do
it 'in helpers' do
subject.helpers do
params :pagination do
end
end
end
it 'in helper module which kind of Grape::DSL::Helpers::BaseHelper' do
shared_params = Module.new do
extend Grape::DSL::Helpers::BaseHelper
params :pagination do
end
end
subject.helpers shared_params
end
end
context 'can be included in usual params' do
before do
shared_params = Module.new do
extend Grape::DSL::Helpers::BaseHelper
params :period do
optional :start_date
optional :end_date
end
end
subject.helpers shared_params
subject.helpers do
params :pagination do
optional :page, type: Integer
optional :per_page, type: Integer
end
end
end
it 'by #use' do
subject.params do
use :pagination
end
expect(subject.route_setting(:declared_params)).to eq %i[page per_page]
end
it 'by #use with multiple params' do
subject.params do
use :pagination, :period
end
expect(subject.route_setting(:declared_params)).to eq %i[page per_page start_date end_date]
end
end
context 'with block' do
before do
subject.helpers do
params :order do |options|
optional :order, type: Symbol, values: %i[asc desc], default: options[:default_order]
optional :order_by, type: Symbol, values: options[:order_by], default: options[:default_order_by]
end
end
subject.format :json
subject.params do
use :order, default_order: :asc, order_by: %i[name created_at], default_order_by: :created_at
end
subject.get '/order' do
{
order: params[:order],
order_by: params[:order_by]
}
end
end
it 'returns defaults' do
get '/order'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ order: :asc, order_by: :created_at }.to_json)
end
it 'overrides default value for order' do
get '/order?order=desc'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ order: :desc, order_by: :created_at }.to_json)
end
it 'overrides default value for order_by' do
get '/order?order_by=name'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq({ order: :asc, order_by: :name }.to_json)
end
it 'fails with invalid value' do
get '/order?order=invalid'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"order does not have a valid value"}')
end
end
end
context 'documentation' do
it 'can be included with a hash' do
documentation = { example: 'Joe' }
subject.params do
requires 'first_name', documentation: documentation
end
subject.get '/' do
end
expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation)
end
end
context 'all or none' do
context 'optional params' do
before :each do
subject.resource :custom_message do
params do
optional :beer
optional :wine
optional :juice
all_or_none_of :beer, :wine, :juice, message: 'all params are required or none is required'
end
get '/all_or_none' do
'all_or_none works!'
end
end
end
context 'with a custom validation message' do
it 'errors when any one is present' do
get '/custom_message/all_or_none', beer: 'string'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine, juice all params are required or none is required'
end
it 'works when all params are present' do
get '/custom_message/all_or_none', beer: 'string', wine: 'anotherstring', juice: 'anotheranotherstring'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'all_or_none works!'
end
it 'works when none are present' do
get '/custom_message/all_or_none'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'all_or_none works!'
end
end
end
end
context 'mutually exclusive' do
context 'optional params' do
context 'with custom validation message' do
it 'errors when two or more are present' do
subject.resources :custom_message do
params do
optional :beer
optional :wine
optional :juice
mutually_exclusive :beer, :wine, :juice, message: 'are mutually exclusive cannot pass both params'
end
get '/mutually_exclusive' do
'mutually_exclusive works!'
end
end
get '/custom_message/mutually_exclusive', beer: 'string', wine: 'anotherstring'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine are mutually exclusive cannot pass both params'
end
end
it 'errors when two or more are present' do
subject.params do
optional :beer
optional :wine
optional :juice
mutually_exclusive :beer, :wine, :juice
end
subject.get '/mutually_exclusive' do
'mutually_exclusive works!'
end
get '/mutually_exclusive', beer: 'string', wine: 'anotherstring'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine are mutually exclusive'
end
end
context 'more than one set of mutually exclusive params' do
context 'with a custom validation message' do
it 'errors for all sets' do
subject.resources :custom_message do
params do
optional :beer
optional :wine
mutually_exclusive :beer, :wine, message: 'are mutually exclusive pass only one'
optional :nested, type: Hash do
optional :scotch
optional :aquavit
mutually_exclusive :scotch, :aquavit, message: 'are mutually exclusive pass only one'
end
optional :nested2, type: Array do
optional :scotch2
optional :aquavit2
mutually_exclusive :scotch2, :aquavit2, message: 'are mutually exclusive pass only one'
end
end
get '/mutually_exclusive' do
'mutually_exclusive works!'
end
end
get '/custom_message/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine are mutually exclusive pass only one, scotch, aquavit are mutually exclusive pass only one, scotch2, aquavit2 are mutually exclusive pass only one'
end
end
it 'errors for all sets' do
subject.params do
optional :beer
optional :wine
mutually_exclusive :beer, :wine
optional :nested, type: Hash do
optional :scotch
optional :aquavit
mutually_exclusive :scotch, :aquavit
end
optional :nested2, type: Array do
optional :scotch2
optional :aquavit2
mutually_exclusive :scotch2, :aquavit2
end
end
subject.get '/mutually_exclusive' do
'mutually_exclusive works!'
end
get '/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive, scotch2, aquavit2 are mutually exclusive'
end
end
context 'in a group' do
it 'works when only one from the set is present' do
subject.params do
group :drink, type: Hash do
optional :wine
optional :beer
optional :juice
mutually_exclusive :beer, :wine, :juice
end
end
subject.get '/mutually_exclusive_group' do
'mutually_exclusive_group works!'
end
get '/mutually_exclusive_group', drink: { beer: 'true' }
expect(last_response.status).to eq(200)
end
it 'errors when more than one from the set is present' do
subject.params do
group :drink, type: Hash do
optional :wine
optional :beer
optional :juice
mutually_exclusive :beer, :wine, :juice
end
end
subject.get '/mutually_exclusive_group' do
'mutually_exclusive_group works!'
end
get '/mutually_exclusive_group', drink: { beer: 'true', juice: 'true', wine: 'true' }
expect(last_response.status).to eq(400)
end
end
context 'mutually exclusive params inside Hash group' do
it 'invalidates if request param is invalid type' do
subject.params do
optional :wine, type: Hash do
optional :grape
optional :country
mutually_exclusive :grape, :country
end
end
subject.post '/mutually_exclusive' do
'mutually_exclusive works!'
end
post '/mutually_exclusive', wine: '2015 sauvignon'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'wine is invalid'
end
end
end
context 'exactly one of' do
context 'params' do
before :each do
subject.resources :custom_message do
params do
optional :beer
optional :wine
optional :juice
exactly_one_of :beer, :wine, :juice, message: { exactly_one: 'are missing, exactly one parameter is required', mutual_exclusion: 'are mutually exclusive, exactly one parameter is required' }
end
get '/exactly_one_of' do
'exactly_one_of works!'
end
end
subject.params do
optional :beer
optional :wine
optional :juice
exactly_one_of :beer, :wine, :juice
end
subject.get '/exactly_one_of' do
'exactly_one_of works!'
end
end
context 'with a custom validation message' do
it 'errors when none are present' do
get '/custom_message/exactly_one_of'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter is required'
end
it 'succeeds when one is present' do
get '/custom_message/exactly_one_of', beer: 'string'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'exactly_one_of works!'
end
it 'errors when two or more are present' do
get '/custom_message/exactly_one_of', beer: 'string', wine: 'anotherstring'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine are mutually exclusive, exactly one parameter is required'
end
end
it 'errors when none are present' do
get '/exactly_one_of'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter must be provided'
end
it 'succeeds when one is present' do
get '/exactly_one_of', beer: 'string'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'exactly_one_of works!'
end
it 'errors when two or more are present' do
get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine are mutually exclusive'
end
end
context 'nested params' do
before :each do
subject.params do
requires :nested, type: Hash do
optional :beer_nested
optional :wine_nested
optional :juice_nested
exactly_one_of :beer_nested, :wine_nested, :juice_nested
end
optional :nested2, type: Array do
optional :beer_nested2
optional :wine_nested2
optional :juice_nested2
exactly_one_of :beer_nested2, :wine_nested2, :juice_nested2
end
end
subject.get '/exactly_one_of_nested' do
'exactly_one_of works!'
end
end
it 'errors when none are present' do
get '/exactly_one_of_nested'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'nested is missing, beer_nested, wine_nested, juice_nested are missing, exactly one parameter must be provided'
end
it 'succeeds when one is present' do
get '/exactly_one_of_nested', nested: { beer_nested: 'string' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'exactly_one_of works!'
end
it 'errors when two or more are present' do
get '/exactly_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'anotherstring' }]
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer_nested2, wine_nested2 are mutually exclusive'
end
end
end
context 'at least one of' do
context 'params' do
before :each do
subject.resources :custom_message do
params do
optional :beer
optional :wine
optional :juice
at_least_one_of :beer, :wine, :juice, message: 'are missing, please specify at least one param'
end
get '/at_least_one_of' do
'at_least_one_of works!'
end
end
subject.params do
optional :beer
optional :wine
optional :juice
at_least_one_of :beer, :wine, :juice
end
subject.get '/at_least_one_of' do
'at_least_one_of works!'
end
end
context 'with a custom validation message' do
it 'errors when none are present' do
get '/custom_message/at_least_one_of'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine, juice are missing, please specify at least one param'
end
it 'does not error when one is present' do
get '/custom_message/at_least_one_of', beer: 'string'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'at_least_one_of works!'
end
it 'does not error when two are present' do
get '/custom_message/at_least_one_of', beer: 'string', wine: 'string'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'at_least_one_of works!'
end
end
it 'errors when none are present' do
get '/at_least_one_of'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'beer, wine, juice are missing, at least one parameter must be provided'
end
it 'does not error when one is present' do
get '/at_least_one_of', beer: 'string'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'at_least_one_of works!'
end
it 'does not error when two are present' do
get '/at_least_one_of', beer: 'string', wine: 'string'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'at_least_one_of works!'
end
end
context 'nested params' do
before :each do
subject.params do
requires :nested, type: Hash do
optional :beer_nested
optional :wine_nested
optional :juice_nested
at_least_one_of :beer_nested, :wine_nested, :juice_nested
end
optional :nested2, type: Array do
optional :beer_nested2
optional :wine_nested2
optional :juice_nested2
at_least_one_of :beer_nested2, :wine_nested2, :juice_nested2
end
end
subject.get '/at_least_one_of_nested' do
'at_least_one_of works!'
end
end
it 'errors when none are present' do
get '/at_least_one_of_nested'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq 'nested is missing, beer_nested, wine_nested, juice_nested are missing, at least one parameter must be provided'
end
it 'does not error when one is present' do
get '/at_least_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string' }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'at_least_one_of works!'
end
it 'does not error when two are present' do
get '/at_least_one_of_nested', nested: { beer_nested: 'string', wine_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'string' }]
expect(last_response.status).to eq(200)
expect(last_response.body).to eq 'at_least_one_of works!'
end
end
end
context 'in a group' do
it 'works when only one from the set is present' do
subject.params do
group :drink, type: Hash do
optional :wine
optional :beer
optional :juice
exactly_one_of :beer, :wine, :juice
end
end
subject.get '/exactly_one_of_group' do
'exactly_one_of_group works!'
end
get '/exactly_one_of_group', drink: { beer: 'true' }
expect(last_response.status).to eq(200)
end
it 'errors when no parameter from the set is present' do
subject.params do
group :drink, type: Hash do
optional :wine
optional :beer
optional :juice
exactly_one_of :beer, :wine, :juice
end
end
subject.get '/exactly_one_of_group' do
'exactly_one_of_group works!'
end
get '/exactly_one_of_group', drink: {}
expect(last_response.status).to eq(400)
end
it 'errors when more than one from the set is present' do
subject.params do
group :drink, type: Hash do
optional :wine
optional :beer
optional :juice
exactly_one_of :beer, :wine, :juice
end
end
subject.get '/exactly_one_of_group' do
'exactly_one_of_group works!'
end
get '/exactly_one_of_group', drink: { beer: 'true', juice: 'true', wine: 'true' }
expect(last_response.status).to eq(400)
end
it 'does not falsely think the param is there if it is provided outside the block' do
subject.params do
group :drink, type: Hash do
optional :wine
optional :beer
optional :juice
exactly_one_of :beer, :wine, :juice
end
end
subject.get '/exactly_one_of_group' do
'exactly_one_of_group works!'
end
get '/exactly_one_of_group', drink: { foo: 'bar' }, beer: 'true'
expect(last_response.status).to eq(400)
end
end
end
end
grape-1.0.2/spec/grape/extensions/ 0000755 0000041 0000041 00000000000 13231337007 017066 5 ustar www-data www-data grape-1.0.2/spec/grape/extensions/param_builders/ 0000755 0000041 0000041 00000000000 13231337007 022057 5 ustar www-data www-data grape-1.0.2/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb 0000644 0000041 0000041 00000005423 13231337007 031276 0 ustar www-data www-data require 'spec_helper'
describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do
subject { Class.new(Grape::API) }
def app
subject
end
describe 'in an endpoint' do
context '#params' do
before do
subject.params do
build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
end
subject.get do
params.class
end
end
it 'should be of type Hash' do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess')
end
end
end
describe 'in an api' do
before do
subject.send(:include, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder)
end
context '#params' do
before do
subject.get do
params.class
end
end
it 'is a Hash' do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess')
end
it 'parses sub hash params' do
subject.params do
build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
optional :a, type: Hash do
optional :b, type: Hash do
optional :c, type: String
end
optional :d, type: Array
end
end
subject.get '/foo' do
[params[:a]['b'][:c], params['a'][:d]]
end
get '/foo', a: { b: { c: 'bar' }, d: ['foo'] }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["bar", ["foo"]]')
end
it 'params are indifferent to symbol or string keys' do
subject.params do
build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
optional :a, type: Hash do
optional :b, type: Hash do
optional :c, type: String
end
optional :d, type: Array
end
end
subject.get '/foo' do
[params[:a]['b'][:c], params['a'][:d]]
end
get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["bar", ["foo"]]')
end
it 'responds to string keys' do
subject.params do
build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
requires :a, type: String
end
subject.get '/foo' do
[params[:a], params['a']]
end
get '/foo', a: 'bar'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["bar", "bar"]')
end
end
end
end
grape-1.0.2/spec/grape/extensions/param_builders/hashie/ 0000755 0000041 0000041 00000000000 13231337007 023320 5 ustar www-data www-data grape-1.0.2/spec/grape/extensions/param_builders/hashie/mash_spec.rb 0000644 0000041 0000041 00000003332 13231337007 025610 0 ustar www-data www-data require 'spec_helper'
describe Grape::Extensions::Hashie::Mash::ParamBuilder do
subject { Class.new(Grape::API) }
def app
subject
end
describe 'in an endpoint' do
context '#params' do
before do
subject.params do
build_with Grape::Extensions::Hashie::Mash::ParamBuilder
end
subject.get do
params.class
end
end
it 'should be of type Hashie::Mash' do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hashie::Mash')
end
end
end
describe 'in an api' do
before do
subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder)
end
context '#params' do
before do
subject.get do
params.class
end
end
it 'should be Hashie::Mash' do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hashie::Mash')
end
end
context 'in a nested namespace api' do
before do
subject.namespace :foo do
get do
params.class
end
end
end
it 'should be Hashie::Mash' do
get '/foo'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hashie::Mash')
end
end
it 'is indifferent to key or symbol access' do
subject.params do
build_with Grape::Extensions::Hashie::Mash::ParamBuilder
requires :a, type: String
end
subject.get '/foo' do
[params[:a], params['a']]
end
get '/foo', a: 'bar'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["bar", "bar"]')
end
end
end
grape-1.0.2/spec/grape/extensions/param_builders/hash_spec.rb 0000644 0000041 0000041 00000003434 13231337007 024345 0 ustar www-data www-data require 'spec_helper'
describe Grape::Extensions::Hash::ParamBuilder do
subject { Class.new(Grape::API) }
def app
subject
end
describe 'in an endpoint' do
context '#params' do
before do
subject.params do
build_with Grape::Extensions::Hash::ParamBuilder
end
subject.get do
params.class
end
end
it 'should be of type Hash' do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hash')
end
end
end
describe 'in an api' do
before do
subject.send(:include, Grape::Extensions::Hash::ParamBuilder)
end
context '#params' do
before do
subject.get do
params.class
end
end
it 'should be Hash' do
get '/'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('Hash')
end
end
it 'symbolizes params keys' do
subject.params do
optional :a, type: Hash do
optional :b, type: Hash do
optional :c, type: String
end
optional :d, type: Array
end
end
subject.get '/foo' do
[params[:a][:b][:c], params[:a][:d]]
end
get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["bar", ["foo"]]')
end
it 'symbolizes the params' do
subject.params do
build_with Grape::Extensions::Hash::ParamBuilder
requires :a, type: String
end
subject.get '/foo' do
[params[:a], params['a']]
end
get '/foo', a: 'bar'
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('["bar", nil]')
end
end
end
grape-1.0.2/spec/grape/util/ 0000755 0000041 0000041 00000000000 13231337007 015644 5 ustar www-data www-data grape-1.0.2/spec/grape/util/reverse_stackable_values_spec.rb 0000644 0000041 0000041 00000007647 13231337007 024264 0 ustar www-data www-data require 'spec_helper'
module Grape
module Util
describe ReverseStackableValues do
let(:parent) { described_class.new }
subject { described_class.new(parent) }
describe '#keys' do
it 'returns all keys' do
subject[:some_thing] = :foo_bar
subject[:some_thing_else] = :foo_bar
expect(subject.keys).to eq %i[some_thing some_thing_else].sort
end
it 'returns merged keys with parent' do
parent[:some_thing] = :foo
parent[:some_thing_else] = :foo
subject[:some_thing] = :foo_bar
subject[:some_thing_more] = :foo_bar
expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort
end
end
describe '#delete' do
it 'deletes a key' do
subject[:some_thing] = :new_foo_bar
subject.delete :some_thing
expect(subject[:some_thing]).to eq []
end
it 'does not delete parent values' do
parent[:some_thing] = :foo
subject[:some_thing] = :new_foo_bar
subject.delete :some_thing
expect(subject[:some_thing]).to eq [:foo]
end
end
describe '#[]' do
it 'returns an array of values' do
subject[:some_thing] = :foo
expect(subject[:some_thing]).to eq [:foo]
end
it 'returns parent value when no value is set' do
parent[:some_thing] = :foo
expect(subject[:some_thing]).to eq [:foo]
end
it 'combines parent and actual values (actual first)' do
parent[:some_thing] = :foo
subject[:some_thing] = :foo_bar
expect(subject[:some_thing]).to eq %i[foo_bar foo]
end
it 'parent values are not changed' do
parent[:some_thing] = :foo
subject[:some_thing] = :foo_bar
expect(parent[:some_thing]).to eq [:foo]
end
end
describe '#[]=' do
it 'sets a value' do
subject[:some_thing] = :foo
expect(subject[:some_thing]).to eq [:foo]
end
it 'pushes further values' do
subject[:some_thing] = :foo
subject[:some_thing] = :bar
expect(subject[:some_thing]).to eq %i[foo bar]
end
it 'can handle array values' do
subject[:some_thing] = :foo
subject[:some_thing] = %i[bar more]
expect(subject[:some_thing]).to eq [:foo, %i[bar more]]
parent[:some_thing_else] = %i[foo bar]
subject[:some_thing_else] = %i[some bar foo]
expect(subject[:some_thing_else]).to eq [%i[some bar foo], %i[foo bar]]
end
end
describe '#to_hash' do
it 'returns a Hash representation' do
parent[:some_thing] = :foo
subject[:some_thing] = %i[bar more]
subject[:some_thing_more] = :foo_bar
expect(subject.to_hash).to eq(
some_thing: [%i[bar more], :foo],
some_thing_more: [:foo_bar]
)
end
end
describe '#clone' do
let(:obj_cloned) { subject.clone }
it 'copies all values' do
parent = described_class.new
child = described_class.new parent
grandchild = described_class.new child
parent[:some_thing] = :foo
child[:some_thing] = %i[bar more]
grandchild[:some_thing] = :grand_foo_bar
grandchild[:some_thing_more] = :foo_bar
expect(grandchild.clone.to_hash).to eq(
some_thing: [:grand_foo_bar, %i[bar more], :foo],
some_thing_more: [:foo_bar]
)
end
context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do
let(:middleware) { double }
before { subject[:middleware] = middleware }
it 'copies values; does not duplicate them' do
expect(obj_cloned[:middleware]).to eq [middleware]
end
end
end
end
end
end
grape-1.0.2/spec/grape/util/inheritable_values_spec.rb 0000644 0000041 0000041 00000004264 13231337007 023056 0 ustar www-data www-data require 'spec_helper'
module Grape
module Util
describe InheritableValues do
let(:parent) { InheritableValues.new }
subject { InheritableValues.new(parent) }
describe '#delete' do
it 'deletes a key' do
subject[:some_thing] = :new_foo_bar
subject.delete :some_thing
expect(subject[:some_thing]).to be_nil
end
it 'does not delete parent values' do
parent[:some_thing] = :foo
subject[:some_thing] = :new_foo_bar
subject.delete :some_thing
expect(subject[:some_thing]).to eq :foo
end
end
describe '#[]' do
it 'returns a value' do
subject[:some_thing] = :foo
expect(subject[:some_thing]).to eq :foo
end
it 'returns parent value when no value is set' do
parent[:some_thing] = :foo
expect(subject[:some_thing]).to eq :foo
end
it 'overwrites parent value with the current one' do
parent[:some_thing] = :foo
subject[:some_thing] = :foo_bar
expect(subject[:some_thing]).to eq :foo_bar
end
it 'parent values are not changed' do
parent[:some_thing] = :foo
subject[:some_thing] = :foo_bar
expect(parent[:some_thing]).to eq :foo
end
end
describe '#[]=' do
it 'sets a value' do
subject[:some_thing] = :foo
expect(subject[:some_thing]).to eq :foo
end
end
describe '#to_hash' do
it 'returns a Hash representation' do
parent[:some_thing] = :foo
subject[:some_thing_more] = :foo_bar
expect(subject.to_hash).to eq(some_thing: :foo, some_thing_more: :foo_bar)
end
end
describe '#clone' do
let(:obj_cloned) { subject.clone }
context 'complex (i.e. not primitive) data types (ex. entity classes, please see bug #891)' do
let(:description) { { entity: double } }
before { subject[:description] = description }
it 'copies values; does not duplicate them' do
expect(obj_cloned[:description]).to eq description
end
end
end
end
end
end
grape-1.0.2/spec/grape/util/inheritable_setting_spec.rb 0000644 0000041 0000041 00000024063 13231337007 023233 0 ustar www-data www-data require 'spec_helper'
module Grape
module Util
describe InheritableSetting do
before :each do
InheritableSetting.reset_global!
end
let(:parent) do
Grape::Util::InheritableSetting.new.tap do |settings|
settings.global[:global_thing] = :global_foo_bar
settings.namespace[:namespace_thing] = :namespace_foo_bar
settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar
settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar
settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar
settings.route[:route_thing] = :route_foo_bar
end
end
let(:other_parent) do
Grape::Util::InheritableSetting.new.tap do |settings|
settings.namespace[:namespace_thing] = :namespace_foo_bar_other
settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar_other
settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar_other
settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar_other
settings.route[:route_thing] = :route_foo_bar_other
end
end
before :each do
subject.inherit_from parent
end
describe '#global' do
it 'sets a global value' do
subject.global[:some_thing] = :foo_bar
expect(subject.global[:some_thing]).to eq :foo_bar
subject.global[:some_thing] = :foo_bar_next
expect(subject.global[:some_thing]).to eq :foo_bar_next
end
it 'sets the global inherited values' do
expect(subject.global[:global_thing]).to eq :global_foo_bar
end
it 'overrides global values' do
subject.global[:global_thing] = :global_new_foo_bar
expect(parent.global[:global_thing]).to eq :global_new_foo_bar
end
it 'should handle different parents' do
subject.global[:global_thing] = :global_new_foo_bar
subject.inherit_from other_parent
expect(parent.global[:global_thing]).to eq :global_new_foo_bar
expect(other_parent.global[:global_thing]).to eq :global_new_foo_bar
end
end
describe '#api_class' do
it 'is specific to the class' do
subject.api_class[:some_thing] = :foo_bar
parent.api_class[:some_thing] = :some_thing
expect(subject.api_class[:some_thing]).to eq :foo_bar
expect(parent.api_class[:some_thing]).to eq :some_thing
end
end
describe '#namespace' do
it 'sets a value until the end of a namespace' do
subject.namespace[:some_thing] = :foo_bar
expect(subject.namespace[:some_thing]).to eq :foo_bar
end
it 'uses new values when a new namespace starts' do
subject.namespace[:namespace_thing] = :new_namespace_foo_bar
expect(subject.namespace[:namespace_thing]).to eq :new_namespace_foo_bar
expect(parent.namespace[:namespace_thing]).to eq :namespace_foo_bar
end
end
describe '#namespace_inheritable' do
it 'works with inheritable values' do
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar
end
it 'should handle different parents' do
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar
subject.inherit_from other_parent
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar_other
subject.inherit_from parent
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar
subject.inherit_from other_parent
subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing
subject.inherit_from parent
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing
end
end
describe '#namespace_stackable' do
it 'works with stackable values' do
expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar]
subject.inherit_from other_parent
expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar_other]
end
end
describe '#namespace_reverse_stackable' do
it 'works with reverse stackable values' do
expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar]
subject.inherit_from other_parent
expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar_other]
end
end
describe '#route' do
it 'sets a value until the next route' do
subject.route[:some_thing] = :foo_bar
expect(subject.route[:some_thing]).to eq :foo_bar
subject.route_end
expect(subject.route[:some_thing]).to be_nil
end
it 'works with route values' do
expect(subject.route[:route_thing]).to eq :route_foo_bar
end
end
describe '#api_class' do
it 'is specific to the class' do
subject.api_class[:some_thing] = :foo_bar
expect(subject.api_class[:some_thing]).to eq :foo_bar
end
end
describe '#inherit_from' do
it 'notifies clones' do
new_settings = subject.point_in_time_copy
expect(new_settings).to receive(:inherit_from).with(other_parent)
subject.inherit_from other_parent
end
end
describe '#point_in_time_copy' do
let!(:cloned_obj) { subject.point_in_time_copy }
it 'resets point_in_time_copies' do
expect(cloned_obj.point_in_time_copies).to be_empty
end
it 'decouples namespace values' do
subject.namespace[:namespace_thing] = :namespace_foo_bar
cloned_obj.namespace[:namespace_thing] = :new_namespace_foo_bar
expect(subject.namespace[:namespace_thing]).to eq :namespace_foo_bar
end
it 'decouples namespace inheritable values' do
expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar
subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing
expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar
cloned_obj.namespace_inheritable[:namespace_inheritable_thing] = :my_cloned_thing
expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_cloned_thing
expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing
end
it 'decouples namespace stackable values' do
expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar]
subject.namespace_stackable[:namespace_stackable_thing] = :other_thing
expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq %i[namespace_stackable_foo_bar other_thing]
expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar]
end
it 'decouples namespace reverse stackable values' do
expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar]
subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :other_thing
expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq %i[other_thing namespace_reverse_stackable_foo_bar]
expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar]
end
it 'decouples route values' do
expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar
subject.route[:route_thing] = :new_route_foo_bar
expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar
end
it 'adds itself to original as clone' do
expect(subject.point_in_time_copies).to include(cloned_obj)
end
end
describe '#to_hash' do
it 'return all settings as a hash' do
subject.global[:global_thing] = :global_foo_bar
subject.namespace[:namespace_thing] = :namespace_foo_bar
subject.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar
subject.namespace_stackable[:namespace_stackable_thing] = [:namespace_stackable_foo_bar]
subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = [:namespace_reverse_stackable_foo_bar]
subject.route[:route_thing] = :route_foo_bar
expect(subject.to_hash).to include(global: { global_thing: :global_foo_bar })
expect(subject.to_hash).to include(namespace: { namespace_thing: :namespace_foo_bar })
expect(subject.to_hash).to include(namespace_inheritable: {
namespace_inheritable_thing: :namespace_inheritable_foo_bar
})
expect(subject.to_hash).to include(namespace_stackable: { namespace_stackable_thing: [:namespace_stackable_foo_bar, [:namespace_stackable_foo_bar]] })
expect(subject.to_hash).to include(namespace_reverse_stackable:
{ namespace_reverse_stackable_thing: [[:namespace_reverse_stackable_foo_bar], :namespace_reverse_stackable_foo_bar] })
expect(subject.to_hash).to include(route: { route_thing: :route_foo_bar })
end
end
end
end
end
grape-1.0.2/spec/grape/util/stackable_values_spec.rb 0000644 0000041 0000041 00000007510 13231337007 022516 0 ustar www-data www-data require 'spec_helper'
module Grape
module Util
describe StackableValues do
let(:parent) { StackableValues.new }
subject { StackableValues.new(parent) }
describe '#keys' do
it 'returns all key' do
subject[:some_thing] = :foo_bar
subject[:some_thing_else] = :foo_bar
expect(subject.keys).to eq %i[some_thing some_thing_else].sort
end
it 'returns merged keys with parent' do
parent[:some_thing] = :foo
parent[:some_thing_else] = :foo
subject[:some_thing] = :foo_bar
subject[:some_thing_more] = :foo_bar
expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort
end
end
describe '#delete' do
it 'deletes a key' do
subject[:some_thing] = :new_foo_bar
subject.delete :some_thing
expect(subject[:some_thing]).to eq []
end
it 'does not delete parent values' do
parent[:some_thing] = :foo
subject[:some_thing] = :new_foo_bar
subject.delete :some_thing
expect(subject[:some_thing]).to eq [:foo]
end
end
describe '#[]' do
it 'returns an array of values' do
subject[:some_thing] = :foo
expect(subject[:some_thing]).to eq [:foo]
end
it 'returns parent value when no value is set' do
parent[:some_thing] = :foo
expect(subject[:some_thing]).to eq [:foo]
end
it 'combines parent and actual values' do
parent[:some_thing] = :foo
subject[:some_thing] = :foo_bar
expect(subject[:some_thing]).to eq %i[foo foo_bar]
end
it 'parent values are not changed' do
parent[:some_thing] = :foo
subject[:some_thing] = :foo_bar
expect(parent[:some_thing]).to eq [:foo]
end
end
describe '#[]=' do
it 'sets a value' do
subject[:some_thing] = :foo
expect(subject[:some_thing]).to eq [:foo]
end
it 'pushes further values' do
subject[:some_thing] = :foo
subject[:some_thing] = :bar
expect(subject[:some_thing]).to eq %i[foo bar]
end
it 'can handle array values' do
subject[:some_thing] = :foo
subject[:some_thing] = %i[bar more]
expect(subject[:some_thing]).to eq [:foo, %i[bar more]]
parent[:some_thing_else] = %i[foo bar]
subject[:some_thing_else] = %i[some bar foo]
expect(subject[:some_thing_else]).to eq [%i[foo bar], %i[some bar foo]]
end
end
describe '#to_hash' do
it 'returns a Hash representation' do
parent[:some_thing] = :foo
subject[:some_thing] = %i[bar more]
subject[:some_thing_more] = :foo_bar
expect(subject.to_hash).to eq(some_thing: [:foo, %i[bar more]], some_thing_more: [:foo_bar])
end
end
describe '#clone' do
let(:obj_cloned) { subject.clone }
it 'copies all values' do
parent = StackableValues.new
child = StackableValues.new parent
grandchild = StackableValues.new child
parent[:some_thing] = :foo
child[:some_thing] = %i[bar more]
grandchild[:some_thing] = :grand_foo_bar
grandchild[:some_thing_more] = :foo_bar
expect(grandchild.clone.to_hash).to eq(some_thing: [:foo, %i[bar more], :grand_foo_bar], some_thing_more: [:foo_bar])
end
context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do
let(:middleware) { double }
before { subject[:middleware] = middleware }
it 'copies values; does not duplicate them' do
expect(obj_cloned[:middleware]).to eq [middleware]
end
end
end
end
end
end
grape-1.0.2/spec/grape/util/strict_hash_configuration_spec.rb 0000644 0000041 0000041 00000001656 13231337007 024455 0 ustar www-data www-data require 'spec_helper'
module Grape
module Util
describe 'StrictHashConfiguration' do
subject do
Class.new do
include Grape::Util::StrictHashConfiguration.module(:config1, :config2, config3: [:config4], config5: [config6: %i[config7 config8]])
end
end
it 'set nested configs' do
subject.configure do
config1 'alpha'
config2 'beta'
config3 do
config4 'gamma'
end
local_var = 8
config5 do
config6 do
config7 7
config8 local_var
end
end
end
expect(subject.settings).to eq(config1: 'alpha',
config2: 'beta',
config3: { config4: 'gamma' },
config5: { config6: { config7: 7, config8: 8 } })
end
end
end
end
grape-1.0.2/spec/grape/path_spec.rb 0000644 0000041 0000041 00000016352 13231337007 017171 0 ustar www-data www-data require 'spec_helper'
module Grape
describe Path do
describe '#initialize' do
it 'remembers the path' do
path = Path.new('/:id', anything, anything)
expect(path.raw_path).to eql('/:id')
end
it 'remembers the namespace' do
path = Path.new(anything, '/users', anything)
expect(path.namespace).to eql('/users')
end
it 'remebers the settings' do
path = Path.new(anything, anything, foo: 'bar')
expect(path.settings).to eql(foo: 'bar')
end
end
describe '#mount_path' do
it 'is nil when no mount path setting exists' do
path = Path.new(anything, anything, {})
expect(path.mount_path).to be_nil
end
it 'is nil when the mount path is nil' do
path = Path.new(anything, anything, mount_path: nil)
expect(path.mount_path).to be_nil
end
it 'splits the mount path' do
path = Path.new(anything, anything, mount_path: %w[foo bar])
expect(path.mount_path).to eql(%w[foo bar])
end
end
describe '#root_prefix' do
it 'is nil when no root prefix setting exists' do
path = Path.new(anything, anything, {})
expect(path.root_prefix).to be_nil
end
it 'is nil when the mount path is nil' do
path = Path.new(anything, anything, root_prefix: nil)
expect(path.root_prefix).to be_nil
end
it 'splits the mount path' do
path = Path.new(anything, anything, root_prefix: 'hello/world')
expect(path.root_prefix).to eql(%w[hello world])
end
end
describe '#uses_path_versioning?' do
it 'is false when the version setting is nil' do
path = Path.new(anything, anything, version: nil)
expect(path.uses_path_versioning?).to be false
end
it 'is false when the version option is header' do
path = Path.new(
anything,
anything,
version: 'v1',
version_options: { using: :header }
)
expect(path.uses_path_versioning?).to be false
end
it 'is true when the version option is path' do
path = Path.new(
anything,
anything,
version: 'v1',
version_options: { using: :path }
)
expect(path.uses_path_versioning?).to be true
end
end
describe '#namespace?' do
it 'is false when the namespace is nil' do
path = Path.new(anything, nil, anything)
expect(path.namespace?).to be nil
end
it 'is false when the namespace starts with whitespace' do
path = Path.new(anything, ' /foo', anything)
expect(path.namespace?).to be nil
end
it 'is false when the namespace is the root path' do
path = Path.new(anything, '/', anything)
expect(path.namespace?).to be false
end
it 'is true otherwise' do
path = Path.new(anything, '/world', anything)
expect(path.namespace?).to be true
end
end
describe '#path?' do
it 'is false when the path is nil' do
path = Path.new(nil, anything, anything)
expect(path.path?).to be nil
end
it 'is false when the path starts with whitespace' do
path = Path.new(' /foo', anything, anything)
expect(path.path?).to be nil
end
it 'is false when the path is the root path' do
path = Path.new('/', anything, anything)
expect(path.path?).to be false
end
it 'is true otherwise' do
path = Path.new('/hello', anything, anything)
expect(path.path?).to be true
end
end
describe '#path' do
context 'mount_path' do
it 'is not included when it is nil' do
path = Path.new(nil, nil, mount_path: '/foo/bar')
expect(path.path).to eql '/foo/bar'
end
it 'is included when it is not nil' do
path = Path.new(nil, nil, {})
expect(path.path).to eql('/')
end
end
context 'root_prefix' do
it 'is not included when it is nil' do
path = Path.new(nil, nil, {})
expect(path.path).to eql('/')
end
it 'is included after the mount path' do
path = Path.new(
nil,
nil,
mount_path: '/foo',
root_prefix: '/hello'
)
expect(path.path).to eql('/foo/hello')
end
end
it 'uses the namespace after the mount path and root prefix' do
path = Path.new(
nil,
'namespace',
mount_path: '/foo',
root_prefix: '/hello'
)
expect(path.path).to eql('/foo/hello/namespace')
end
it 'uses the raw path after the namespace' do
path = Path.new(
'raw_path',
'namespace',
mount_path: '/foo',
root_prefix: '/hello'
)
expect(path.path).to eql('/foo/hello/namespace/raw_path')
end
end
describe '#suffix' do
context 'when using a specific format' do
it 'accepts specified format' do
path = Path.new(nil, nil, {})
allow(path).to receive(:uses_specific_format?) { true }
allow(path).to receive(:settings) { { format: :json } }
expect(path.suffix).to eql('(.json)')
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)')
end
end
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)')
end
end
end
describe '#path_with_suffix' do
it 'combines the path and suffix' do
path = Path.new(nil, nil, {})
allow(path).to receive(:path) { '/the/path' }
allow(path).to receive(:suffix) { 'suffix' }
expect(path.path_with_suffix).to eql('/the/pathsuffix')
end
context 'when using a specific format' do
it 'might have a suffix with specified format' do
path = Path.new(nil, nil, {})
allow(path).to receive(:path) { '/the/path' }
allow(path).to receive(:uses_specific_format?) { true }
allow(path).to receive(:settings) { { format: :json } }
expect(path.path_with_suffix).to eql('/the/path(.json)')
end
end
end
end
end
grape-1.0.2/spec/grape/loading_spec.rb 0000644 0000041 0000041 00000001452 13231337007 017645 0 ustar www-data www-data require 'spec_helper'
describe Grape::API do
let(:jobs_api) do
Class.new(Grape::API) do
namespace :one do
namespace :two do
namespace :three do
get :one do
end
get :two do
end
end
end
end
end
end
let(:combined_api) do
JobsApi = jobs_api
Class.new(Grape::API) do
version :v1, using: :accept_version_header, cascade: true
mount JobsApi
end
end
subject do
CombinedApi = combined_api
Class.new(Grape::API) do
format :json
mount CombinedApi => '/'
end
end
def app
subject
end
it 'execute first request in reasonable time' do
started = Time.now
get '/mount1/nested/test_method'
expect(Time.now - started).to be < 5
end
end
grape-1.0.2/spec/grape/request_spec.rb 0000644 0000041 0000041 00000005323 13231337007 017721 0 ustar www-data www-data require 'spec_helper'
module Grape
describe Request do
let(:default_method) { 'GET' }
let(:default_params) { {} }
let(:default_options) do
{
method: method,
params: params
}
end
let(:default_env) do
Rack::MockRequest.env_for('/', options)
end
let(:method) { default_method }
let(:params) { default_params }
let(:options) { default_options }
let(:env) { default_env }
let(:request) do
Grape::Request.new(env)
end
describe '#params' do
let(:params) do
{
a: '123',
b: 'xyz'
}
end
it 'by default returns stringified parameter keys' do
expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz'))
end
context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do
let(:request) do
Grape::Request.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder)
end
it 'returns symbolized params' do
expect(request.params).to eq(a: '123', b: 'xyz')
end
end
describe 'with grape.routing_args' do
let(:options) do
default_options.merge('grape.routing_args' => routing_args)
end
let(:routing_args) do
{
version: '123',
route_info: '456',
c: 'ccc'
}
end
it 'cuts version and route_info' do
expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc'))
end
end
end
describe '#headers' do
let(:options) do
default_options.merge(request_headers)
end
describe 'with http headers in env' do
let(:request_headers) do
{
'HTTP_X_GRAPE_IS_COOL' => 'yeah'
}
end
it 'cuts HTTP_ prefix and capitalizes header name words' do
expect(request.headers).to eq('X-Grape-Is-Cool' => 'yeah')
end
end
describe 'with non-HTTP_* stuff in env' do
let(:request_headers) do
{
'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape'
}
end
it 'does not include them' do
expect(request.headers).to eq({})
end
end
describe 'with symbolic header names' do
let(:request_headers) do
{
HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true'
}
end
let(:env) do
default_env.merge(request_headers)
end
it 'converts them to string' do
expect(request.headers).to eq('Grape-Likes-Symbolic' => 'it is true')
end
end
end
end
end
grape-1.0.2/spec/spec_helper.rb 0000644 0000041 0000041 00000001204 13231337007 016404 0 ustar www-data www-data $LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support'))
require 'grape'
require 'rubygems'
require 'bundler'
Bundler.require :default, :test
Dir["#{File.dirname(__FILE__)}/support/*.rb"].each do |file|
require file
end
I18n.enforce_available_locales = false
RSpec.configure do |config|
config.include Rack::Test::Methods
config.include Spec::Support::Helpers
config.raise_errors_for_deprecations!
config.before(:each) { Grape::Util::InheritableSetting.reset_global! }
end
require 'coveralls'
Coveralls.wear!
grape-1.0.2/spec/shared/ 0000755 0000041 0000041 00000000000 13231337007 015037 5 ustar www-data www-data grape-1.0.2/spec/shared/versioning_examples.rb 0000644 0000041 0000041 00000013633 13231337007 021453 0 ustar www-data www-data shared_examples_for 'versioning' do
it 'sets the API version' do
subject.format :txt
subject.version 'v1', macro_options
subject.get :hello do
"Version: #{request.env['api.version']}"
end
versioned_get '/hello', 'v1', macro_options
expect(last_response.body).to eql 'Version: v1'
end
it 'adds the prefix before the API version' do
subject.format :txt
subject.prefix 'api'
subject.version 'v1', macro_options
subject.get :hello do
"Version: #{request.env['api.version']}"
end
versioned_get '/hello', 'v1', macro_options.merge(prefix: 'api')
expect(last_response.body).to eql 'Version: v1'
end
it 'is able to specify version as a nesting' do
subject.version 'v2', macro_options
subject.get '/awesome' do
'Radical'
end
subject.version 'v1', macro_options do
get '/legacy' do
'Totally'
end
end
versioned_get '/awesome', 'v1', macro_options
expect(last_response.status).to eql 404
versioned_get '/awesome', 'v2', macro_options
expect(last_response.status).to eql 200
versioned_get '/legacy', 'v1', macro_options
expect(last_response.status).to eql 200
versioned_get '/legacy', 'v2', macro_options
expect(last_response.status).to eql 404
end
it 'is able to specify multiple versions' do
subject.version 'v1', 'v2', macro_options
subject.get 'awesome' do
'I exist'
end
versioned_get '/awesome', 'v1', macro_options
expect(last_response.status).to eql 200
versioned_get '/awesome', 'v2', macro_options
expect(last_response.status).to eql 200
versioned_get '/awesome', 'v3', macro_options
expect(last_response.status).to eql 404
end
context 'with different versions for the same endpoint' do
context 'without a prefix' do
it 'allows the same endpoint to be implemented' do
subject.format :txt
subject.version 'v2', macro_options
subject.get 'version' do
request.env['api.version']
end
subject.version 'v1', macro_options do
get 'version' do
'version ' + request.env['api.version']
end
end
versioned_get '/version', 'v2', macro_options
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v2')
versioned_get '/version', 'v1', macro_options
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('version v1')
end
end
context 'with a prefix' do
it 'allows the same endpoint to be implemented' do
subject.format :txt
subject.prefix 'api'
subject.version 'v2', macro_options
subject.get 'version' do
request.env['api.version']
end
subject.version 'v1', macro_options do
get 'version' do
'version ' + request.env['api.version']
end
end
versioned_get '/version', 'v1', macro_options.merge(prefix: subject.prefix)
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('version v1')
versioned_get '/version', 'v2', macro_options.merge(prefix: subject.prefix)
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v2')
end
end
end
context 'with before block defined within a version block' do
it 'calls before block that is defined within the version block' do
subject.format :txt
subject.prefix 'api'
subject.version 'v2', macro_options do
before do
@output ||= 'v2-'
end
get 'version' do
@output += 'version'
end
end
subject.version 'v1', macro_options do
before do
@output ||= 'v1-'
end
get 'version' do
@output += 'version'
end
end
versioned_get '/version', 'v1', macro_options.merge(prefix: subject.prefix)
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v1-version')
versioned_get '/version', 'v2', macro_options.merge(prefix: subject.prefix)
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v2-version')
end
end
it 'does not overwrite version parameter with API version' do
subject.format :txt
subject.version 'v1', macro_options
subject.params { requires :version }
subject.get :api_version_with_version_param do
params[:version]
end
versioned_get '/api_version_with_version_param?version=1', 'v1', macro_options
expect(last_response.body).to eql '1'
end
context 'with catch-all' do
let(:options) { macro_options }
let(:v1) do
klass = Class.new(Grape::API)
klass.version 'v1', options
klass.get 'version' do
'v1'
end
klass
end
let(:v2) do
klass = Class.new(Grape::API)
klass.version 'v2', options
klass.get 'version' do
'v2'
end
klass
end
before do
subject.format :txt
subject.mount v1
subject.mount v2
subject.route :any, '*path' do
params[:path]
end
end
context 'v1' do
it 'finds endpoint' do
versioned_get '/version', 'v1', macro_options
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v1')
end
it 'finds catch all' do
versioned_get '/whatever', 'v1', macro_options
expect(last_response.status).to eq(200)
expect(last_response.body).to end_with 'whatever'
end
end
context 'v2' do
it 'finds endpoint' do
versioned_get '/version', 'v2', macro_options
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('v2')
end
it 'finds catch all' do
versioned_get '/whatever', 'v2', macro_options
expect(last_response.status).to eq(200)
expect(last_response.body).to end_with 'whatever'
end
end
end
end
grape-1.0.2/spec/integration/ 0000755 0000041 0000041 00000000000 13231337007 016114 5 ustar www-data www-data grape-1.0.2/spec/integration/multi_xml/ 0000755 0000041 0000041 00000000000 13231337007 020126 5 ustar www-data www-data grape-1.0.2/spec/integration/multi_xml/xml_spec.rb 0000644 0000041 0000041 00000000172 13231337007 022265 0 ustar www-data www-data require 'spec_helper'
describe Grape::Xml do
it 'uses multi_xml' do
expect(Grape::Xml).to eq(::MultiXml)
end
end
grape-1.0.2/spec/integration/multi_json/ 0000755 0000041 0000041 00000000000 13231337007 020277 5 ustar www-data www-data grape-1.0.2/spec/integration/multi_json/json_spec.rb 0000644 0000041 0000041 00000000176 13231337007 022613 0 ustar www-data www-data require 'spec_helper'
describe Grape::Json do
it 'uses multi_json' do
expect(Grape::Json).to eq(::MultiJson)
end
end
grape-1.0.2/spec/support/ 0000755 0000041 0000041 00000000000 13231337007 015305 5 ustar www-data www-data grape-1.0.2/spec/support/endpoint_faker.rb 0000644 0000041 0000041 00000000706 13231337007 020625 0 ustar www-data www-data module Spec
module Support
class EndpointFaker
class FakerAPI < Grape::API
get '/' do
end
end
def initialize(app, endpoint = FakerAPI.endpoints.first)
@app = app
@endpoint = endpoint
end
def call(env)
@endpoint.instance_exec do
@request = Grape::Request.new(env.dup)
end
@app.call(env.merge('api.endpoint' => @endpoint))
end
end
end
end
grape-1.0.2/spec/support/versioned_helpers.rb 0000644 0000041 0000041 00000003406 13231337007 021355 0 ustar www-data www-data # Versioning
module Spec
module Support
module Helpers
# Returns the path with options[:version] prefixed if options[:using] is :path.
# Returns normal path otherwise.
def versioned_path(options = {})
case options[:using]
when :path
File.join('/', options[:prefix] || '', options[:version], options[:path])
when :param
File.join('/', options[:prefix] || '', options[:path])
when :header
File.join('/', options[:prefix] || '', options[:path])
when :accept_version_header
File.join('/', options[:prefix] || '', options[:path])
else
raise ArgumentError.new("unknown versioning strategy: #{options[:using]}")
end
end
def versioned_headers(options)
case options[:using]
when :path
{} # no-op
when :param
{} # no-op
when :header
{
'HTTP_ACCEPT' => [
"application/vnd.#{options[:vendor]}-#{options[:version]}",
options[:format]
].compact.join('+')
}
when :accept_version_header
{
'HTTP_ACCEPT_VERSION' => options[:version].to_s
}
else
raise ArgumentError.new("unknown versioning strategy: #{options[:using]}")
end
end
def versioned_get(path, version_name, version_options = {})
path = versioned_path(version_options.merge(version: version_name, path: path))
headers = versioned_headers(version_options.merge(version: version_name))
params = {}
if version_options[:using] == :param
params = { version_options[:parameter] => version_name }
end
get path, params, headers
end
end
end
end
grape-1.0.2/spec/support/content_type_helpers.rb 0000644 0000041 0000041 00000000546 13231337007 022074 0 ustar www-data www-data module Spec
module Support
module Helpers
%w[put patch post delete].each do |method|
define_method :"#{method}_with_json" do |uri, params = {}, env = {}, &block|
params = params.to_json
env['CONTENT_TYPE'] ||= 'application/json'
send(method, uri, params, env, &block)
end
end
end
end
end
grape-1.0.2/spec/support/integer_helpers.rb 0000644 0000041 0000041 00000000271 13231337007 021011 0 ustar www-data www-data module Spec
module Support
module Helpers
INTEGER_CLASS_NAME = 0.to_i.class.to_s.freeze
def integer_class_name
INTEGER_CLASS_NAME
end
end
end
end
grape-1.0.2/spec/support/basic_auth_encode_helpers.rb 0000644 0000041 0000041 00000000272 13231337007 022774 0 ustar www-data www-data module Spec
module Support
module Helpers
def encode_basic_auth(username, password)
'Basic ' + Base64.encode64("#{username}:#{password}")
end
end
end
end
grape-1.0.2/spec/support/file_streamer.rb 0000644 0000041 0000041 00000000270 13231337007 020452 0 ustar www-data www-data class FileStreamer
def initialize(file_path)
@file_path = file_path
end
def each(&blk)
File.open(@file_path, 'rb') do |file|
file.each(10, &blk)
end
end
end
grape-1.0.2/benchmark/ 0000755 0000041 0000041 00000000000 13231337007 014571 5 ustar www-data www-data grape-1.0.2/benchmark/simple_with_type_coercer.rb 0000644 0000041 0000041 00000000673 13231337007 022213 0 ustar www-data www-data $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'grape'
require 'benchmark/ips'
api = Class.new(Grape::API) do
prefix :api
version 'v1', using: :path
params do
requires :param, type: Array[String]
end
get '/' do
'hello'
end
end
env = Rack::MockRequest.env_for('/api/v1?param=value', method: 'GET')
Benchmark.ips do |ips|
ips.report('simple_with_type_coercer') do
api.call(env)
end
end
grape-1.0.2/benchmark/simple.rb 0000644 0000041 0000041 00000000650 13231337007 016410 0 ustar www-data www-data $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'grape'
require 'benchmark/ips'
class API < Grape::API
prefix :api
version 'v1', using: :path
get '/' do
'hello'
end
end
options = {
method: 'GET'
}
env = Rack::MockRequest.env_for('/api/v1', options)
10.times do |i|
env["HTTP_HEADER#{i}"] = '123'
end
Benchmark.ips do |ips|
ips.report('simple') do
API.call env
end
end
grape-1.0.2/lib/ 0000755 0000041 0000041 00000000000 13231337007 013405 5 ustar www-data www-data grape-1.0.2/lib/grape/ 0000755 0000041 0000041 00000000000 13231337007 014503 5 ustar www-data www-data grape-1.0.2/lib/grape/middleware/ 0000755 0000041 0000041 00000000000 13231337007 016620 5 ustar www-data www-data grape-1.0.2/lib/grape/middleware/formatter.rb 0000644 0000041 0000041 00000013375 13231337007 021161 0 ustar www-data www-data require 'grape/middleware/base'
module Grape
module Middleware
class Formatter < Base
CHUNKED = 'chunked'.freeze
def default_options
{
default_format: :txt,
formatters: {},
parsers: {}
}
end
def before
negotiate_content_type
read_body_input
end
def after
return unless @app_response
status, headers, bodies = *@app_response
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
@app_response
else
build_formatted_response(status, headers, bodies)
end
end
private
def build_formatted_response(status, headers, bodies)
headers = ensure_content_type(headers)
if bodies.is_a?(Grape::ServeFile::FileResponse)
Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
resp.body = bodies.file
end
else
# Allow content-type to be explicitly overwritten
formatter = fetch_formatter(headers, options)
bodymap = bodies.collect { |body| formatter.call(body, env) }
Rack::Response.new(bodymap, status, headers)
end
rescue Grape::Exceptions::InvalidFormatter => e
throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
end
def fetch_formatter(headers, options)
api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
Grape::Formatter.formatter_for(api_format, options)
end
# Set the content type header for the API format if it is not already present.
#
# @param headers [Hash]
# @return [Hash]
def ensure_content_type(headers)
if headers[Grape::Http::Headers::CONTENT_TYPE]
headers
else
headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
end
end
def request
@request ||= Rack::Request.new(env)
end
# store read input in env['api.request.input']
def read_body_input
return unless
(request.post? || request.put? || request.patch? || request.delete?) &&
(!request.form_data? || !request.media_type) &&
!request.parseable_data? &&
(request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
return unless (input = env[Grape::Env::RACK_INPUT])
input.rewind
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
begin
read_rack_input(body) if body && !body.empty?
ensure
input.rewind
end
end
# store parsed input in env['api.request.body']
def read_rack_input(body)
fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
unless content_type_for(fmt)
throw :error, status: 406, message: "The requested content-type '#{request.media_type}' is not supported."
end
parser = Grape::Parser.parser_for fmt, options
if parser
begin
body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
if body.is_a?(Hash)
env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env[Grape::Env::RACK_REQUEST_FORM_HASH]
env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
else
body
end
env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
end
rescue Grape::Exceptions::Base => e
raise e
rescue StandardError => e
throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
end
else
env[Grape::Env::API_REQUEST_BODY] = body
end
end
def negotiate_content_type
fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
if content_type_for(fmt)
env[Grape::Env::API_FORMAT] = fmt
else
throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
end
end
def format_from_extension
parts = request.path.split('.')
if parts.size > 1
extension = parts.last
# avoid symbol memory leak on an unknown format
return extension.to_sym if content_type_for(extension)
end
nil
end
def format_from_params
fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
# avoid symbol memory leak on an unknown format
return fmt.to_sym if content_type_for(fmt)
fmt
end
def format_from_header
mime_array.each do |t|
return mime_types[t] if mime_types.key?(t)
end
nil
end
def mime_array
accept = env[Grape::Http::Headers::HTTP_ACCEPT]
return [] unless accept
accept_into_mime_and_quality = %r{
(
\w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
(?:
(?:;[^,]*?)? # optionally multiple formats in a row
;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5)
)?
}x
vendor_prefix_pattern = /vnd\.[^+]+\+/
accept.scan(accept_into_mime_and_quality)
.sort_by { |_, quality_preference| -quality_preference.to_f }
.flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
end
end
end
end
grape-1.0.2/lib/grape/middleware/versioner/ 0000755 0000041 0000041 00000000000 13231337007 020634 5 ustar www-data www-data grape-1.0.2/lib/grape/middleware/versioner/path.rb 0000644 0000041 0000041 00000003547 13231337007 022126 0 ustar www-data www-data require 'grape/middleware/base'
module Grape
module Middleware
module Versioner
# This middleware sets various version related rack environment variables
# based on the uri path and removes the version substring from the uri
# path. If the version substring does not match any potential initialized
# versions, a 404 error is thrown.
#
# Example: For a uri path
# /v1/resource
#
# The following rack env variables are set and path is rewritten to
# '/resource':
#
# env['api.version'] => 'v1'
#
class Path < Base
def default_options
{
pattern: /.*/i
}
end
def before
path = env[Grape::Http::Headers::PATH_INFO].dup
path.sub!(mount_path, '') if mounted_path?(path)
if prefix && path.index(prefix) == 0 # rubocop:disable all
path.sub!(prefix, '')
path = Grape::Router.normalize_path(path)
end
pieces = path.split('/')
potential_version = pieces[1]
return unless potential_version =~ options[:pattern]
throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
env[Grape::Env::API_VERSION] = potential_version
end
private
def mounted_path?(path)
return false unless mount_path && path.start_with?(mount_path)
rest = path.slice(mount_path.length..-1)
rest.start_with?('/') || rest.empty?
end
def mount_path
@mount_path ||= options[:mount_path] && options[:mount_path] != '/' ? options[:mount_path] : ''
end
def prefix
Grape::Router.normalize_path(options[:prefix].to_s) if options[:prefix]
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/versioner/param.rb 0000644 0000041 0000041 00000003315 13231337007 022263 0 ustar www-data www-data require 'grape/middleware/base'
module Grape
module Middleware
module Versioner
# This middleware sets various version related rack environment variables
# based on the request parameters and removes that parameter from the
# request parameters for subsequent middleware and API.
# If the version substring does not match any potential initialized
# versions, a 404 error is thrown.
# If the version substring is not passed the version (highest mounted)
# version will be used.
#
# Example: For a uri path
# /resource?apiver=v1
#
# The following rack env variables are set and path is rewritten to
# '/resource':
#
# env['api.version'] => 'v1'
class Param < Base
def default_options
{
version_options: {
parameter: 'apiver'.freeze
}
}
end
def before
potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
return if potential_version.nil?
throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
env[Grape::Env::API_VERSION] = potential_version
env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
end
private
def paramkey
version_options[:parameter] || default_options[:version_options][:parameter]
end
def version_options
options[:version_options]
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/versioner/header.rb 0000644 0000041 0000041 00000015040 13231337007 022411 0 ustar www-data www-data require 'grape/middleware/base'
require 'grape/middleware/versioner/parse_media_type_patch'
module Grape
module Middleware
module Versioner
# This middleware sets various version related rack environment variables
# based on the HTTP Accept header with the pattern:
# application/vnd.:vendor-:version+:format
#
# Example: For request header
# Accept: application/vnd.mycompany.a-cool-resource-v1+json
#
# The following rack env variables are set:
#
# env['api.type'] => 'application'
# env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json'
# env['api.vendor] => 'mycompany.a-cool-resource'
# env['api.version] => 'v1'
# env['api.format] => 'json'
#
# If version does not match this route, then a 406 is raised with
# X-Cascade header to alert Grape::Router to attempt the next matched
# route.
class Header < Base
VENDOR_VERSION_HEADER_REGEX =
/\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/
HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/
HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/
def before
strict_header_checks if strict?
if media_type || env[Grape::Env::GRAPE_ALLOWED_METHODS]
media_type_header_handler
elsif headers_contain_wrong_vendor?
fail_with_invalid_accept_header!('API vendor not found.')
elsif headers_contain_wrong_version?
fail_with_invalid_version_header!('API version not found.')
end
end
private
def strict_header_checks
strict_accept_header_presence_check
strict_version_vendor_accept_header_presence_check
end
def strict_accept_header_presence_check
return unless header.qvalues.empty?
fail_with_invalid_accept_header!('Accept header must be set.')
end
def strict_version_vendor_accept_header_presence_check
return unless versions.present?
return if an_accept_header_with_version_and_vendor_is_present?
fail_with_invalid_accept_header!('API vendor or version not found.')
end
def an_accept_header_with_version_and_vendor_is_present?
header.qvalues.keys.any? do |h|
VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
end
end
def header
@header ||= rack_accept_header
end
def media_type
@media_type ||= header.best_of(available_media_types)
end
def media_type_header_handler
type, subtype = Rack::Accept::Header.parse_media_type(media_type)
env[Grape::Env::API_TYPE] = type
env[Grape::Env::API_SUBTYPE] = subtype
return unless VENDOR_VERSION_HEADER_REGEX =~ subtype
env[Grape::Env::API_VENDOR] = Regexp.last_match[1]
env[Grape::Env::API_VERSION] = Regexp.last_match[2]
# weird that Grape::Middleware::Formatter also does this
env[Grape::Env::API_FORMAT] = Regexp.last_match[3]
end
def fail_with_invalid_accept_header!(message)
raise Grape::Exceptions::InvalidAcceptHeader
.new(message, error_headers)
end
def fail_with_invalid_version_header!(message)
raise Grape::Exceptions::InvalidVersionHeader
.new(message, error_headers)
end
def available_media_types
available_media_types = []
content_types.each do |extension, _media_type|
versions.reverse_each do |version|
available_media_types += [
"application/vnd.#{vendor}-#{version}+#{extension}",
"application/vnd.#{vendor}-#{version}"
]
end
available_media_types << "application/vnd.#{vendor}+#{extension}"
end
available_media_types << "application/vnd.#{vendor}"
content_types.each do |_, media_type|
available_media_types << media_type
end
available_media_types.flatten
end
def headers_contain_wrong_vendor?
header.values.all? do |header_value|
vendor?(header_value) && request_vendor(header_value) != vendor
end
end
def headers_contain_wrong_version?
header.values.all? do |header_value|
version?(header_value) && !versions.include?(request_version(header_value))
end
end
def rack_accept_header
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
rescue RuntimeError => e
fail_with_invalid_accept_header!(e.message)
end
def versions
options[:versions] || []
end
def vendor
version_options && version_options[:vendor]
end
def strict?
version_options && version_options[:strict]
end
def version_options
options[:version_options]
end
# By default those errors contain an `X-Cascade` header set to `pass`,
# which allows nesting and stacking of routes
# (see Grape::Router for more
# information). To prevent # this behavior, and not add the `X-Cascade`
# header, one can set the `:cascade` option to `false`.
def cascade?
if version_options && version_options.key?(:cascade)
version_options[:cascade]
else
true
end
end
def error_headers
cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
end
# @param [String] media_type a content type
# @return [Boolean] whether the content type sets a vendor
def vendor?(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype[HAS_VENDOR_REGEX]
end
def request_vendor(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
end
def request_version(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
end
# @param [String] media_type a content type
# @return [Boolean] whether the content type sets an API version
def version?(media_type)
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
subtype[HAS_VERSION_REGEX]
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/versioner/parse_media_type_patch.rb 0000644 0000041 0000041 00000001173 13231337007 025654 0 ustar www-data www-data module Rack
module Accept
module Header
class << self
# Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
def parse_media_type(media_type)
# see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names
m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$})
m ? [m[1], m[2], m[3] || ''] : []
end
end
end
class MediaType
def parse_media_type(media_type)
Header.parse_media_type(media_type)
end
end
end
end
grape-1.0.2/lib/grape/middleware/versioner/accept_version_header.rb 0000644 0000041 0000041 00000004134 13231337007 025477 0 ustar www-data www-data require 'grape/middleware/base'
module Grape
module Middleware
module Versioner
# This middleware sets various version related rack environment variables
# based on the HTTP Accept-Version header
#
# Example: For request header
# Accept-Version: v1
#
# The following rack env variables are set:
#
# env['api.version'] => 'v1'
#
# If version does not match this route, then a 406 is raised with
# X-Cascade header to alert Grape::Router to attempt the next matched
# route.
class AcceptVersionHeader < Base
def before
potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
if strict?
# If no Accept-Version header:
if potential_version.empty?
throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
end
end
return if potential_version.empty?
# If the requested version is not supported:
throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version }
env[Grape::Env::API_VERSION] = potential_version
end
private
def versions
options[:versions] || []
end
def strict?
options[:version_options] && options[:version_options][:strict]
end
# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
# of routes (see Grape::Router) for more information). To prevent
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
def cascade?
if options[:version_options] && options[:version_options].key?(:cascade)
options[:version_options][:cascade]
else
true
end
end
def error_headers
cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/stack.rb 0000644 0000041 0000041 00000005465 13231337007 020264 0 ustar www-data www-data module Grape
module Middleware
# Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
# It allows to insert and insert after
class Stack
class Middleware
attr_reader :args, :block, :klass
def initialize(klass, *args, &block)
@klass = klass
@args = args
@block = block
end
def name
klass.name
end
def ==(other)
case other
when Middleware
klass == other.klass
when Class
klass == other || (name.nil? && klass.superclass == other)
end
end
def inspect
klass.to_s
end
end
include Enumerable
attr_accessor :middlewares, :others
def initialize
@middlewares = []
@others = []
end
def each
@middlewares.each { |x| yield x }
end
def size
middlewares.size
end
def last
middlewares.last
end
def [](i)
middlewares[i]
end
def insert(index, *args, &block)
index = assert_index(index, :before)
middleware = self.class::Middleware.new(*args, &block)
middlewares.insert(index, middleware)
end
alias insert_before insert
def insert_after(index, *args, &block)
index = assert_index(index, :after)
insert(index + 1, *args, &block)
end
def use(*args, &block)
middleware = self.class::Middleware.new(*args, &block)
middlewares.push(middleware)
end
def merge_with(middleware_specs)
middleware_specs.each do |operation, *args|
if args.last.is_a?(Proc)
public_send(operation, *args, &args.pop)
else
public_send(operation, *args)
end
end
end
# @return [Rack::Builder] the builder object with our middlewares applied
def build(builder = Rack::Builder.new)
others.shift(others.size).each(&method(:merge_with))
middlewares.each do |m|
m.block ? builder.use(m.klass, *m.args, &m.block) : builder.use(m.klass, *m.args)
end
builder
end
# @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
# @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
def concat(other_specs)
@others << Array(other_specs).reject { |o| o.first == :use }
merge_with Array(other_specs).select { |o| o.first == :use }
end
protected
def assert_index(index, where)
i = index.is_a?(Integer) ? index : middlewares.index(index)
i || raise("No such middleware to insert #{where}: #{index.inspect}")
end
end
end
end
grape-1.0.2/lib/grape/middleware/auth/ 0000755 0000041 0000041 00000000000 13231337007 017561 5 ustar www-data www-data grape-1.0.2/lib/grape/middleware/auth/strategy_info.rb 0000644 0000041 0000041 00000000465 13231337007 022770 0 ustar www-data www-data module Grape
module Middleware
module Auth
StrategyInfo = Struct.new(:auth_class, :settings_fetcher) do
def create(app, options, &block)
strategy_args = settings_fetcher.call(options)
auth_class.new(app, *strategy_args, &block)
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/auth/strategies.rb 0000644 0000041 0000041 00000001217 13231337007 022261 0 ustar www-data www-data module Grape
module Middleware
module Auth
module Strategies
module_function
def add(label, strategy, option_fetcher = ->(_) { [] })
auth_strategies[label] = StrategyInfo.new(strategy, option_fetcher)
end
def auth_strategies
@auth_strategies ||= {
http_basic: StrategyInfo.new(Rack::Auth::Basic, ->(settings) { [settings[:realm]] }),
http_digest: StrategyInfo.new(Rack::Auth::Digest::MD5, ->(settings) { [settings[:realm], settings[:opaque]] })
}
end
def [](label)
auth_strategies[label]
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/auth/base.rb 0000644 0000041 0000041 00000001762 13231337007 021026 0 ustar www-data www-data require 'rack/auth/basic'
module Grape
module Middleware
module Auth
class Base
attr_accessor :options, :app, :env
def initialize(app, **options)
@app = app
@options = options
end
def context
env[Grape::Env::API_ENDPOINT]
end
def call(env)
dup._call(env)
end
def _call(env)
self.env = env
if options.key?(:type)
auth_proc = options[:proc]
auth_proc_context = context
strategy_info = Grape::Middleware::Auth::Strategies[options[:type]]
throw(:error, status: 401, message: 'API Authorization Failed.') unless strategy_info.present?
strategy = strategy_info.create(@app, options) do |*args|
auth_proc_context.instance_exec(*args, &auth_proc)
end
strategy.call(env)
else
app.call(env)
end
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/auth/dsl.rb 0000644 0000041 0000041 00000002357 13231337007 020677 0 ustar www-data www-data require 'rack/auth/basic'
require 'active_support/concern'
module Grape
module Middleware
module Auth
module DSL
extend ActiveSupport::Concern
module ClassMethods
# Add an authentication type to the API. Currently
# only `:http_basic`, `:http_digest` are supported.
def auth(type = nil, options = {}, &block)
if type
namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block))
use Grape::Middleware::Auth::Base, namespace_inheritable(:auth)
else
namespace_inheritable(:auth)
end
end
# Add HTTP Basic authorization to the API.
#
# @param [Hash] options A hash of options.
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
def http_basic(options = {}, &block)
options[:realm] ||= 'API Authorization'
auth :http_basic, options, &block
end
def http_digest(options = {}, &block)
options[:realm] ||= 'API Authorization'
options[:opaque] ||= 'secret'
auth :http_digest, options, &block
end
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/filter.rb 0000644 0000041 0000041 00000000727 13231337007 020440 0 ustar www-data www-data module Grape
module Middleware
# This is a simple middleware for adding before and after filters
# to Grape APIs. It is used like so:
#
# use Grape::Middleware::Filter, before: -> { do_something }, after: -> { do_something }
class Filter < Base
def before
app.instance_eval(&options[:before]) if options[:before]
end
def after
app.instance_eval(&options[:after]) if options[:after]
end
end
end
end
grape-1.0.2/lib/grape/middleware/globals.rb 0000644 0000041 0000041 00000000670 13231337007 020573 0 ustar www-data www-data require 'grape/middleware/base'
module Grape
module Middleware
class Globals < Base
def before
request = Grape::Request.new(@env, build_params_with: @options[:build_params_with])
@env[Grape::Env::GRAPE_REQUEST] = request
@env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers
@env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Grape::Env::RACK_INPUT]
end
end
end
end
grape-1.0.2/lib/grape/middleware/versioner.rb 0000644 0000041 0000041 00000001573 13231337007 021167 0 ustar www-data www-data # Versioners set env['api.version'] when a version is defined on an API and
# on the requests. The current methods for determining version are:
#
# :header - version from HTTP Accept header.
# :path - version from uri. e.g. /v1/resource
# :param - version from uri query string, e.g. /v1/resource?apiver=v1
#
# See individual classes for details.
module Grape
module Middleware
module Versioner
module_function
# @param strategy [Symbol] :path, :header or :param
# @return a middleware class based on strategy
def using(strategy)
case strategy
when :path
Path
when :header
Header
when :param
Param
when :accept_version_header
AcceptVersionHeader
else
raise Grape::Exceptions::InvalidVersionerOption.new(strategy)
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/base.rb 0000644 0000041 0000041 00000004547 13231337007 020071 0 ustar www-data www-data require 'grape/dsl/headers'
module Grape
module Middleware
class Base
attr_reader :app, :env, :options
TEXT_HTML = 'text/html'.freeze
include Grape::DSL::Headers
# @param [Rack Application] app The standard argument for a Rack middleware.
# @param [Hash] options A hash of options, simply stored for use by subclasses.
def initialize(app, **options)
@app = app
@options = default_options.merge(**options)
@app_response = nil
end
def default_options
{}
end
def call(env)
dup.call!(env)
end
def call!(env)
@env = env
before
begin
@app_response = @app.call(@env)
ensure
begin
after_response = after
rescue StandardError => e
warn "caught error of type #{e.class} in after callback inside #{self.class.name} : #{e.message}"
raise e
end
end
response = after_response || @app_response
merge_headers response
response
end
# @abstract
# Called before the application is called in the middleware lifecycle.
def before; end
# @abstract
# Called after the application is called in the middleware lifecycle.
# @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
def after; end
def response
return @app_response if @app_response.is_a?(Rack::Response)
Rack::Response.new(@app_response[2], @app_response[0], @app_response[1])
end
def content_type_for(format)
HashWithIndifferentAccess.new(content_types)[format]
end
def content_types
ContentTypes.content_types_for(options[:content_types])
end
def content_type
content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML
end
def mime_types
types_without_params = {}
content_types.each_pair do |k, v|
types_without_params[v.split(';').first] = k
end
types_without_params
end
private
def merge_headers(response)
return unless headers.is_a?(Hash)
case response
when Rack::Response then response.headers.merge!(headers)
when Array then response[1].merge!(headers)
end
end
end
end
end
grape-1.0.2/lib/grape/middleware/error.rb 0000644 0000041 0000041 00000011530 13231337007 020276 0 ustar www-data www-data require 'grape/middleware/base'
module Grape
module Middleware
class Error < Base
def default_options
{
default_status: 500, # default status returned on error
default_message: '',
format: :txt,
helpers: nil,
formatters: {},
error_formatters: {},
rescue_all: false, # true to rescue all exceptions
rescue_grape_exceptions: false,
rescue_subclasses: true, # rescue subclasses of exceptions listed
rescue_options: {
backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
original_exception: false, # true to display exception
},
rescue_handlers: {}, # rescue handler blocks
base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
all_rescue_handler: nil # rescue handler block to rescue from all exceptions
}
end
def initialize(app, **options)
super
self.class.send(:include, @options[:helpers]) if @options[:helpers]
end
def call!(env)
@env = env
begin
error_response(catch(:error) do
return @app.call(@env)
end)
rescue StandardError => e
is_rescuable = rescuable?(e.class)
if e.is_a?(Grape::Exceptions::Base) && (!is_rescuable || rescuable_by_grape?(e.class))
handler = ->(arg) { error_response(arg) }
else
raise unless is_rescuable
handler = find_handler(e.class)
end
handler.nil? ? handle_error(e) : exec_handler(e, &handler)
end
end
def find_handler(klass)
handler = options[:rescue_handlers].find(-> { [] }) { |error, _| klass <= error }[1]
handler ||= options[:base_only_rescue_handlers][klass]
handler ||= options[:all_rescue_handler]
if handler.instance_of?(Symbol)
raise NoMethodError, "undefined method `#{handler}'" unless respond_to?(handler)
handler = self.class.instance_method(handler).bind(self)
end
handler
end
def rescuable?(klass)
return false if klass == Grape::Exceptions::InvalidVersionHeader
rescue_all? || rescue_class_or_its_ancestor?(klass) || rescue_with_base_only_handler?(klass)
end
def rescuable_by_grape?(klass)
return false if klass == Grape::Exceptions::InvalidVersionHeader
options[:rescue_grape_exceptions]
end
def exec_handler(e, &handler)
if handler.lambda? && handler.arity.zero?
instance_exec(&handler)
else
instance_exec(e, &handler)
end
end
def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type)
rack_response(format_message(message, backtrace, original_exception), status, headers)
end
def handle_error(e)
error_response(message: e.message, backtrace: e.backtrace, original_exception: e)
end
# TODO: This method is deprecated. Refactor out.
def error_response(error = {})
status = error[:status] || options[:default_status]
message = error[:message] || options[:default_message]
headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }
headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
backtrace = error[:backtrace] || error[:original_exception] && error[:original_exception].backtrace || []
original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
rack_response(format_message(message, backtrace, original_exception), status, headers)
end
def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
Rack::Response.new([message], status, headers).finish
end
def format_message(message, backtrace, original_exception = nil)
format = env[Grape::Env::API_FORMAT] || options[:format]
formatter = Grape::ErrorFormatter.formatter_for(format, options)
throw :error,
status: 406,
message: "The requested format '#{format}' is not supported.",
backtrace: backtrace,
original_exception: original_exception unless formatter
formatter.call(message, backtrace, options, env, original_exception)
end
private
def rescue_all?
options[:rescue_all]
end
def rescue_class_or_its_ancestor?(klass)
(options[:rescue_handlers] || []).any? { |error, _handler| klass <= error }
end
def rescue_with_base_only_handler?(klass)
(options[:base_only_rescue_handlers] || []).include?(klass)
end
end
end
end
grape-1.0.2/lib/grape/namespace.rb 0000644 0000041 0000041 00000002272 13231337007 016767 0 ustar www-data www-data module Grape
# A container for endpoints or other namespaces, which allows for both
# logical grouping of endpoints as well as sharing common configuration.
# May also be referred to as group, segment, or resource.
class Namespace
attr_reader :space, :options
# @param space [String] the name of this namespace
# @param options [Hash] options hash
# @option options :requirements [Hash] param-regex pairs, all of which must
# be met by a request's params for all endpoints in this namespace, or
# validation will fail and return a 422.
def initialize(space, **options)
@space = space.to_s
@options = options
end
# Retrieves the requirements from the options hash, if given.
# @return [Hash]
def requirements
options[:requirements] || {}
end
# (see ::joined_space_path)
def self.joined_space(settings)
(settings || []).map(&:space).join('/')
end
# Join the namespaces from a list of settings to create a path prefix.
# @param settings [Array] list of Grape::Util::InheritableSettings.
def self.joined_space_path(settings)
Grape::Router.normalize_path(joined_space(settings))
end
end
end
grape-1.0.2/lib/grape/formatter.rb 0000644 0000041 0000041 00000001423 13231337007 017033 0 ustar www-data www-data module Grape
module Formatter
extend Util::Registrable
class << self
def builtin_formmaters
@builtin_formatters ||= {
json: Grape::Formatter::Json,
jsonapi: Grape::Formatter::Json,
serializable_hash: Grape::Formatter::SerializableHash,
txt: Grape::Formatter::Txt,
xml: Grape::Formatter::Xml
}
end
def formatters(options)
builtin_formmaters.merge(default_elements).merge(options[:formatters] || {})
end
def formatter_for(api_format, **options)
spec = formatters(**options)[api_format]
case spec
when nil
->(obj, _env) { obj }
when Symbol
method(spec)
else
spec
end
end
end
end
end
grape-1.0.2/lib/grape/exceptions/ 0000755 0000041 0000041 00000000000 13231337007 016664 5 ustar www-data www-data grape-1.0.2/lib/grape/exceptions/missing_option.rb 0000644 0000041 0000041 00000000302 13231337007 022245 0 ustar www-data www-data module Grape
module Exceptions
class MissingOption < Base
def initialize(option)
super(message: compose_message(:missing_option, option: option))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/validation.rb 0000644 0000041 0000041 00000001262 13231337007 021344 0 ustar www-data www-data require 'grape/exceptions/base'
module Grape
module Exceptions
class Validation < Grape::Exceptions::Base
attr_accessor :params
attr_accessor :message_key
def initialize(params:, message: nil, **args)
@params = params
if message
@message_key = message if message.is_a?(Symbol)
args[:message] = translate_message(message)
end
super(args)
end
# remove all the unnecessary stuff from Grape::Exceptions::Base like status
# and headers when converting a validation error to json or string
def as_json(*_args)
to_s
end
def to_s
message
end
end
end
end
grape-1.0.2/lib/grape/exceptions/missing_group_type.rb 0000644 0000041 0000041 00000000266 13231337007 023143 0 ustar www-data www-data module Grape
module Exceptions
class MissingGroupTypeError < Base
def initialize
super(message: compose_message(:missing_group_type))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/validation_array_errors.rb 0000644 0000041 0000041 00000000265 13231337007 024140 0 ustar www-data www-data module Grape
module Exceptions
class ValidationArrayErrors < Base
attr_reader :errors
def initialize(errors)
@errors = errors
end
end
end
end
grape-1.0.2/lib/grape/exceptions/invalid_versioner_option.rb 0000644 0000041 0000041 00000000333 13231337007 024322 0 ustar www-data www-data module Grape
module Exceptions
class InvalidVersionerOption < Base
def initialize(strategy)
super(message: compose_message(:invalid_versioner_option, strategy: strategy))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/missing_vendor_option.rb 0000644 0000041 0000041 00000000267 13231337007 023634 0 ustar www-data www-data module Grape
module Exceptions
class MissingVendorOption < Base
def initialize
super(message: compose_message(:missing_vendor_option))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/incompatible_option_values.rb 0000644 0000041 0000041 00000000447 13231337007 024633 0 ustar www-data www-data module Grape
module Exceptions
class IncompatibleOptionValues < Base
def initialize(option1, value1, option2, value2)
super(message: compose_message(:incompatible_option_values, option1: option1, value1: value1, option2: option2, value2: value2))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/validation_errors.rb 0000644 0000041 0000041 00000002520 13231337007 022736 0 ustar www-data www-data require 'grape/exceptions/base'
module Grape
module Exceptions
class ValidationErrors < Grape::Exceptions::Base
include Enumerable
attr_reader :errors
def initialize(errors: [], headers: {}, **_options)
@errors = {}
errors.each do |validation_error|
@errors[validation_error.params] ||= []
@errors[validation_error.params] << validation_error
end
super message: full_messages.join(', '), status: 400, headers: headers
end
def each
errors.each_pair do |attribute, errors|
errors.each do |error|
yield attribute, error
end
end
end
def as_json(**_opts)
errors.map do |k, v|
{
params: k,
messages: v.map(&:to_s)
}
end
end
def to_json(**_opts)
as_json.to_json
end
def full_messages
map { |attributes, error| full_message(attributes, error) }.uniq
end
private
def full_message(attributes, error)
I18n.t(
'grape.errors.format'.to_sym,
default: '%{attributes} %{message}',
attributes: attributes.count == 1 ? translate_attribute(attributes.first) : translate_attributes(attributes),
message: error.message
)
end
end
end
end
grape-1.0.2/lib/grape/exceptions/invalid_with_option_for_represent.rb 0000644 0000041 0000041 00000000315 13231337007 026216 0 ustar www-data www-data module Grape
module Exceptions
class InvalidWithOptionForRepresent < Base
def initialize
super(message: compose_message(:invalid_with_option_for_represent))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/invalid_accept_header.rb 0000644 0000041 0000041 00000000372 13231337007 023470 0 ustar www-data www-data module Grape
module Exceptions
class InvalidAcceptHeader < Base
def initialize(message, headers)
super(message: compose_message(:invalid_accept_header, message: message), status: 406, headers: headers)
end
end
end
end
grape-1.0.2/lib/grape/exceptions/unknown_parameter.rb 0000644 0000041 0000041 00000000305 13231337007 022746 0 ustar www-data www-data module Grape
module Exceptions
class UnknownParameter < Base
def initialize(param)
super(message: compose_message(:unknown_parameter, param: param))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/unknown_options.rb 0000644 0000041 0000041 00000000307 13231337007 022463 0 ustar www-data www-data module Grape
module Exceptions
class UnknownOptions < Base
def initialize(options)
super(message: compose_message(:unknown_options, options: options))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/invalid_version_header.rb 0000644 0000041 0000041 00000000374 13231337007 023720 0 ustar www-data www-data module Grape
module Exceptions
class InvalidVersionHeader < Base
def initialize(message, headers)
super(message: compose_message(:invalid_version_header, message: message), status: 406, headers: headers)
end
end
end
end
grape-1.0.2/lib/grape/exceptions/invalid_formatter.rb 0000644 0000041 0000041 00000000346 13231337007 022725 0 ustar www-data www-data module Grape
module Exceptions
class InvalidFormatter < Base
def initialize(klass, to_format)
super(message: compose_message(:invalid_formatter, klass: klass, to_format: to_format))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/method_not_allowed.rb 0000644 0000041 0000041 00000000306 13231337007 023057 0 ustar www-data www-data module Grape
module Exceptions
class MethodNotAllowed < Base
def initialize(headers)
super(message: '405 Not Allowed', status: 405, headers: headers)
end
end
end
end
grape-1.0.2/lib/grape/exceptions/unknown_validator.rb 0000644 0000041 0000041 00000000340 13231337007 022752 0 ustar www-data www-data module Grape
module Exceptions
class UnknownValidator < Base
def initialize(validator_type)
super(message: compose_message(:unknown_validator, validator_type: validator_type))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/invalid_message_body.rb 0000644 0000041 0000041 00000000351 13231337007 023357 0 ustar www-data www-data module Grape
module Exceptions
class InvalidMessageBody < Base
def initialize(body_format)
super(message: compose_message(:invalid_message_body, body_format: body_format), status: 400)
end
end
end
end
grape-1.0.2/lib/grape/exceptions/base.rb 0000644 0000041 0000041 00000004614 13231337007 020130 0 ustar www-data www-data module Grape
module Exceptions
class Base < StandardError
BASE_MESSAGES_KEY = 'grape.errors.messages'.freeze
BASE_ATTRIBUTES_KEY = 'grape.errors.attributes'.freeze
FALLBACK_LOCALE = :en
attr_reader :status, :message, :headers
def initialize(status: nil, message: nil, headers: nil, **_options)
@status = status
@message = message
@headers = headers
end
def [](index)
send index
end
protected
# TODO: translate attribute first
# if BASE_ATTRIBUTES_KEY.key respond to a string message, then short_message is returned
# if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution
def compose_message(key, **attributes)
short_message = translate_message(key, **attributes)
if short_message.is_a? Hash
@problem = problem(key, **attributes)
@summary = summary(key, **attributes)
@resolution = resolution(key, **attributes)
[['Problem', @problem], ['Summary', @summary], ['Resolution', @resolution]].reduce('') do |message, detail_array|
message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank?
message
end
else
short_message
end
end
def problem(key, attributes)
translate_message("#{key}.problem".to_sym, attributes)
end
def summary(key, attributes)
translate_message("#{key}.summary".to_sym, attributes)
end
def resolution(key, attributes)
translate_message("#{key}.resolution".to_sym, attributes)
end
def translate_attributes(keys, **options)
keys.map do |key|
translate("#{BASE_ATTRIBUTES_KEY}.#{key}", default: key, **options)
end.join(', ')
end
def translate_attribute(key, **options)
translate("#{BASE_ATTRIBUTES_KEY}.#{key}", default: key, **options)
end
def translate_message(key, **options)
case key
when Symbol
translate("#{BASE_MESSAGES_KEY}.#{key}", default: '', **options)
when Proc
key.call
else
key
end
end
def translate(key, **options)
message = ::I18n.translate(key, **options)
message.present? ? message : ::I18n.translate(key, locale: FALLBACK_LOCALE, **options)
end
end
end
end
grape-1.0.2/lib/grape/exceptions/missing_mime_type.rb 0000644 0000041 0000041 00000000323 13231337007 022730 0 ustar www-data www-data module Grape
module Exceptions
class MissingMimeType < Base
def initialize(new_format)
super(message: compose_message(:missing_mime_type, new_format: new_format))
end
end
end
end
grape-1.0.2/lib/grape/exceptions/unsupported_group_type.rb 0000644 0000041 0000041 00000000276 13231337007 024063 0 ustar www-data www-data module Grape
module Exceptions
class UnsupportedGroupTypeError < Base
def initialize
super(message: compose_message(:unsupported_group_type))
end
end
end
end
grape-1.0.2/lib/grape/router.rb 0000644 0000041 0000041 00000011246 13231337007 016354 0 ustar www-data www-data require 'grape/router/route'
module Grape
class Router
attr_reader :map, :compiled
class Any < AttributeTranslator
def initialize(pattern, **attributes)
@pattern = pattern
super(attributes)
end
end
def self.normalize_path(path)
path = "/#{path}"
path.squeeze!('/')
path.sub!(%r{/+\Z}, '')
path = '/' if path == ''
path
end
def self.supported_methods
@supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
end
def initialize
@neutral_map = []
@map = Hash.new { |hash, key| hash[key] = [] }
@optimized_map = Hash.new { |hash, key| hash[key] = // }
end
def compile!
return if compiled
@union = Regexp.union(@neutral_map.map(&:regexp))
self.class.supported_methods.each do |method|
routes = map[method]
@optimized_map[method] = routes.map.with_index do |route, index|
route.index = index
route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/
end
@optimized_map[method] = Regexp.union(@optimized_map[method])
end
@compiled = true
end
def append(route)
map[route.request_method.to_s.upcase] << route
end
def associate_routes(pattern, **options)
regexp = /(?<_#{@neutral_map.length}>)#{pattern.to_regexp}/
@neutral_map << Any.new(pattern, regexp: regexp, index: @neutral_map.length, **options)
end
def call(env)
with_optimization do
response, route = identity(env)
response || rotation(env, route)
end
end
def recognize_path(input)
any = with_optimization { greedy_match?(input) }
return if any == default_response
any.endpoint
end
private
def identity(env)
route = nil
response = transaction(env) do |input, method|
route = match?(input, method)
process_route(route, env) if route
end
[response, route]
end
def rotation(env, exact_route = nil)
response = nil
input, method = *extract_input_and_method(env)
map[method].each do |route|
next if exact_route == route
next unless route.match?(input)
response = process_route(route, env)
break unless cascade?(response)
end
response
end
def transaction(env)
input, method = *extract_input_and_method(env)
response = yield(input, method)
return response if response && !(cascade = cascade?(response))
neighbor = greedy_match?(input)
# If neighbor exists and request method is OPTIONS,
# return response by using #call_with_allow_headers.
return call_with_allow_headers(
env,
neighbor.allow_header,
neighbor.endpoint
) if neighbor && method == 'OPTIONS' && !cascade
route = match?(input, '*')
return neighbor.endpoint.call(env) if neighbor && cascade && route
if route
response = process_route(route, env)
return response if response && !(cascade = cascade?(response))
end
!cascade && neighbor ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
end
def process_route(route, env)
input, = *extract_input_and_method(env)
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
route.exec(env)
end
def make_routing_args(default_args, route, input)
args = default_args || { route_info: route }
args.merge(route.params(input))
end
def extract_input_and_method(env)
input = string_for(env[Grape::Http::Headers::PATH_INFO])
method = env[Grape::Http::Headers::REQUEST_METHOD]
[input, method]
end
def with_optimization
compile! unless compiled
yield || default_response
end
def default_response
[404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
end
def match?(input, method)
current_regexp = @optimized_map[method]
return unless current_regexp.match(input)
last_match = Regexp.last_match
@map[method].detect { |route| last_match["_#{route.index}"] }
end
def greedy_match?(input)
return unless @union.match(input)
last_match = Regexp.last_match
@neutral_map.detect { |route| last_match["_#{route.index}"] }
end
def call_with_allow_headers(env, methods, endpoint)
env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods
endpoint.call(env)
end
def cascade?(response)
response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
end
def string_for(input)
self.class.normalize_path(input)
end
end
end
grape-1.0.2/lib/grape/formatter/ 0000755 0000041 0000041 00000000000 13231337007 016506 5 ustar www-data www-data grape-1.0.2/lib/grape/formatter/xml.rb 0000644 0000041 0000041 00000000425 13231337007 017634 0 ustar www-data www-data module Grape
module Formatter
module Xml
class << self
def call(object, _env)
return object.to_xml if object.respond_to?(:to_xml)
raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
end
end
end
end
end
grape-1.0.2/lib/grape/formatter/json.rb 0000644 0000041 0000041 00000000360 13231337007 020003 0 ustar www-data www-data module Grape
module Formatter
module Json
class << self
def call(object, _env)
return object.to_json if object.respond_to?(:to_json)
::Grape::Json.dump(object)
end
end
end
end
end
grape-1.0.2/lib/grape/formatter/serializable_hash.rb 0000644 0000041 0000041 00000002052 13231337007 022503 0 ustar www-data www-data module Grape
module Formatter
module SerializableHash
class << self
def call(object, _env)
return object if object.is_a?(String)
return ::Grape::Json.dump(serialize(object)) if serializable?(object)
return object.to_json if object.respond_to?(:to_json)
::Grape::Json.dump(object)
end
private
def serializable?(object)
object.respond_to?(:serializable_hash) || object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } || object.is_a?(Hash)
end
def serialize(object)
if object.respond_to? :serializable_hash
object.serializable_hash
elsif object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash }
object.map(&:serializable_hash)
elsif object.is_a?(Hash)
h = {}
object.each_pair do |k, v|
h[k] = serialize(v)
end
h
else
object
end
end
end
end
end
end
grape-1.0.2/lib/grape/formatter/txt.rb 0000644 0000041 0000041 00000000316 13231337007 017652 0 ustar www-data www-data module Grape
module Formatter
module Txt
class << self
def call(object, _env)
object.respond_to?(:to_txt) ? object.to_txt : object.to_s
end
end
end
end
end
grape-1.0.2/lib/grape/parser.rb 0000644 0000041 0000041 00000001171 13231337007 016324 0 ustar www-data www-data module Grape
module Parser
extend Util::Registrable
class << self
def builtin_parsers
@builtin_parsers ||= {
json: Grape::Parser::Json,
jsonapi: Grape::Parser::Json,
xml: Grape::Parser::Xml
}
end
def parsers(options)
builtin_parsers.merge(default_elements).merge(options[:parsers] || {})
end
def parser_for(api_format, **options)
spec = parsers(**options)[api_format]
case spec
when nil
nil
when Symbol
method(spec)
else
spec
end
end
end
end
end
grape-1.0.2/lib/grape/path.rb 0000644 0000041 0000041 00000003523 13231337007 015767 0 ustar www-data www-data module Grape
# Represents a path to an endpoint.
class Path
def self.prepare(raw_path, namespace, settings)
Path.new(raw_path, namespace, settings)
end
attr_reader :raw_path, :namespace, :settings
def initialize(raw_path, namespace, settings)
@raw_path = raw_path
@namespace = namespace
@settings = settings
end
def mount_path
settings[:mount_path]
end
def root_prefix
split_setting(:root_prefix)
end
def uses_specific_format?
if settings.key?(:format) && settings.key?(:content_types)
(settings[:format] && Array(settings[:content_types]).size == 1)
else
false
end
end
def uses_path_versioning?
if settings.key?(:version) && settings[:version_options] && settings[:version_options].key?(:using)
(settings[:version] && settings[:version_options][:using] == :path)
else
false
end
end
def namespace?
namespace && namespace.to_s =~ /^\S/ && namespace != '/'
end
def path?
raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
end
def suffix
if uses_specific_format?
"(.#{settings[:format]})"
elsif !uses_path_versioning? || (namespace? || path?)
'(.:format)'
else
'(/.:format)'
end
end
def path
Grape::Router.normalize_path(parts.join('/'))
end
def path_with_suffix
"#{path}#{suffix}"
end
def to_s
path_with_suffix
end
private
def parts
parts = [mount_path, root_prefix].compact
parts << ':version' if uses_path_versioning?
parts << namespace.to_s
parts << raw_path.to_s
parts.flatten.reject { |part| part == '/' }
end
def split_setting(key)
return if settings[key].nil?
settings[key].to_s.split('/')
end
end
end
grape-1.0.2/lib/grape/parser/ 0000755 0000041 0000041 00000000000 13231337007 015777 5 ustar www-data www-data grape-1.0.2/lib/grape/parser/xml.rb 0000644 0000041 0000041 00000000564 13231337007 017131 0 ustar www-data www-data module Grape
module Parser
module Xml
class << self
def call(object, _env)
::Grape::Xml.parse(object)
rescue ::Grape::Xml::ParseError
# handle XML parsing errors via the rescue handlers or provide error message
raise Grape::Exceptions::InvalidMessageBody, 'application/xml'
end
end
end
end
end
grape-1.0.2/lib/grape/parser/json.rb 0000644 0000041 0000041 00000000570 13231337007 017277 0 ustar www-data www-data module Grape
module Parser
module Json
class << self
def call(object, _env)
::Grape::Json.load(object)
rescue ::Grape::Json::ParseError
# handle JSON parsing errors via the rescue handlers or provide error message
raise Grape::Exceptions::InvalidMessageBody, 'application/json'
end
end
end
end
end
grape-1.0.2/lib/grape/api/ 0000755 0000041 0000041 00000000000 13231337007 015254 5 ustar www-data www-data grape-1.0.2/lib/grape/api/helpers.rb 0000644 0000041 0000041 00000000154 13231337007 017243 0 ustar www-data www-data module Grape
class API
module Helpers
include Grape::DSL::Helpers::BaseHelper
end
end
end
grape-1.0.2/lib/grape/request.rb 0000644 0000041 0000041 00000001560 13231337007 016522 0 ustar www-data www-data module Grape
class Request < Rack::Request
HTTP_PREFIX = 'HTTP_'.freeze
alias rack_params params
def initialize(env, options = {})
extend options[:build_params_with] || Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
super(env)
end
def params
@params ||= build_params
end
def headers
@headers ||= build_headers
end
private
def grape_routing_args
args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup
# preserve version from query string parameters
args.delete(:version)
args.delete(:route_info)
args
end
def build_headers
headers = {}
env.each_pair do |k, v|
next unless k.to_s.start_with? HTTP_PREFIX
k = k[5..-1].split('_').each(&:capitalize!).join('-')
headers[k] = v
end
headers
end
end
end
grape-1.0.2/lib/grape/api.rb 0000644 0000041 0000041 00000020131 13231337007 015576 0 ustar www-data www-data require 'grape/router'
module Grape
# The API class is the primary entry point for creating Grape APIs. Users
# should subclass this class in order to build an API.
class API
include Grape::DSL::API
class << self
attr_reader :instance
# A class-level lock to ensure the API is not compiled by multiple
# threads simultaneously within the same process.
LOCK = Mutex.new
# Clears all defined routes, endpoints, etc., on this API.
def reset!
reset_endpoints!
reset_routes!
reset_validations!
end
# Parses the API's definition and compiles it into an instance of
# Grape::API.
def compile
@instance ||= new
end
# Wipe the compiled API so we can recompile after changes were made.
def change!
@instance = nil
end
# This is the interface point between Rack and Grape; it accepts a request
# from Rack and ultimately returns an array of three values: the status,
# the headers, and the body. See [the rack specification]
# (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
def call(env)
LOCK.synchronize { compile } unless instance
call!(env)
end
# A non-synchronized version of ::call.
def call!(env)
instance.call(env)
end
# (see #cascade?)
def cascade(value = nil)
if value.nil?
inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
else
namespace_inheritable(:cascade, value)
end
end
# see Grape::Router#recognize_path
def recognize_path(path)
LOCK.synchronize { compile } unless instance
instance.router.recognize_path(path)
end
protected
def prepare_routes
endpoints.map(&:routes).flatten
end
# Execute first the provided block, then each of the
# block passed in. Allows for simple 'before' setups
# of settings stack pushes.
def nest(*blocks, &block)
blocks.reject!(&:nil?)
if blocks.any?
instance_eval(&block) if block_given?
blocks.each { |b| instance_eval(&b) }
reset_validations!
else
instance_eval(&block)
end
end
def inherited(subclass)
subclass.reset!
subclass.logger = logger.clone
end
def inherit_settings(other_settings)
top_level_setting.inherit_from other_settings.point_in_time_copy
# Propagate any inherited params down to our endpoints, and reset any
# compiled routes.
endpoints.each do |e|
e.inherit_settings(top_level_setting.namespace_stackable)
e.reset_routes!
end
reset_routes!
end
end
attr_reader :router
# Builds the routes from the defined endpoints, effectively compiling
# this API into a usable form.
def initialize
@router = Router.new
add_head_not_allowed_methods_and_options_methods
self.class.endpoints.each do |endpoint|
endpoint.mount_in(@router)
end
@router.compile!
@router.freeze
end
# Handle a request. See Rack documentation for what `env` is.
def call(env)
result = @router.call(env)
result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade?
result
end
# Some requests may return a HTTP 404 error if grape cannot find a matching
# route. In this case, Grape::Router adds a X-Cascade header to the response
# and sets it to 'pass', indicating to grape's parents they should keep
# looking for a matching route on other resources.
#
# In some applications (e.g. mounting grape on rails), one might need to trap
# errors from reaching upstream. This is effectivelly done by unsetting
# X-Cascade. Default :cascade is true.
def cascade?
return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.keys.include?(:cascade)
return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.namespace_inheritable(:version_options).key?(:cascade)
true
end
reset!
private
# For every resource add a 'OPTIONS' route that returns an HTTP 204 response
# with a list of HTTP methods that can be called. Also add a route that
# will return an HTTP 405 response for any HTTP method that the resource
# cannot handle.
def add_head_not_allowed_methods_and_options_methods
routes_map = {}
self.class.endpoints.each do |endpoint|
routes = endpoint.routes
routes.each do |route|
# using the :any shorthand produces [nil] for route methods, substitute all manually
route_key = route.pattern.to_regexp
routes_map[route_key] ||= {}
route_settings = routes_map[route_key]
route_settings[:pattern] = route.pattern
route_settings[:requirements] = route.requirements
route_settings[:path] = route.origin
route_settings[:methods] ||= []
route_settings[:methods] << route.request_method
route_settings[:endpoint] = route.app
# using the :any shorthand produces [nil] for route methods, substitute all manually
route_settings[:methods] = %w[GET PUT POST DELETE PATCH HEAD OPTIONS] if route_settings[:methods].include?('*')
end
end
# The paths we collected are prepared (cf. Path#prepare), so they
# contain already versioning information when using path versioning.
# Disable versioning so adding a route won't prepend versioning
# informations again.
without_root_prefix do
without_versioning do
routes_map.each do |_, config|
methods = config[:methods]
allowed_methods = methods.dup
unless self.class.namespace_inheritable(:do_not_route_head)
allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
end
allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ')
unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
config[:endpoint].options[:options_route_enabled] = true
end
attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
generate_not_allowed_method(config[:pattern], attributes)
end
end
end
end
# Generate a route that returns an HTTP 405 response for a user defined
# path on methods not specified
def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods
not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)
return if not_allowed_methods.empty?
@router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
end
# Allows definition of endpoints that ignore the versioning configuration
# used by the rest of your API.
def without_versioning(&_block)
old_version = self.class.namespace_inheritable(:version)
old_version_options = self.class.namespace_inheritable(:version_options)
self.class.namespace_inheritable_to_nil(:version)
self.class.namespace_inheritable_to_nil(:version_options)
yield
self.class.namespace_inheritable(:version, old_version)
self.class.namespace_inheritable(:version_options, old_version_options)
end
# Allows definition of endpoints that ignore the root prefix used by the
# rest of your API.
def without_root_prefix(&_block)
old_prefix = self.class.namespace_inheritable(:root_prefix)
self.class.namespace_inheritable_to_nil(:root_prefix)
yield
self.class.namespace_inheritable(:root_prefix, old_prefix)
end
end
end
grape-1.0.2/lib/grape/endpoint.rb 0000644 0000041 0000041 00000033321 13231337007 016652 0 ustar www-data www-data module Grape
# An Endpoint is the proxy scope in which all routing
# blocks are executed. In other words, any methods
# on the instance level of this class may be called
# from inside a `get`, `post`, etc.
class Endpoint
include Grape::DSL::Settings
include Grape::DSL::InsideRoute
attr_accessor :block, :source, :options
attr_reader :env, :request, :headers, :params
class << self
def new(*args, &block)
self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super
end
def before_each(new_setup = false, &block)
@before_each ||= []
if new_setup == false
return @before_each unless block_given?
@before_each << block
else
@before_each = [new_setup]
end
end
def run_before_each(endpoint)
superclass.run_before_each(endpoint) unless self == Endpoint
before_each.each { |blk| blk.call(endpoint) if blk.respond_to?(:call) }
end
# @api private
#
# Create an UnboundMethod that is appropriate for executing an endpoint
# route.
#
# The unbound method allows explicit calls to +return+ without raising a
# +LocalJumpError+. The method will be removed, but a +Proc+ reference to
# it will be returned. The returned +Proc+ expects a single argument: the
# instance of +Endpoint+ to bind to the method during the call.
#
# @param [String, Symbol] method_name
# @return [Proc]
# @raise [NameError] an instance method with the same name already exists
def generate_api_method(method_name, &block)
if method_defined?(method_name)
raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name")
end
define_method(method_name, &block)
method = instance_method(method_name)
remove_method(method_name)
proc do |endpoint_instance|
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
method.bind(endpoint_instance).call
end
end
end
end
# Create a new endpoint.
# @param new_settings [InheritableSetting] settings to determine the params,
# validations, and other properties from.
# @param options [Hash] attributes of this endpoint
# @option options path [String or Array] the path to this endpoint, within
# the current scope.
# @option options method [String or Array] which HTTP method(s) can be used
# to reach this endpoint.
# @option options route_options [Hash]
# @note This happens at the time of API definition, so in this context the
# endpoint does not know if it will be mounted under a different endpoint.
# @yield a block defining what your API should do when this endpoint is hit
def initialize(new_settings, options = {}, &block)
require_option(options, :path)
require_option(options, :method)
self.inheritable_setting = new_settings.point_in_time_copy
route_setting(:saved_declared_params, namespace_stackable(:declared_params))
route_setting(:saved_validations, namespace_stackable(:validations))
namespace_stackable(:representations, []) unless namespace_stackable(:representations)
namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status)
@options = options
@options[:path] = Array(options[:path])
@options[:path] << '/' if options[:path].empty?
@options[:method] = Array(options[:method])
@options[:route_options] ||= {}
@lazy_initialize_lock = Mutex.new
@lazy_initialized = nil
@block = nil
@status = nil
@file = nil
@body = nil
@proc = nil
return unless block_given?
@source = block
@block = self.class.generate_api_method(method_name, &block)
end
# Update our settings from a given set of stackable parameters. Used when
# the endpoint's API is mounted under another one.
def inherit_settings(namespace_stackable)
inheritable_setting.route[:saved_validations] += namespace_stackable[:validations]
parent_declared_params = namespace_stackable[:declared_params]
if parent_declared_params
inheritable_setting.route[:declared_params] ||= []
inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten)
end
endpoints && endpoints.each { |e| e.inherit_settings(namespace_stackable) }
end
def require_option(options, key)
raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
end
def method_name
[options[:method],
Namespace.joined_space(namespace_stackable(:namespace)),
(namespace_stackable(:mount_path) || []).join('/'),
options[:path].join('/')]
.join(' ')
end
def routes
@routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes
end
def reset_routes!
endpoints.each(&:reset_routes!) if endpoints
@namespace = nil
@routes = nil
end
def mount_in(router)
if endpoints
endpoints.each { |e| e.mount_in(router) }
else
reset_routes!
routes.each do |route|
methods = [route.request_method]
if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
methods << Grape::Http::Headers::HEAD
end
methods.each do |method|
unless route.request_method.to_s.upcase == method
route = Grape::Router::Route.new(method, route.origin, route.attributes.to_h)
end
router.append(route.apply(self))
end
end
end
end
def to_routes
route_options = prepare_default_route_attributes
map_routes do |method, path|
path = prepare_path(path)
params = merge_route_options(route_options.merge(suffix: path.suffix))
route = Router::Route.new(method, path.path, params)
route.apply(self)
end.flatten
end
def prepare_routes_requirements
endpoint_requirements = options[:route_options][:requirements] || {}
all_requirements = (namespace_stackable(:namespace).map(&:requirements) << endpoint_requirements)
all_requirements.reduce({}) do |base_requirements, single_requirements|
base_requirements.merge!(single_requirements)
end
end
def prepare_default_route_attributes
{
namespace: namespace,
version: prepare_version,
requirements: prepare_routes_requirements,
prefix: namespace_inheritable(:root_prefix),
anchor: options[:route_options].fetch(:anchor, true),
settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations),
forward_match: options[:forward_match]
}
end
def prepare_version
version = namespace_inheritable(:version) || []
return if version.empty?
version.length == 1 ? version.first.to_s : version
end
def merge_route_options(**default)
options[:route_options].clone.reverse_merge(**default)
end
def map_routes
options[:method].map { |method| options[:path].map { |path| yield method, path } }
end
def prepare_path(path)
path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable])
Path.prepare(path, namespace, path_settings)
end
def namespace
@namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
end
def call(env)
lazy_initialize!
dup.call!(env)
end
def call!(env)
env[Grape::Env::API_ENDPOINT] = self
@env = env
@app.call(env)
end
# Return the collection of endpoints within this endpoint.
# This is the case when an Grape::API mounts another Grape::API.
def endpoints
options[:app].endpoints if options[:app] && options[:app].respond_to?(:endpoints)
end
def equals?(e)
(options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash)
end
protected
def run
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
@header = {}
@request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
@params = @request.params
@headers = @request.headers
cookies.read(@request)
self.class.run_before_each(self)
run_filters befores, :before
if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options?
header 'Allow', allowed_methods
response_object = ''
status 204
else
run_filters before_validations, :before_validation
run_validators validations, request
run_filters after_validations, :after_validation
response_object = @block ? @block.call(self) : nil
end
run_filters afters, :after
cookies.write(header)
# status verifies body presence when DELETE
@body ||= response_object
# The Body commonly is an Array of Strings, the application instance itself, or a File-like object
response_object = file || [body]
[status, header, response_object]
end
end
def build_stack(helpers)
stack = Grape::Middleware::Stack.new
stack.use Rack::Head
stack.use Class.new(Grape::Middleware::Error),
helpers: helpers,
format: namespace_inheritable(:format),
content_types: namespace_stackable_with_hash(:content_types),
default_status: namespace_inheritable(:default_error_status),
rescue_all: namespace_inheritable(:rescue_all),
rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions),
default_error_formatter: namespace_inheritable(:default_error_formatter),
error_formatters: namespace_stackable_with_hash(:error_formatters),
rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {},
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
all_rescue_handler: namespace_inheritable(:all_rescue_handler)
stack.concat namespace_stackable(:middleware)
if namespace_inheritable(:version)
stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil,
version_options: namespace_inheritable(:version_options),
prefix: namespace_inheritable(:root_prefix),
mount_path: namespace_stackable(:mount_path).first
end
stack.use Grape::Middleware::Formatter,
format: namespace_inheritable(:format),
default_format: namespace_inheritable(:default_format) || :txt,
content_types: namespace_stackable_with_hash(:content_types),
formatters: namespace_stackable_with_hash(:formatters),
parsers: namespace_stackable_with_hash(:parsers)
builder = stack.build
builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
builder.to_app
end
def build_helpers
helpers = namespace_stackable(:helpers) || []
Module.new { helpers.each { |mod_to_include| include mod_to_include } }
end
private :build_stack, :build_helpers
def helpers
lazy_initialize! && @helpers
end
def lazy_initialize!
return true if @lazy_initialized
@lazy_initialize_lock.synchronize do
return true if @lazy_initialized
@helpers = build_helpers.tap { |mod| self.class.send(:include, mod) }
@app = options[:app] || build_stack(@helpers)
@lazy_initialized = true
end
end
def run_validators(validator_factories, request)
validation_errors = []
validators = validator_factories.map(&:create_validator)
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
validators.each do |validator|
begin
validator.validate(request)
rescue Grape::Exceptions::Validation => e
validation_errors << e
break if validator.fail_fast?
rescue Grape::Exceptions::ValidationArrayErrors => e
validation_errors += e.errors
break if validator.fail_fast?
end
end
end
validation_errors.any? && raise(Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header)
end
def run_filters(filters, type = :other)
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
(filters || []).each { |filter| instance_eval(&filter) }
end
post_extension = DSL::InsideRoute.post_filter_methods(type)
extend post_extension if post_extension
end
def befores
namespace_stackable(:befores) || []
end
def before_validations
namespace_stackable(:before_validations) || []
end
def after_validations
namespace_stackable(:after_validations) || []
end
def afters
namespace_stackable(:afters) || []
end
def validations
route_setting(:saved_validations) || []
end
def options?
options[:options_route_enabled] &&
env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS
end
end
end
grape-1.0.2/lib/grape/version.rb 0000644 0000041 0000041 00000000116 13231337007 016513 0 ustar www-data www-data module Grape
# The current version of Grape.
VERSION = '1.0.2'.freeze
end
grape-1.0.2/lib/grape/validations/ 0000755 0000041 0000041 00000000000 13231337007 017020 5 ustar www-data www-data grape-1.0.2/lib/grape/validations/params_scope.rb 0000644 0000041 0000041 00000043007 13231337007 022025 0 ustar www-data www-data module Grape
module Validations
class ParamsScope
attr_accessor :element, :parent, :index
attr_reader :type
include Grape::DSL::Parameters
# Open up a new ParamsScope, allowing parameter definitions per
# Grape::DSL::Params.
# @param opts [Hash] options for this scope
# @option opts :element [Symbol] the element that contains this scope; for
# this to be relevant, @parent must be set
# @option opts :parent [ParamsScope] the scope containing this scope
# @option opts :api [API] the API endpoint to modify
# @option opts :optional [Boolean] whether or not this scope needs to have
# any parameters set or not
# @option opts :type [Class] a type meant to govern this scope (deprecated)
# @option opts :type [Hash] group options for this scope
# @option opts :dependent_on [Symbol] if present, this scope should only
# validate if this param is present in the parent scope
# @yield the instance context, open for parameter definitions
def initialize(opts, &block)
@element = opts[:element]
@parent = opts[:parent]
@api = opts[:api]
@optional = opts[:optional] || false
@type = opts[:type]
@group = opts[:group] || {}
@dependent_on = opts[:dependent_on]
@declared_params = []
@index = nil
instance_eval(&block) if block_given?
configure_declared_params
end
# @return [Boolean] whether or not this entire scope needs to be
# validated
def should_validate?(parameters)
return false if @optional && (params(parameters).blank? || all_element_blank?(parameters))
return true if parent.nil?
parent.should_validate?(parameters)
end
def meets_dependency?(params, request_params)
if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
return false
end
return true unless @dependent_on
params = params.with_indifferent_access
@dependent_on.each do |dependency|
if dependency.is_a?(Hash)
dependency_key = dependency.keys[0]
proc = dependency.values[0]
return false unless proc.call(params.try(:[], dependency_key))
elsif params.respond_to?(:key?) && params.try(:[], dependency).blank?
return false
end
end
true
end
# @return [String] the proper attribute name, with nesting considered.
def full_name(name, index: nil)
if nested?
# Find our containing element's name, and append ours.
[@parent.full_name(@element), [@index || index, name].map(&method(:brackets))].compact.join
elsif lateral?
# Find the name of the element as if it was at the same nesting level
# as our parent. We need to forward our index upward to achieve this.
@parent.full_name(name, index: @index)
else
# We must be the root scope, so no prefix needed.
name.to_s
end
end
def brackets(val)
"[#{val}]" if val
end
# @return [Boolean] whether or not this scope is the root-level scope
def root?
!@parent
end
# A nested scope is contained in one of its parent's elements.
# @return [Boolean] whether or not this scope is nested
def nested?
@parent && @element
end
# A lateral scope is subordinate to its parent, but its keys are at the
# same level as its parent and thus is not contained within an element.
# @return [Boolean] whether or not this scope is lateral
def lateral?
@parent && !@element
end
# @return [Boolean] whether or not this scope needs to be present, or can
# be blank
def required?
!@optional
end
protected
# Adds a parameter declaration to our list of validations.
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
def push_declared_params(attrs, **opts)
if lateral?
@parent.push_declared_params(attrs)
else
if opts && opts[:as]
@api.route_setting(:aliased_params, @api.route_setting(:aliased_params) || [])
@api.route_setting(:aliased_params) << { attrs.first => opts[:as] }
end
@declared_params.concat attrs
end
end
private
def require_required_and_optional_fields(context, opts)
if context == :all
optional_fields = Array(opts[:except])
required_fields = opts[:using].keys - optional_fields
else # context == :none
required_fields = Array(opts[:except])
optional_fields = opts[:using].keys - required_fields
end
required_fields.each do |field|
field_opts = opts[:using][field]
raise ArgumentError, "required field not exist: #{field}" unless field_opts
requires(field, field_opts)
end
optional_fields.each do |field|
field_opts = opts[:using][field]
optional(field, field_opts) if field_opts
end
end
def require_optional_fields(context, opts)
optional_fields = opts[:using].keys
optional_fields -= Array(opts[:except]) unless context == :all
optional_fields.each do |field|
field_opts = opts[:using][field]
optional(field, field_opts) if field_opts
end
end
def validate_attributes(attrs, opts, &block)
validations = opts.clone
validations[:type] ||= Array if block
validates(attrs, validations)
end
# Returns a new parameter scope, subordinate to the current one and nested
# under the parameter corresponding to `attrs.first`.
# @param attrs [Array] the attributes passed to the `requires` or
# `optional` invocation that opened this scope.
# @param optional [Boolean] whether the parameter this are nested under
# is optional or not (and hence, whether this block's params will be).
# @yield parameter scope
def new_scope(attrs, optional = false, &block)
# if required params are grouped and no type or unsupported type is provided, raise an error
type = attrs[1] ? attrs[1][:type] : nil
if attrs.first && !optional
raise Grape::Exceptions::MissingGroupTypeError.new if type.nil?
raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type)
end
opts = attrs[1] || { type: Array }
self.class.new(
api: @api,
element: attrs.first,
parent: self,
optional: optional,
type: opts[:type],
&block
)
end
# Returns a new parameter scope, not nested under any current-level param
# but instead at the same level as the current scope.
# @param options [Hash] options to control how this new scope behaves
# @option options :dependent_on [Symbol] if given, specifies that this
# scope should only validate if this parameter from the above scope is
# present
# @yield parameter scope
def new_lateral_scope(options, &block)
self.class.new(
api: @api,
element: nil,
parent: self,
options: @optional,
type: type == Array ? Array : Hash,
dependent_on: options[:dependent_on],
&block
)
end
# Returns a new parameter scope, subordinate to the current one and nested
# under the parameter corresponding to `attrs.first`.
# @param attrs [Array] the attributes passed to the `requires` or
# `optional` invocation that opened this scope.
# @yield parameter scope
def new_group_scope(attrs, &block)
self.class.new(
api: @api,
parent: self,
group: attrs.first,
&block
)
end
# Pushes declared params to parent or settings
def configure_declared_params
if nested?
@parent.push_declared_params [element => @declared_params]
else
@api.namespace_stackable(:declared_params, @declared_params)
@api.route_setting(:declared_params, []) unless @api.route_setting(:declared_params)
@api.route_setting(:declared_params, @api.namespace_stackable(:declared_params).flatten)
end
end
def validates(attrs, validations)
doc_attrs = { required: validations.keys.include?(:presence) }
coerce_type = infer_coercion(validations)
doc_attrs[:type] = coerce_type.to_s if coerce_type
desc = validations.delete(:desc) || validations.delete(:description)
doc_attrs[:desc] = desc if desc
default = validations[:default]
doc_attrs[:default] = default if validations.key?(:default)
if (values_hash = validations[:values]).is_a? Hash
values = values_hash[:value]
# NB: excepts is deprecated
excepts = values_hash[:except]
else
values = validations[:values]
end
doc_attrs[:values] = values if values
except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
# NB. values and excepts should be nil, Proc, Array, or Range.
# Specifically, values should NOT be a Hash
# use values or excepts to guess coerce type when stated type is Array
coerce_type = guess_coerce_type(coerce_type, values, except_values, excepts)
# default value should be present in values array, if both exist and are not procs
check_incompatible_option_values(default, values, except_values, excepts)
# type should be compatible with values array, if both exist
validate_value_coercion(coerce_type, values, except_values, excepts)
doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)
full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
@api.document_attribute(full_attrs, doc_attrs)
# slice out fail_fast attribute
opts = {}
opts[:fail_fast] = validations.delete(:fail_fast) || false
# Validate for presence before any other validators
if validations.key?(:presence) && validations[:presence]
validate('presence', validations[:presence], attrs, doc_attrs, opts)
validations.delete(:presence)
validations.delete(:message) if validations.key?(:message)
end
# Before we run the rest of the validators, let's handle
# whatever coercion so that we are working with correctly
# type casted values
coerce_type validations, attrs, doc_attrs, opts
validations.each do |type, options|
validate(type, options, attrs, doc_attrs, opts)
end
end
# Validate and comprehend the +:type+, +:types+, and +:coerce_with+
# options that have been supplied to the parameter declaration.
# The +:type+ and +:types+ options will be removed from the
# validations list, replaced appropriately with +:coerce+ and
# +:coerce_with+ options that will later be passed to
# {Validators::CoerceValidator}. The type that is returned may be
# used for documentation and further validation of parameter
# options.
#
# @param validations [Hash] list of validations supplied to the
# parameter declaration
# @return [class-like] type to which the parameter will be coerced
# @raise [ArgumentError] if the given type options are invalid
def infer_coercion(validations)
if validations.key?(:type) && validations.key?(:types)
raise ArgumentError, ':type may not be supplied with :types'
end
validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
validations.delete(:types) if validations.key?(:types)
coerce_type = validations[:coerce]
# Special case - when the argument is a single type that is a
# variant-type collection.
if Types.multiple?(coerce_type) && validations.key?(:type)
validations[:coerce] = Types::VariantCollectionCoercer.new(
coerce_type,
validations.delete(:coerce_with)
)
end
validations.delete(:type)
coerce_type
end
# Enforce correct usage of :coerce_with parameter.
# We do not allow coercion without a type, nor with
# +JSON+ as a type since this defines its own coercion
# method.
def check_coerce_with(validations)
return unless validations.key?(:coerce_with)
# type must be supplied for coerce_with..
raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
# but not special JSON types, which
# already imply coercion method
return unless [JSON, Array[JSON]].include? validations[:coerce]
raise ArgumentError, 'coerce_with disallowed for type: JSON'
end
# Add type coercion validation to this scope,
# if any has been specified.
# This validation has special handling since it is
# composited from more than one +requires+/+optional+
# parameter, and needs to be run before most other
# validations.
def coerce_type(validations, attrs, doc_attrs, opts)
check_coerce_with(validations)
return unless validations.key?(:coerce)
coerce_options = {
type: validations[:coerce],
method: validations[:coerce_with],
message: validations[:coerce_message]
}
validate('coerce', coerce_options, attrs, doc_attrs, opts)
validations.delete(:coerce_with)
validations.delete(:coerce)
validations.delete(:coerce_message)
end
def guess_coerce_type(coerce_type, *values_list)
return coerce_type unless coerce_type == Array
values_list.each do |values|
next if !values || values.is_a?(Proc)
return values.first.class if values.is_a?(Range) || !values.empty?
end
coerce_type
end
def check_incompatible_option_values(default, values, except_values, excepts)
return unless default && !default.is_a?(Proc)
if values && !values.is_a?(Proc)
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) \
unless Array(default).all? { |def_val| values.include?(def_val) }
end
if except_values && !except_values.is_a?(Proc)
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \
unless Array(default).none? { |def_val| except_values.include?(def_val) }
end
return unless excepts && !excepts.is_a?(Proc)
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \
unless Array(default).none? { |def_val| excepts.include?(def_val) }
end
def validate(type, options, attrs, doc_attrs, opts)
validator_class = Validations.validators[type.to_s]
raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class
factory = Grape::Validations::ValidatorFactory.new(attributes: attrs,
options: options,
required: doc_attrs[:required],
params_scope: self,
opts: opts,
validator_class: validator_class)
@api.namespace_stackable(:validations, factory)
end
def validate_value_coercion(coerce_type, *values_list)
return unless coerce_type
coerce_type = coerce_type.first if coerce_type.is_a?(Array)
values_list.each do |values|
next if !values || values.is_a?(Proc)
value_types = values.is_a?(Range) ? [values.begin, values.end] : values
if coerce_type == Virtus::Attribute::Boolean
value_types = value_types.map { |type| Virtus::Attribute.build(type) }
end
unless value_types.all? { |v| v.is_a? coerce_type }
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
end
end
end
def extract_message_option(attrs)
return nil unless attrs.is_a?(Array)
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
end
def options_key?(type, key, validations)
validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
end
def all_element_blank?(parameters)
params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?)
end
end
end
end
grape-1.0.2/lib/grape/validations/validator_factory.rb 0000644 0000041 0000041 00000000771 13231337007 023066 0 ustar www-data www-data module Grape
module Validations
class ValidatorFactory
def initialize(**options)
@validator_class = options.delete(:validator_class)
@options = options
end
def create_validator
@validator_class.new(@options[:attributes],
@options[:options],
@options[:required],
@options[:params_scope],
@options[:opts])
end
end
end
end
grape-1.0.2/lib/grape/validations/types.rb 0000644 0000041 0000041 00000013227 13231337007 020516 0 ustar www-data www-data require_relative 'types/build_coercer'
require_relative 'types/custom_type_coercer'
require_relative 'types/custom_type_collection_coercer'
require_relative 'types/multiple_type_coercer'
require_relative 'types/variant_collection_coercer'
require_relative 'types/json'
require_relative 'types/file'
# Patch for Virtus::Attribute::Collection
# See the file for more details
require_relative 'types/virtus_collection_patch'
module Grape
module Validations
# Module for code related to grape's system for
# coercion and type validation of incoming request
# parameters.
#
# Grape uses a number of tests and assertions to
# work out exactly how a parameter should be handled,
# based on the +type+ and +coerce_with+ options that
# may be supplied to {Grape::Dsl::Parameters#requires}
# and {Grape::Dsl::Parameters#optional}. The main
# entry point for this process is {Types.build_coercer}.
module Types
# Instances of this class may be used as tokens to denote that
# a parameter value could not be coerced.
class InvalidValue; end
# Types representing a single value, which are coerced through Virtus
# or special logic in Grape.
PRIMITIVES = [
# Numerical
Integer,
Float,
BigDecimal,
Numeric,
# Date/time
Date,
DateTime,
Time,
# Misc
Virtus::Attribute::Boolean,
String,
Symbol,
Rack::Multipart::UploadedFile
].freeze
# Types representing data structures.
STRUCTURES = [
Hash,
Array,
Set
].freeze
# Types for which Grape provides special coercion
# and type-checking logic.
SPECIAL = {
JSON => Json,
Array[JSON] => JsonArray,
::File => File,
Rack::Multipart::UploadedFile => File
}.freeze
GROUPS = [
Array,
Hash,
JSON,
Array[JSON]
].freeze
# Is the given class a primitive type as recognized by Grape?
#
# @param type [Class] type to check
# @return [Boolean] whether or not the type is known by Grape as a valid
# type for a single value
def self.primitive?(type)
PRIMITIVES.include?(type)
end
# Is the given class a standard data structure (collection or map)
# as recognized by Grape?
#
# @param type [Class] type to check
# @return [Boolean] whether or not the type is known by Grape as a valid
# data structure type
# @note This method does not yet consider 'complex types', which inherit
# Virtus.model.
def self.structure?(type)
STRUCTURES.include?(type)
end
# Is the declared type in fact an array of multiple allowed types?
# For example the declaration +types: [Integer,String]+ will attempt
# first to coerce given values to integer, but will also accept any
# other string.
#
# @param type [Array,Set] type (or type list!) to check
# @return [Boolean] +true+ if the given value will be treated as
# a list of types.
def self.multiple?(type)
(type.is_a?(Array) || type.is_a?(Set)) && type.size > 1
end
# Does the given class implement a type system that Grape
# (i.e. the underlying virtus attribute system) supports
# out-of-the-box? Currently supported are +axiom-types+
# and +virtus+.
#
# The type will be passed to +Virtus::Attribute.build+,
# and the resulting attribute object will be expected to
# respond correctly to +coerce+ and +value_coerced?+.
#
# @param type [Class] type to check
# @return [Boolean] +true+ where the type is recognized
def self.recognized?(type)
return false if type.is_a?(Array) || type.is_a?(Set)
type.is_a?(Virtus::Attribute) ||
type.ancestors.include?(Axiom::Types::Type) ||
type.include?(Virtus::Model::Core)
end
# Does Grape provide special coercion and validation
# routines for the given class? This does not include
# automatic handling for primitives, structures and
# otherwise recognized types. See {Types::SPECIAL}.
#
# @param type [Class] type to check
# @return [Boolean] +true+ if special routines are available
def self.special?(type)
SPECIAL.key? type
end
# Is the declared type a supported group type?
# Currently supported group types are Array, Hash, JSON, and Array[JSON]
#
# @param type [Array,Class] type to check
# @return [Boolean] +true+ if the type is a supported group type
def self.group?(type)
GROUPS.include? type
end
# A valid custom type must implement a class-level `parse` method, taking
# one String argument and returning the parsed value in its correct type.
#
# @param type [Class] type to check
# @return [Boolean] whether or not the type can be used as a custom type
def self.custom?(type)
!primitive?(type) &&
!structure?(type) &&
!multiple?(type) &&
!recognized?(type) &&
!special?(type) &&
type.respond_to?(:parse) &&
type.method(:parse).arity == 1
end
# Is the declared type an +Array+ or +Set+ of a {#custom?} type?
#
# @param type [Array,Class] type to check
# @return [Boolean] true if +type+ is a collection of a type that implements
# its own +#parse+ method.
def self.collection_of_custom?(type)
(type.is_a?(Array) || type.is_a?(Set)) &&
type.length == 1 &&
custom?(type.first)
end
end
end
end
grape-1.0.2/lib/grape/validations/types/ 0000755 0000041 0000041 00000000000 13231337007 020164 5 ustar www-data www-data grape-1.0.2/lib/grape/validations/types/multiple_type_coercer.rb 0000644 0000041 0000041 00000006277 13231337007 025123 0 ustar www-data www-data module Grape
module Validations
module Types
# This class is intended for use with Grape endpoint parameters that
# have been declared to be of variant-type using the +:types+ option.
# +MultipleTypeCoercer+ will build a coercer for each type declared
# in the array passed to +:types+ using {Types.build_coercer}. It will
# apply these coercers to parameter values in the order given to
# +:types+, and will return the value returned by the first coercer
# to successfully coerce the parameter value. Therefore if +String+ is
# an allowed type it should be declared last, since it will always
# successfully "coerce" the value.
class MultipleTypeCoercer
# Construct a new coercer that will attempt to coerce
# values to the given list of types in the given order.
#
# @param types [Array] list of allowed types
# @param method [#call,#parse] method by which values should be
# coerced. See class docs for default behaviour.
def initialize(types, method = nil)
@method = method.respond_to?(:parse) ? method.method(:parse) : method
@type_coercers = types.map do |type|
if Types.multiple? type
VariantCollectionCoercer.new type
else
Types.build_coercer type
end
end
end
# This method is called from somewhere within
# +Virtus::Attribute::coerce+ in order to coerce
# the given value.
#
# @param value [String] value to be coerced, in grape
# this should always be a string.
# @return [Object,InvalidValue] the coerced result, or an instance
# of {InvalidValue} if the value could not be coerced.
def call(value)
return @method.call(value) if @method
@type_coercers.each do |coercer|
coerced = coercer.coerce(value)
return coerced if coercer.value_coerced? coerced
end
# Declare that we couldn't coerce the value in such a way
# that Grape won't ask us again if the value is valid
InvalidValue.new
end
# This method is called from somewhere within
# +Virtus::Attribute::value_coerced?+ in order to
# assert that the value has been coerced successfully.
# Due to Grape's design this will in fact only be called
# if a custom coercion method is being used, since {#call}
# returns an {InvalidValue} object if the value could not
# be coerced.
#
# @param _primitive [Axiom::Types::Type] primitive type
# for the coercion as detected by axiom-types' inference
# system. For custom types this is typically not much use
# (i.e. it is +Axiom::Types::Object+) unless special
# inference rules have been declared for the type.
# @param value [Object] a coerced result returned from {#call}
# @return [true,false] whether or not the coerced value
# satisfies type requirements.
def success?(_primitive, value)
@type_coercers.any? { |coercer| coercer.value_coerced? value }
end
end
end
end
end
grape-1.0.2/lib/grape/validations/types/json.rb 0000644 0000041 0000041 00000004174 13231337007 021470 0 ustar www-data www-data require 'json'
module Grape
module Validations
module Types
# +Virtus::Attribute+ implementation that handles coercion
# and type checking for parameters that are complex types
# given as JSON-encoded strings. It accepts both JSON objects
# and arrays of objects, and will coerce the input to a +Hash+
# or +Array+ object respectively. In either case the Grape
# validation system will apply nested validation rules to
# all returned objects.
class Json < Virtus::Attribute
# Coerce the input into a JSON-like data structure.
#
# @param input [String] a JSON-encoded parameter value
# @return [Hash,Array,nil]
def coerce(input)
# Allow nulls and blank strings
return if input.nil? || input =~ /^\s*$/
JSON.parse(input, symbolize_names: true)
end
# Checks that the input was parsed successfully
# and isn't something odd such as an array of primitives.
#
# @param value [Object] result of {#coerce}
# @return [true,false]
def value_coerced?(value)
value.is_a?(::Hash) || coerced_collection?(value)
end
protected
# Is the value an array of JSON-like objects?
#
# @param value [Object] result of {#coerce}
# @return [true,false]
def coerced_collection?(value)
value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
end
end
# Specialization of the {Json} attribute that is guaranteed
# to return an array of objects. Accepts both JSON-encoded
# objects and arrays of objects, but wraps single objects
# in an Array.
class JsonArray < Json
# See {Json#coerce}. Wraps single objects in an array.
#
# @param input [String] JSON-encoded parameter value
# @return [Array]
def coerce(input)
json = super
Array.wrap(json) unless json.nil?
end
# See {Json#coerced_collection?}
def value_coerced?(value)
coerced_collection? value
end
end
end
end
end
grape-1.0.2/lib/grape/validations/types/variant_collection_coercer.rb 0000644 0000041 0000041 00000004374 13231337007 026102 0 ustar www-data www-data module Grape
module Validations
module Types
# This class wraps {MultipleTypeCoercer}, for use with collections
# that allow members of more than one type.
class VariantCollectionCoercer < Virtus::Attribute
# Construct a new coercer that will attempt to coerce
# a list of values such that all members are of one of
# the given types. The container may also optionally be
# coerced to a +Set+. An arbitrary coercion +method+ may
# be supplied, which will be passed the entire collection
# as a parameter and should return a new collection, or
# may return the same one if no coercion was required.
#
# @param types [Array,Set] list of allowed types,
# also specifying the container type
# @param method [#call,#parse] method by which values should be coerced
def initialize(types, method = nil)
@types = types
@method = method.respond_to?(:parse) ? method.method(:parse) : method
# If we have a coercion method, pass it in here to save
# building another one, even though we call it directly.
@member_coercer = MultipleTypeCoercer.new types, method
end
# Coerce the given value.
#
# @param value [Array] collection of values to be coerced
# @return [Array