grape-0.13.0/0000755000004100000410000000000012563420522012722 5ustar www-datawww-datagrape-0.13.0/Rakefile0000644000004100000410000000330712563420522014372 0ustar www-datawww-datarequire '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' 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: [:rubocop, :spec] begin require 'yard' DOC_FILES = ['lib/**/*.rb', 'README.md'] YARD::Rake::YardocTask.new(:doc) do |t| t.files = DOC_FILES end namespace :doc do YARD::Rake::YardocTask.new(:pages) do |t| t.files = DOC_FILES t.options = ['-o', '../grape.doc/docs'] end namespace :pages do desc 'Check out gh-pages.' task :checkout do dir = File.dirname(__FILE__) + '/../grape.doc' unless Dir.exist?(dir) Dir.mkdir(dir) Dir.chdir(dir) do system('git init') system('git remote add origin git@github.com:ruby-grape/grape.git') system('git pull') system('git checkout gh-pages') end end end desc 'Generate and publish YARD docs to GitHub pages.' task publish: ['doc:pages:checkout', 'doc:pages'] do Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do system('git checkout gh-pages') system('git add .') system('git add -u') system("git commit -m 'Generating docs for version #{Grape::VERSION}.'") system('git push origin gh-pages') end end end end rescue LoadError puts 'You need to install YARD.' end grape-0.13.0/UPGRADING.md0000644000004100000410000003557612563420522014604 0ustar www-datawww-dataUpgrading Grape =============== ### 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. #### 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-0.13.0/Gemfile0000644000004100000410000000023612563420522014216 0ustar www-datawww-datasource 'https://rubygems.org' gemspec group :development, :test do gem 'rubocop', '~> 0.31.0' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' end grape-0.13.0/.rubocop_todo.yml0000644000004100000410000000351312563420522016223 0ustar www-datawww-data# This configuration was generated by `rubocop --auto-gen-config` # on 2015-06-04 09:15:17 -0400 using RuboCop version 0.31.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 37 Metrics/AbcSize: Max: 48 # Offense count: 2 Metrics/BlockNesting: Max: 4 # Offense count: 4 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 252 # Offense count: 23 Metrics/CyclomaticComplexity: Max: 20 # Offense count: 675 # Configuration parameters: AllowURI, URISchemes. Metrics/LineLength: Max: 198 # Offense count: 44 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 35 # Offense count: 8 # Configuration parameters: CountComments. Metrics/ModuleLength: Max: 271 # Offense count: 17 Metrics/PerceivedComplexity: Max: 22 # Offense count: 26 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. Style/BlockDelimiters: Enabled: false # Offense count: 174 Style/Documentation: Enabled: false # Offense count: 7 Style/DoubleNegation: Enabled: false # Offense count: 5 Style/EachWithObject: Enabled: false # Offense count: 15 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false # Offense count: 4 # Cop supports --auto-correct. Style/Lambda: Enabled: false # Offense count: 3 Style/MultilineTernaryOperator: Enabled: false # Offense count: 3 # Configuration parameters: NamePrefix, NamePrefixBlacklist. Style/PredicateName: Enabled: false # Offense count: 13 # Configuration parameters: EnforcedStyle, SupportedStyles. Style/RaiseArgs: Enabled: false grape-0.13.0/.rspec0000644000004100000410000000003712563420522014037 0ustar www-datawww-data--color --format=documentation grape-0.13.0/grape.gemspec0000644000004100000410000000316612563420522015373 0ustar www-datawww-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 'rack', '>= 1.3.0' s.add_runtime_dependency 'rack-mount' s.add_runtime_dependency 'rack-accept' s.add_runtime_dependency 'activesupport' s.add_runtime_dependency 'multi_json', '>= 1.3.2' s.add_runtime_dependency 'multi_xml', '>= 0.5.2' s.add_runtime_dependency 'hashie', '>= 2.1.0' s.add_runtime_dependency 'virtus', '>= 1.0.0' s.add_runtime_dependency 'builder' s.add_development_dependency 'grape-entity', '>= 0.4.4' s.add_development_dependency 'rake' s.add_development_dependency 'maruku' s.add_development_dependency 'yard' s.add_development_dependency 'rack-test' s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'bundler' s.add_development_dependency 'cookiejar' s.add_development_dependency 'rack-contrib' s.add_development_dependency 'mime-types' s.add_development_dependency 'appraisal' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ['lib'] end grape-0.13.0/spec/0000755000004100000410000000000012563420522013654 5ustar www-datawww-datagrape-0.13.0/spec/grape/0000755000004100000410000000000012563420522014752 5ustar www-datawww-datagrape-0.13.0/spec/grape/middleware/0000755000004100000410000000000012563420522017067 5ustar www-datawww-datagrape-0.13.0/spec/grape/middleware/exception_spec.rb0000644000004100000410000001347212563420522022433 0ustar www-datawww-datarequire 'spec_helper' require 'active_support/core_ext/hash' describe Grape::Middleware::Error do # raises a text exception class ExceptionApp class << self def call(_env) fail 'rain!' 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) fail CustomError, status: 400, message: 'failed validation' end end end attr_reader :app it 'does not trap errors by default' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error run ExceptionApp end expect { get '/' }.to raise_error end context 'with rescue_all set to true' do it 'sets the message appropriately' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true run ExceptionApp end get '/' expect(last_response.body).to eq('rain!') end it 'defaults to a 500 status' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true run ExceptionApp end get '/' expect(last_response.status).to eq(500) end it 'is possible to specify a different default status code' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, default_status: 500 run ExceptionApp end get '/' expect(last_response.status).to eq(500) end it 'is possible to return errors in json format' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :json run ExceptionApp end get '/' expect(last_response.body).to eq('{"error":"rain!"}') end it 'is possible to return hash errors in json format' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :json run ErrorHashApp end get '/' expect(['{"error":"rain!","detail":"missing widget"}', '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body) end it 'is possible to return errors in jsonapi format' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :jsonapi run ExceptionApp end get '/' expect(last_response.body).to eq('{"error":"rain!"}') end it 'is possible to return hash errors in jsonapi format' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :jsonapi run ErrorHashApp end get '/' expect(['{"error":"rain!","detail":"missing widget"}', '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body) end it 'is possible to return errors in xml format' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :xml run ExceptionApp end get '/' expect(last_response.body).to eq("\n\n rain!\n\n") end it 'is possible to return hash errors in xml format' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :xml run ErrorHashApp end get '/' expect(["\n\n missing widget\n rain!\n\n", "\n\n rain!\n missing widget\n\n"]).to include(last_response.body) end it 'is possible to specify a custom formatter' do @app ||= 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| { custom_formatter: message }.inspect end } run ExceptionApp end get '/' expect(last_response.body).to eq('{:custom_formatter=>"rain!"}') end it 'does not trap regular error! codes' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error run AccessDeniedApp end get '/' expect(last_response.status).to eq(401) end it 'responds to custom Grape exceptions appropriately' do @app ||= Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: false run CustomErrorApp end get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('failed validation') end end end grape-0.13.0/spec/grape/middleware/versioner/0000755000004100000410000000000012563420522021103 5ustar www-datawww-datagrape-0.13.0/spec/grape/middleware/versioner/header_spec.rb0000644000004100000410000002421412563420522023675 0ustar www-datawww-datarequire '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 or version 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 or version 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::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('API vendor or 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 'fails with 406 Not Acceptable if type is a range' 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 not contain ranges ("*").') end end it 'fails with 406 Not Acceptable if subtype is a range' do expect { subject.call('HTTP_ACCEPT' => 'application/*').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 not contain ranges ("*").') 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 are set' 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 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 type is a range' 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 not contain ranges ("*").') end end it 'fails with 406 Not Acceptable if subtype is a range' do expect { subject.call('HTTP_ACCEPT' => 'application/*').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 not contain ranges ("*").') 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::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('API vendor or version not found') end end end end grape-0.13.0/spec/grape/middleware/versioner/path_spec.rb0000644000004100000410000000267312563420522023406 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Middleware::Versioner::Path do let(:app) { ->(env) { [200, env, env['api.version']] } } 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 before { @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), [:v1, :v2], [:v1, 'v2'], ['v1', :v2]].each do |versions| context 'with specified versions as #{versions}' do before { @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 end grape-0.13.0/spec/grape/middleware/versioner/accept_version_header_spec.rb0000644000004100000410000000631512563420522026763 0ustar www-datawww-datarequire '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 are set' 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-0.13.0/spec/grape/middleware/versioner/param_spec.rb0000644000004100000410000000441512563420522023546 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Middleware::Versioner::Param do let(:app) { ->(env) { [200, env, env['api.version']] } } 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 before { @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 before { @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 it 'returns a 200 when no version is set (matches the first version found)' do @options = { versions: ['v1'], version_options: { using: :header } } env = Rack::MockRequest.env_for('/awesome', params: {}) expect(subject.call(env).first).to eq(200) end end grape-0.13.0/spec/grape/middleware/auth/0000755000004100000410000000000012563420522020030 5ustar www-datawww-datagrape-0.13.0/spec/grape/middleware/auth/strategies_spec.rb0000644000004100000410000000405712563420522023547 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/middleware/auth/dsl_spec.rb0000644000004100000410000000324112563420522022151 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/middleware/auth/base_spec.rb0000644000004100000410000000135112563420522022301 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/middleware/error_spec.rb0000644000004100000410000000324612563420522021564 0ustar www-datawww-datarequire '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 end class ErrApp class << self attr_accessor :error attr_accessor :format def call(_env) throw :error, error end end end def app opts = options Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, opts run ErrApp end end let(:options) { { default_message: 'Aww, hamburgers.' } } it 'sets the status code appropriately' do ErrApp.error = { status: 410 } get '/' expect(last_response.status).to eq(410) end it 'sets the error message appropriately' do ErrApp.error = { message: 'Awesome stuff.' } get '/' expect(last_response.body).to eq('Awesome stuff.') end it 'defaults to a 500 status' do ErrApp.error = {} get '/' expect(last_response.status).to eq(500) end it 'has a default message' do 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 ErrApp.error = { message: { code: 200 } } get '/' expect(last_response.body).to eq({ code: 200 }.to_json) end it 'presents an error message' do 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-0.13.0/spec/grape/middleware/globals_spec.rb0000644000004100000410000000160212563420522022050 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/middleware/versioner_spec.rb0000644000004100000410000000113112563420522022436 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/middleware/base_spec.rb0000644000004100000410000000452612563420522021347 0ustar www-datawww-datarequire '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 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 class ExampleWare < Grape::Middleware::Base def default_options { monkey: true } end end it 'persists the default options' do expect(ExampleWare.new(blank_app).options[:monkey]).to be true end it 'overrides default options when provided' do expect(ExampleWare.new(blank_app, monkey: false).options[:monkey]).to be false end end end end grape-0.13.0/spec/grape/middleware/formatter_spec.rb0000644000004100000410000002255012563420522022435 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Middleware::Formatter do subject { Grape::Middleware::Formatter.new(app) } before { allow(subject).to receive(:dup).and_return(subject) } let(:app) { ->(_env) { [200, {}, [@body || { 'foo' => 'bar' }]] } } context 'serialization' do it 'looks at the bodies for possibly serializable data' do @body = { 'abc' => 'def' } _, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json') bodies.each { |b| expect(b).to eq(MultiJson.dump(@body)) } end it 'calls #to_json since default format is json' do @body = ['foo'] @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 it 'calls #to_json if the content type is jsonapi' do @body = { 'foos' => [{ 'bar' => 'baz' }] } @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 it 'calls #to_xml if the content type is xml' do @body = 'string' @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 context 'error handling' do let(:formatter) { double(:formatter) } before do allow(Grape::Formatter::Base).to receive(:formatter_for) { formatter } end it 'rescues formatter-specific exceptions' do allow(formatter).to receive(:call) { fail 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) { fail StandardError } expect do catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } end.to raise_error 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 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 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 it 'uses default json formatter' do @body = ['blah'] _, _, body = subject.call('PATH_INFO' => '/info.json') expect(body.body).to eq(['["blah"]']) 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 '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 ) expect(subject.env['rack.request.form_hash']['thing']['name']).to eq('Test') 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 end grape-0.13.0/spec/grape/exceptions/0000755000004100000410000000000012563420522017133 5ustar www-datawww-datagrape-0.13.0/spec/grape/exceptions/invalid_accept_header_spec.rb0000644000004100000410000002572212563420522024757 0ustar www-datawww-datarequire '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 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 rescued 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 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 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 rescued 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 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-0.13.0/spec/grape/exceptions/invalid_versioner_option_spec.rb0000644000004100000410000000053312563420522025605 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/spec/grape/exceptions/invalid_formatter_spec.rb0000644000004100000410000000052012563420522024200 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/spec/grape/exceptions/unknown_validator_spec.rb0000644000004100000410000000050612563420522024237 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/spec/grape/exceptions/body_parse_errors_spec.rb0000644000004100000410000000616012563420522024220 0ustar www-datawww-datarequire '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 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-0.13.0/spec/grape/exceptions/missing_mime_type_spec.rb0000644000004100000410000000072512563420522024217 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/exceptions/unknown_options_spec.rb0000644000004100000410000000047612563420522023753 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe Grape::Exceptions::UnknownOptions do describe '#message' do let(:error) do described_class.new([:a, :b]) end it 'contains the problem in the message' do expect(error.message).to include( 'unknown options: ' ) end end end grape-0.13.0/spec/grape/exceptions/missing_option_spec.rb0000644000004100000410000000051012563420522023527 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/spec/grape/exceptions/validation_errors_spec.rb0000644000004100000410000000425212563420522024223 0ustar www-datawww-datarequire '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) { { 'A-Header-Key' => 'A-Header-Value' } } 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_key: 'presence') } let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message_key: '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-0.13.0/spec/grape/api/0000755000004100000410000000000012563420522015523 5ustar www-datawww-datagrape-0.13.0/spec/grape/api/nested_helpers_spec.rb0000644000004100000410000000157712563420522022100 0ustar www-datawww-datarequire 'spec_helper' describe Grape::API::Helpers do subject do 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 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-0.13.0/spec/grape/api/shared_helpers_spec.rb0000644000004100000410000000120112563420522022044 0ustar www-datawww-datarequire 'spec_helper' describe Grape::API::Helpers do module SharedParams extend Grape::API::Helpers params :pagination do optional :page, type: Integer optional :size, type: Integer end end subject do Class.new(Grape::API) do helpers SharedParams 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-0.13.0/spec/grape/api/custom_validations_spec.rb0000644000004100000410000000233712563420522022776 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Validations do before do class DefaultLength < Grape::Validations::Base def validate_param!(attr_name, params) @option = params[:max].to_i if params.key?(:max) unless params[attr_name].length <= @option fail 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 context 'using a custom length validator' do 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 end grape-0.13.0/spec/grape/api/deeply_included_options_spec.rb0000644000004100000410000000177312563420522023776 0ustar www-datawww-datarequire 'spec_helper' module API module Defaults extend ActiveSupport::Concern included do format :json end end module Admin module Defaults extend ActiveSupport::Concern include API::Defaults end class Users < Grape::API include API::Admin::Defaults resource :users do get do status 200 end end end end end class Main < Grape::API mount API::Admin::Users end describe Grape::API do subject { 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-0.13.0/spec/grape/entity_spec.rb0000644000004100000410000002150712563420522017632 0ustar www-datawww-datarequire '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') class TestObject end class FakeCollection def first TestObject.new end end subject.represent TestObject, with: entity subject.get '/example' do present [TestObject.new] end subject.get '/example2' do present 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 [: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 require 'rack/contrib' # 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-0.13.0/spec/grape/api_spec.rb0000644000004100000410000025172712563420522017100 0ustar www-datawww-datarequire 'spec_helper' require 'shared/versioning_examples' require 'grape-entity' 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(Grape::DSL::Configuration.stacked_hash_to_hash(subject.namespace_stackable(: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 'should be able to define 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 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 'v1', using: :path subject.enable_root_route! end it 'without a format' do versioned_get '/', 'v1', using: :path end it 'with a format' do get '/v1/.json' 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 before(:each) do allow_any_instance_of(Object).to receive(:to_json).and_return('abc') allow_any_instance_of(Object).to receive(:to_txt).and_return('def') subject.get('/abc') do Object.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([:get, :post], '/abc') do 'hiya' end subject.endpoints.first.routes.each do |route| expect(route.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 [: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, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql MultiJson.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, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql MultiJson.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, '/', MultiJson.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 MultiJson.dump(object).to_json end end end end end it 'allows for multipart paths' do subject.route([:get, :post], '/:id/first') do 'first' end subject.route([:get, :post], '/:id') do 'ola' end subject.route([: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 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 a method other than the properly constrained one. send(used_verb = verbs[(verbs.index(verb) + 2) % verbs.size], '/example') expect(last_response.status).to eql used_verb == 'options' ? 204 : 405 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 '' expect(last_response.headers['X-Custom-Header']).to eql 'foo' 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 it 'adds an OPTIONS route that returns a 204, an Allow header and a X-Custom-Header' do subject.before { header 'X-Custom-Header', 'foo' } subject.get 'example' do 'example' end options '/example' expect(last_response.status).to eql 204 expect(last_response.body).to eql '' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD' expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'allows HEAD on a GET request' do subject.get 'example' do 'example' end head '/example' expect(last_response.status).to eql 200 expect(last_response.body).to eql '' 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 'options does not exist' do options '/example' expect(last_response.status).to eql 405 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:Fixnum 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') { |io| io.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 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''#{URI.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 = true if block_given? 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 [[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 [ [ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc'], [ApiSpec::PhonyMiddleware, 'foo'] ] end end describe '.use' do it 'adds middleware' do subject.use ApiSpec::PhonyMiddleware, 123 expect(subject.middleware).to eql [[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 [[ApiSpec::PhonyMiddleware, 123]] expect(inner_middleware).to eql [[ApiSpec::PhonyMiddleware, 123], [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 [[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 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_an_instance_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) if ActiveSupport::VERSION::MAJOR >= 4 expect(subject.io).to receive(:write).with("I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : this will be logged\n") else expect(subject.io).to receive(:write).with("this will be logged\n") end 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 fail 'rain!' end expect { get '/exception' }.to raise_error end it 'rescues all errors if rescue_from :all is called' do subject.rescue_from :all subject.get '/exception' do fail '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 fail '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') { fail ArgumentError } subject.get('/unrescued') { fail 'beefcake' } get '/rescued' expect(last_response.status).to eql 500 expect { get '/unrescued' }.to raise_error end context 'CustomError subclass of Grape::Exceptions::Base' do before do class CustomError < Grape::Exceptions::Base; end end it 'does not re-raise exceptions of type Grape::Exceptions::Base' do subject.get('/custom_exception') { fail CustomError } expect { get '/custom_exception' }.not_to raise_error end it 'rescues custom grape exceptions' do subject.rescue_from CustomError do |e| rack_response('New Error', e.status) end subject.get '/custom_error' do fail 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) { fail StandardError } allow(Grape::Formatter::Base).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 fail '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 fail 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 fail 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 fail 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 fail ConnectionError end subject.get '/database' do fail 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 fail 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, -> { rack_response('rescued with a lambda', 400) } subject.get('/rescue_lambda') { fail 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, ->(e) { rack_response(e.message, 400) } subject.get('/rescue_lambda') { fail 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' do it 'rescues an error with the specified message' do def rescue_arg_error Rack::Response.new('rescued with a method', 400) end subject.rescue_from ArgumentError, with: rescue_arg_error subject.get('/rescue_method') { fail ArgumentError } get '/rescue_method' expect(last_response.status).to eq(400) expect(last_response.body).to eq('rescued with a method') end end describe '.rescue_from klass, rescue_subclasses: boolean' do before do module APIErrors class ParentError < StandardError; end class ChildError < ParentError; end end end it 'rescues error as well as subclass errors with rescue_subclasses option set' do subject.rescue_from APIErrors::ParentError, rescue_subclasses: true do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do fail APIErrors::ChildError end subject.get '/caught_parent' do fail APIErrors::ParentError end subject.get '/uncaught_parent' do fail 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 APIErrors::ParentError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do fail 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 APIErrors::ParentError, rescue_subclasses: false do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do fail APIErrors::ChildError end expect { get '/uncaught' }.to raise_error(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 fail '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 fail '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 fail '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 fail '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 class CustomErrorFormatter def self.call(message, _backtrace, _options, _env) "message: #{message} @backtrace" end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, CustomErrorFormatter subject.get '/exception' do fail 'rain!' end get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end end describe 'with' do context 'class' do before :each do class CustomErrorFormatter def self.call(message, _backtrace, _option, _env) "message: #{message} @backtrace" end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, with: CustomErrorFormatter subject.get('/exception') { fail '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 fail '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 fail 'rain!' end get '/exception' json = MultiJson.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 it 'rescues error! and return json' do subject.format :json subject.get '/error' do error!('Access Denied', 401) end get '/error' expect(last_response.body).to eql '{"error":"Access Denied"}' 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 CustomFormatter def self.call(object, _env) "{\"custom_formatter\":\"#{object[:some] }\"}" end end before :each do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, 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 CustomParser def self.call(object, _env) { object.to_sym => object.to_s.reverse } end end before :each do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, 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 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 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 fail '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 fail '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 } 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.route_version).to be_nil expect(route.route_path).to eq('/ping(.:format)') expect(route.route_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].route_path).to eq('/:version/version(.:format)') expect(subject.routes[1].route_path).to eq('/p/:version/n1/n2/version(.:format)') end it 'sets route versions' do expect(subject.routes[0].route_version).to eq('v1') expect(subject.routes[1].route_version).to eq('v2') end it 'sets a nested namespace' do expect(subject.routes[1].route_namespace).to eq('/n1/n2') end it 'sets prefix' do expect(subject.routes[1].route_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 route_params' do expect(subject.routes.map { |route| { params: route.route_params } }).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 route_params' do expect(subject.routes.map { |route| { params: route.route_params } }).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 route_params' do expect(subject.routes.map { |route| { params: route.route_params } }).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.route_settings[:custom]).to eq(key: 'value') end end describe 'status' do it 'can be set to arbitrary Fixnum 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 do; end expect(subject.routes.length).to eq(1) route = subject.routes.first expect(route.route_description).to eq('first method') expect(route.route_foo).to be_nil expect(route.route_params).to eq({}) end it 'describes methods separately' do subject.desc 'first method' subject.get :first do; end subject.desc 'second method' subject.get :second do; end expect(subject.routes.count).to eq(2) expect(subject.routes.map { |route| { description: route.route_description, params: route.route_params } }).to eq [ { description: 'first method', params: {} }, { description: 'second method', params: {} } ] end it 'resets desc' do subject.desc 'first method' subject.get :first do; end subject.get :second do; end expect(subject.routes.map { |route| { description: route.route_description, params: route.route_params } }).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' do; end end expect(subject.routes.map { |route| { description: route.route_description, foo: route.route_foo, params: route.route_params } }).to eq [ { description: 'ns second', foo: 'bar', params: {} } ] end it 'includes details' do subject.desc 'method', details: 'method details' subject.get 'method' do; end expect(subject.routes.map { |route| { description: route.route_description, details: route.route_details, params: route.route_params } }).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 { |route| { description: route.route_description, params: route.route_params } }).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 do; end end subject.params do optional :param2 end subject.namespace 'ns2' do get do; end end routes_doc = subject.routes.map { |route| { description: route.route_description, params: route.route_params } } 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' do; end end routes_doc = subject.routes.map { |route| { description: route.route_description, params: route.route_params } } 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' do; end end end expect(subject.routes.map { |route| { description: route.route_description, params: route.route_params } }).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' do; end expect(subject.routes.map(&:route_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' do; end expect(subject.routes.map { |route| { description: route.route_description, params: route.route_params } }).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' do; end expect(subject.routes.map { |route| { description: route.route_description, params: route.route_params } }).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 { |route| { description: route.route_description, params: route.route_params } }).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 it 'inherits rescues even when some defined by mounted' 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') { fail '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 '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.route_path).to match(%r{\/cool\/awesome}) expect(subject.routes.last.route_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 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 = MultiJson.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.route_path end subject.get '/path' do route.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.route_description end subject.desc 'returns parameters', params: { 'x' => 'y' } subject.get '/params/:id' do route.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 ActiveSupport::OrderedHash[ :example1, 'example1', :example2, 'example2' ] end get '/example' expect(last_response.status).to eq(200) expect(last_response.body).to eq <<-XML example1 example2 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 example1 example2 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') 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-0.13.0/spec/grape/endpoint_spec.rb0000644000004100000410000007665512563420522020154 0ustar www-datawww-datarequire '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 'should be settable via block' do block = lambda { |_endpoint| 'noop' } Grape::Endpoint.before_each(&block) expect(Grape::Endpoint.before_each).to eq(block) end it 'should be settable via reference' do block = lambda { |_endpoint| 'noop' } Grape::Endpoint.before_each block expect(Grape::Endpoint.before_each).to eq(block) end it 'should be 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 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 '#declared' do before do subject.params do requires :first optional :second optional :third, default: 'third-default' optional :nested, type: Hash do optional :fourth end end end it 'has as many keys as there are declared params' do inner_params = nil subject.get '/declared' do inner_params = declared(params).keys '' end get '/declared?first=present' expect(last_response.status).to eq(200) expect(inner_params.size).to eq(4) end it 'has a optional param with default value all the time' do inner_params = nil subject.get '/declared' do inner_params = declared(params) '' end get '/declared?first=one' expect(last_response.status).to eq(200) expect(inner_params[:third]).to eql('third-default') end it 'builds nested params' do inner_params = nil subject.get '/declared' do inner_params = declared(params) '' end get '/declared?first=present&nested[fourth]=1' expect(last_response.status).to eq(200) expect(inner_params[:nested].keys.size).to eq 1 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 inner_params = nil subject.get '/declared' do inner_params = declared(params) '' end get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2' expect(last_response.status).to eq(200) expect(inner_params[:nested].size).to eq 2 end it 'builds nested params' do inner_params = nil subject.get '/declared' do inner_params = declared(params) '' end get '/declared?first=present&nested[fourth]=1' expect(last_response.status).to eq(200) expect(inner_params[:nested].keys.size).to eq 1 end context 'sets nested array when the param is missing' do it 'to be array when include_missing is true' do inner_params = nil subject.get '/declared' do inner_params = declared(params, include_missing: true) '' end get '/declared?first=present' expect(last_response.status).to eq(200) expect(inner_params[:nested]).to be_a(Array) end it 'to be nil when include_missing is false' do inner_params = nil subject.get '/declared' do inner_params = declared(params, include_missing: false) '' end get '/declared?first=present' expect(last_response.status).to eq(200) expect(inner_params[:nested]).to be_nil end end it 'filters out any additional params that are given' do inner_params = nil subject.get '/declared' do inner_params = declared(params) '' end get '/declared?first=one&other=two' expect(last_response.status).to eq(200) expect(inner_params.key?(:other)).to eq false end it 'stringifies if that option is passed' do inner_params = nil subject.get '/declared' do inner_params = declared(params, stringify: true) '' end get '/declared?first=one&other=two' expect(last_response.status).to eq(200) expect(inner_params['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', MultiJson.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', MultiJson.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) 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, 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 inner_params = nil subject.get '/declared' do inner_params = declared(params, include_missing: false) '' end get '/declared?first=present&nested[fourth]=&nested[nested_nested][sixth]=sixth' expect(last_response.status).to eq(200) expect(inner_params[:first]).to eq 'present' expect(inner_params[:nested].keys).to eq %w(fourth fifth nested_nested) expect(inner_params[:nested][:fourth]).to eq '' expect(inner_params[:nested][:nested_nested].keys).to eq %w(sixth seven) expect(inner_params[:nested][:nested_nested][:sixth]).to eq 'sixth' end end describe '#declared; call from child namespace' do before do subject.format :json subject.namespace :something do params do requires :id, type: Integer end resource ':id' do params do requires :foo optional :bar end get do { params: params, declared_params: declared(params) } end params do requires :happy optional :days end get '/test' do { params: params, declared_params: declared(params, include_parent_namespaces: false) } end end end end it 'should include params defined in the parent namespace' do get '/something/123', foo: 'test', extra: 'hello' expect(last_response.status).to eq 200 json = JSON.parse(last_response.body, symbolize_names: true) expect(json[:params][:id]).to eq 123 expect(json[:declared_params].keys).to match_array [:foo, :bar, :id] end it 'does not include params defined in the parent namespace with include_parent_namespaces: false' do get '/something/123/test', happy: 'test', extra: 'hello' expect(last_response.status).to eq 200 json = JSON.parse(last_response.body, symbolize_names: true) expect(json[:params][:id]).to eq 123 expect(json[:declared_params].keys).to match_array [:happy, :days] 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', MultiJson.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 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 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', MultiJson.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 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 '/', MultiJson.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 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 '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 '' 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' 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 '' 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 end context 'anchoring' do verbs = %w(post get head delete put options patch) verbs.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 context 'request' do it 'should be 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: an_instance_of(Grape::Endpoint), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: [], type: :before_validation }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: [], type: :after_validation }), have_attributes(name: 'endpoint_render.grape', payload: { endpoint: an_instance_of(Grape::Endpoint) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: [], type: :after }), have_attributes(name: 'endpoint_run.grape', payload: { endpoint: an_instance_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: an_instance_of(Grape::Endpoint), env: an_instance_of(Hash) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: [], type: :before_validation }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: [], type: :after_validation }), have_attributes(name: 'endpoint_render.grape', payload: { endpoint: an_instance_of(Grape::Endpoint) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), filters: [], type: :after }) ) end end end grape-0.13.0/spec/grape/validations/0000755000004100000410000000000012563420522017267 5ustar www-datawww-datagrape-0.13.0/spec/grape/validations/params_scope_spec.rb0000644000004100000410000002165712563420522023315 0ustar www-datawww-datarequire '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.route_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.route_params expect(documentation).to have_key('object') expect(documentation['object']).not_to have_key(:default) end end context 'setting description' do [: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 class CustomType attr_reader :value def self.parse(value) fail if value == 'invalid' new(value) end def initialize(value) @value = value end end it 'coerces the parameter via the type\'s parse method' do subject.params do requires :foo, type: 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 '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 '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 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 '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 '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 '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 end end grape-0.13.0/spec/grape/validations/attributes_iterator_spec.rb0000644000004100000410000000011612563420522024723 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Validations::AttributesIterator do end grape-0.13.0/spec/grape/validations/validators/0000755000004100000410000000000012563420522021437 5ustar www-datawww-datagrape-0.13.0/spec/grape/validations/validators/exactly_one_of_spec.rb0000644000004100000410000000416612563420522026003 0ustar www-datawww-datarequire '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) { [: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-0.13.0/spec/grape/validations/validators/coerce_spec.rb0000644000004100000410000001554712563420522024252 0ustar www-datawww-data# encoding: utf-8 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 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('Fixnum') 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('Fixnum') 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 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('Fixnum') 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 'file' do subject.params do requires :file, coerce: 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) 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('Fixnum') end end end end grape-0.13.0/spec/grape/validations/validators/default_spec.rb0000644000004100000410000001612412563420522024426 0ustar www-datawww-datarequire '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 end end end def app ValidationsSpec::DefaultValidatorSpec::API 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 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-0.13.0/spec/grape/validations/validators/at_least_one_of_spec.rb0000644000004100000410000000367012563420522026125 0ustar www-datawww-datarequire '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) { [: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-0.13.0/spec/grape/validations/validators/zh-CN.yml0000644000004100000410000000033612563420522023103 0ustar www-datawww-datazh-CN: grape: errors: format: ! '%{attributes}%{message}' attributes: age: 年龄 messages: coerce: 'æ ¼å¼ä¸æ­£ç¡®' presence: '请填写' regexp: 'æ ¼å¼ä¸æ­£ç¡®' grape-0.13.0/spec/grape/validations/validators/presence_spec.rb0000644000004100000410000001546312563420522024613 0ustar www-datawww-datarequire '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 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 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"}') 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-0.13.0/spec/grape/validations/validators/values_spec.rb0000644000004100000410000002071412563420522024301 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Validations::ValuesValidator do class ValuesModel DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'] class << self def values @values ||= [] [DEFAULT_VALUES + @values].flatten.uniq end def add_value(value) @values ||= [] @values << value end end end module ValidationsSpec module ValuesValidatorSpec class API < Grape::API default_format :json params do requires :type, values: ValuesModel.values end get '/' 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: -> { ValuesModel.values }, default: 'valid-type2' end get '/lambda' do { type: params[:type] } end 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' end end end def app ValidationsSpec::ValuesValidatorSpec::API 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 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 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 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 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 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 '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: 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 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 end grape-0.13.0/spec/grape/validations/validators/allow_blank_spec.rb0000644000004100000410000001751512563420522025274 0ustar www-datawww-datarequire '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 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: Fixnum, allow_blank: true end get '/allow_fixnum_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' end end end def app ValidationsSpec::AllowBlankValidatorSpec::API end context 'invalid input' do it 'refuses empty string' do get '/', 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 '/', name: ' ' expect(last_response.status).to eq(400) get '/', name: " \n " expect(last_response.status).to eq(400) get '/', name: "\n" expect(last_response.status).to eq(400) end it 'refuses nil' do get '/', name: nil expect(last_response.status).to eq(400) end end context 'valid input' do it 'accepts valid input' do get '/', 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 fixnum allow_blank' do get '/allow_fixnum_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 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-0.13.0/spec/grape/validations/validators/regexp_spec.rb0000644000004100000410000000151612563420522024273 0ustar www-datawww-datarequire 'spec_helper' describe Grape::Validations::RegexpValidator do module ValidationsSpec module RegexpValidatorSpec class API < Grape::API default_format :json params do requires :name, regexp: /^[a-z]+$/ end get do end end end end def app ValidationsSpec::RegexpValidatorSpec::API end context 'invalid input' do it 'refuses inapppopriate' do get '/', name: 'invalid name' expect(last_response.status).to eq(400) end it 'refuses empty' do get '/', name: '' expect(last_response.status).to eq(400) 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 end grape-0.13.0/spec/grape/validations/validators/mutual_exclusion_spec.rb0000644000004100000410000000345612563420522026406 0ustar www-datawww-datarequire '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) { [: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-0.13.0/spec/grape/validations/validators/all_or_none_spec.rb0000644000004100000410000000335612563420522025274 0ustar www-datawww-datarequire '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) { [: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-0.13.0/spec/grape/dsl/0000755000004100000410000000000012563420522015534 5ustar www-datawww-datagrape-0.13.0/spec/grape/dsl/request_response_spec.rb0000644000004100000410000001612412563420522022505 0ustar www-datawww-datarequire '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 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: 'ExampleHandler' end end describe 'list of exceptions is passed' do it 'sets hash of exceptions as rescue handlers' do expect(subject).to receive(:namespace_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_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_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_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 expect(subject).to receive(:namespace_stackable).with(:rescue_handlers, StandardError => an_instance_of(Proc)) expect(subject).to receive(:namespace_stackable).with(:rescue_options, with: 'ExampleHandler') subject.rescue_from StandardError, with: 'ExampleHandler' 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-0.13.0/spec/grape/dsl/parameters_spec.rb0000644000004100000410000001205612563420522021242 0ustar www-datawww-datarequire '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 # rubocop:disable TrivialAccessors def validate_attributes_reader @validate_attributes end # rubocop:enable TrivialAccessors def push_declared_params(*args) @push_declared_params = args end # rubocop:disable TrivialAccessors def push_declared_params_reader @push_declared_params end # rubocop:enable TrivialAccessors def validates(*args) @validates = *args end # rubocop:disable TrivialAccessors def validates_reader @validates end # rubocop:enable TrivialAccessors 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(Grape::DSL::Configuration).to receive(:stacked_hash_to_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(Grape::DSL::Configuration).to receive(:stacked_hash_to_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: true }]) 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 '#mutually_exclusive' do it 'adds an mutally exclusive parameter validation' do subject.mutually_exclusive :media, :audio expect(subject.validates_reader).to eq([[:media, :audio], { mutual_exclusion: true }]) 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([[:media, :audio], { exactly_one_of: true }]) 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([[:media, :audio], { at_least_one_of: true }]) 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([[:media, :audio], { all_or_none_of: true }]) 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-0.13.0/spec/grape/dsl/routing_spec.rb0000644000004100000410000002023212563420522020561 0ustar www-datawww-datarequire '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 '.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(Grape::DSL::Configuration).to receive(:stacked_hash_to_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-0.13.0/spec/grape/dsl/configuration_spec.rb0000644000004100000410000000446412563420522021752 0ustar www-datawww-datarequire 'spec_helper' module Grape module DSL module ConfigurationSpec class Dummy include Grape::DSL::Configuration end end describe Configuration do subject { Class.new(ConfigurationSpec::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 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 end end end end grape-0.13.0/spec/grape/dsl/helpers_spec.rb0000644000004100000410000000245612563420522020544 0ustar www-datawww-datarequire 'spec_helper' module Grape module DSL module HelpersSpec class Dummy include Grape::DSL::Helpers # rubocop:disable TrivialAccessors def self.mod namespace_stackable(:helpers).first end # rubocop:enable TrivialAccessors end 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.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 expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original subject.helpers(mod, &proc) expect(subject.mod).to eq mod end end end end end grape-0.13.0/spec/grape/dsl/validations_spec.rb0000644000004100000410000000375012563420522021415 0ustar www-datawww-datarequire '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 { fail '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-0.13.0/spec/grape/dsl/inside_route_spec.rb0000644000004100000410000002265212563420522021573 0ustar www-datawww-datarequire '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 describe Endpoint do subject { 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 DELETE 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 '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 Fixnum status codes' do expect { subject.status 210 }.to_not raise_error end it 'raises error if status is not a fixnum or symbol' do expect { subject.status Object.new } .to raise_error(ArgumentError, 'Status code must be Fixnum or Symbol.') end end 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 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 before do subject.file 'file' end it 'returns value wrapped in FileResponse' do expect(subject.file).to eq Grape::Util::FileResponse.new('file') end end it 'returns default' do expect(subject.file).to be nil end end describe '#stream' do describe 'set' do before do subject.header 'Cache-Control', 'cache' subject.header 'Content-Length', 123 subject.header 'Transfer-Encoding', 'base64' subject.stream 'file' end it 'returns value wrapped in FileResponse' do expect(subject.stream).to eq Grape::Util::FileResponse.new('file') end it 'also sets result of file to value wrapped in FileResponse' do expect(subject.file).to eq Grape::Util::FileResponse.new('file') 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['rack.routing_args'] = {} subject.env['rack.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_mock1) do entity_mock1 = Object.new allow(entity_mock1).to receive(:represent).and_return(dummy1: 'dummy1') entity_mock1 end let(:entity_mock2) do entity_mock2 = Object.new allow(entity_mock2).to receive(:represent).and_return(dummy2: 'dummy2') entity_mock2 end describe 'instance' do before do subject.present 'dummy1', with: entity_mock1 subject.present 'dummy2', with: entity_mock2 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_mock1) do entity_mock1 = Object.new allow(entity_mock1).to receive(:represent).and_return(dummy1: 'dummy1') entity_mock1 end let(:entity_mock2) do entity_mock2 = Object.new allow(entity_mock2).to receive(:represent).and_return('not a hash') entity_mock2 end describe 'instance' do it 'fails' do subject.present 'dummy1', with: entity_mock1 expect do subject.present 'dummy2', with: entity_mock2 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 'returns an empty hash' do expect(subject.declared({})).to eq({}) end end end end end grape-0.13.0/spec/grape/dsl/middleware_spec.rb0000644000004100000410000000135012563420522021207 0ustar www-datawww-datarequire '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) { ->() {} } describe '.use' do it 'adds a middleware' do expect(subject).to receive(:namespace_stackable).with(:middleware, [:my_middleware, :arg1, proc]) subject.use :my_middleware, :arg1, &proc end end describe '.middleware' do it 'returns the middleware stack' do subject.use :my_middleware, :arg1, &proc expect(subject.middleware).to eq [[:my_middleware, :arg1, proc]] end end end end end grape-0.13.0/spec/grape/dsl/callbacks_spec.rb0000644000004100000410000000222012563420522021006 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/dsl/settings_spec.rb0000644000004100000410000001547412563420522020746 0ustar www-datawww-datarequire '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 '#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 '#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 '#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 '#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 [:foo_bar, :foo_bar_2] subject.namespace_end expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] subject.namespace_end 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 '#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 [:obj3, :obj2, :obj1] end end end end end grape-0.13.0/spec/grape/integration/0000755000004100000410000000000012563420522017275 5ustar www-datawww-datagrape-0.13.0/spec/grape/integration/rack_spec.rb0000644000004100000410000000160712563420522021560 0ustar www-datawww-datarequire '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, release = Rack.release.split('.').map(&:to_i) pending 'Rack 1.5.3 or 1.6.1 required' unless major >= 1 && ((minor == 5 && release >= 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-0.13.0/spec/grape/presenters/0000755000004100000410000000000012563420522017144 5ustar www-datawww-datagrape-0.13.0/spec/grape/presenters/presenter_spec.rb0000644000004100000410000000321512563420522022513 0ustar www-datawww-datarequire 'spec_helper' module Grape module Presenters 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 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 { InsideRouteSpec::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-0.13.0/spec/grape/validations_spec.rb0000644000004100000410000013251212563420522020632 0ustar www-datawww-datarequire '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([: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([: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([: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([: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, 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' }, { 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 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 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 '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', key: 'world'] 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 DateRangeValidations class DateRangeValidator < Grape::Validations::Base def validate_param!(attr_name, params) unless params[attr_name][:from] <= params[attr_name][:to] fail 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 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: 'Job', parents: [{ name: 'Joy' }] } ] # 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[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[parents] is missing') end it 'errors when param is not an Array' do # NOTE: would be nicer if these just returned 'children is invalid' get '/within_array', children: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid, children[name] is missing, children[parents] is missing, children[parents] is invalid, children[parents][name] is missing') get '/within_array', children: { name: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid, children[parents] is missing') get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[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, planets[name] is missing') 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, moons[name] is missing') 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, stars[name] is missing') 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[parents][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[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[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, items[key] is missing') 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[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[optional_subitems][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[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[optional_subitems][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) unless params[attr_name] == 'im custom' fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'is not custom!' end 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) unless params[attr_name] == @option[:text] fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: @option[:error_message] end end end end before do subject.params do optional :custom, customvalidator_with_options: { text: 'im custom with options', error_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 module SharedParams extend Grape::DSL::Helpers::BaseHelper params :pagination do end end subject.helpers SharedParams end end context 'can be included in usual params' do before do module SharedParams extend Grape::DSL::Helpers::BaseHelper params :period do optional :start_date optional :end_date end end subject.helpers SharedParams 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 [: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 [: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: [: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: [: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.route_params['first_name'][:documentation]).to eq(documentation) end end context 'mutually exclusive' do context 'optional params' do 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 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.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 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.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 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-0.13.0/spec/grape/util/0000755000004100000410000000000012563420522015727 5ustar www-datawww-datagrape-0.13.0/spec/grape/util/inheritable_values_spec.rb0000644000004100000410000000426412563420522023141 0ustar www-datawww-datarequire '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-0.13.0/spec/grape/util/parameter_types_spec.rb0000644000004100000410000000253012563420522022472 0ustar www-datawww-datarequire 'spec_helper' describe Grape::ParameterTypes do class FooType def self.parse(_) end end class BarType def self.parse end 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?(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 '::custom_type?' do it 'returns false if the type does not respond to :parse' do expect(described_class.custom_type?(Object)).to be_falsy end it 'returns true if the type responds to :parse with one argument' do expect(described_class.custom_type?(FooType)).to be_truthy end it 'returns false if the type\'s #parse method takes other than one argument' do expect(described_class.custom_type?(BarType)).to be_falsy end end end grape-0.13.0/spec/grape/util/inheritable_setting_spec.rb0000644000004100000410000002066312563420522023320 0ustar www-datawww-datarequire '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.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.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 '#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 [:namespace_stackable_foo_bar, :other_thing] expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_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.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(route: { route_thing: :route_foo_bar }) end end end end end grape-0.13.0/spec/grape/util/stackable_values_spec.rb0000644000004100000410000000753412563420522022607 0ustar www-datawww-datarequire '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 [: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 [: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 [: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 [:foo, :bar] end it 'can handle array values' do subject[:some_thing] = :foo subject[:some_thing] = [:bar, :more] expect(subject[:some_thing]).to eq [:foo, [:bar, :more]] parent[:some_thing_else] = [:foo, :bar] subject[:some_thing_else] = [:some, :bar, :foo] expect(subject[:some_thing_else]).to eq [[:foo, :bar], [:some, :bar, :foo]] end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing] = [:bar, :more] subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq(some_thing: [:foo, [: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] = [:bar, :more] grandchild[:some_thing] = :grand_foo_bar grandchild[:some_thing_more] = :foo_bar expect(grandchild.clone.to_hash).to eq(some_thing: [:foo, [: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-0.13.0/spec/grape/util/strict_hash_configuration_spec.rb0000644000004100000410000000172612563420522024536 0ustar www-datawww-datarequire '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: [: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-0.13.0/spec/grape/path_spec.rb0000644000004100000410000001635412563420522017256 0ustar www-datawww-datarequire '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 '#has_namespace?' do it 'is false when the namespace is nil' do path = Path.new(anything, nil, anything) expect(path).not_to have_namespace end it 'is false when the namespace starts with whitespace' do path = Path.new(anything, ' /foo', anything) expect(path).not_to have_namespace end it 'is false when the namespace is the root path' do path = Path.new(anything, '/', anything) expect(path).not_to have_namespace end it 'is true otherwise' do path = Path.new(anything, '/world', anything) expect(path).to have_namespace end end describe '#has_path?' do it 'is false when the path is nil' do path = Path.new(nil, anything, anything) expect(path).not_to have_path end it 'is false when the path starts with whitespace' do path = Path.new(' /foo', anything, anything) expect(path).not_to have_path end it 'is false when the path is the root path' do path = Path.new('/', anything, anything) expect(path).not_to have_path end it 'is true otherwise' do path = Path.new('/hello', anything, anything) expect(path).to have_path 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-0.13.0/spec/grape/loading_spec.rb0000644000004100000410000000145212563420522017730 0ustar www-datawww-datarequire '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-0.13.0/spec/spec_helper.rb0000644000004100000410000000124212563420522016471 0ustar www-datawww-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.setup :default, :test require 'json' require 'rack/test' require 'base64' require 'cookiejar' require 'json' require 'mime/types' 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.raise_errors_for_deprecations! config.before(:each) { Grape::Util::InheritableSetting.reset_global! } end grape-0.13.0/spec/shared/0000755000004100000410000000000012563420522015122 5ustar www-datawww-datagrape-0.13.0/spec/shared/versioning_examples.rb0000644000004100000410000001105512563420522021532 0ustar www-datawww-datashared_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 end grape-0.13.0/spec/support/0000755000004100000410000000000012563420522015370 5ustar www-datawww-datagrape-0.13.0/spec/support/endpoint_faker.rb0000644000004100000410000000070612563420522020710 0ustar www-datawww-datamodule 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-0.13.0/spec/support/versioned_helpers.rb0000644000004100000410000000266212563420522021443 0ustar www-datawww-data# Versioning # 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 fail 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] }" } else fail 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 grape-0.13.0/spec/support/content_type_helpers.rb0000644000004100000410000000050312563420522022150 0ustar www-datawww-datamodule ContentTypeHelpers %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 include(ContentTypeHelpers) grape-0.13.0/spec/support/basic_auth_encode_helpers.rb0000644000004100000410000000014612563420522023057 0ustar www-datawww-datadef encode_basic_auth(username, password) 'Basic ' + Base64.encode64("#{username}:#{password}") end grape-0.13.0/spec/support/file_streamer.rb0000644000004100000410000000027012563420522020535 0ustar www-datawww-dataclass 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-0.13.0/.travis.yml0000644000004100000410000000040212563420522015027 0ustar www-datawww-datalanguage: ruby rvm: - 2.2 - 2.1 - 2.0.0 - rbx-2.2.10 - jruby-19mode - ruby-head - jruby-head matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head gemfile: - Gemfile - gemfiles/rails_3.gemfile - gemfiles/rails_4.gemfile grape-0.13.0/lib/0000755000004100000410000000000012563420522013470 5ustar www-datawww-datagrape-0.13.0/lib/grape/0000755000004100000410000000000012563420522014566 5ustar www-datawww-datagrape-0.13.0/lib/grape/middleware/0000755000004100000410000000000012563420522016703 5ustar www-datawww-datagrape-0.13.0/lib/grape/middleware/formatter.rb0000644000004100000410000001236712563420522021244 0ustar www-datawww-datarequire 'grape/middleware/base' module Grape module Middleware class Formatter < Base def default_options { default_format: :txt, formatters: {}, parsers: {} } end def before negotiate_content_type read_body_input end def after status, headers, bodies = *@app_response if bodies.is_a?(Grape::Util::FileResponse) headers = ensure_content_type(headers) response = Rack::Response.new([], status, headers) do |resp| resp.body = bodies.file end else # Allow content-type to be explicitly overwritten api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env['api.format'] formatter = Grape::Formatter::Base.formatter_for(api_format, options) begin bodymap = bodies.collect do |body| formatter.call(body, env) end headers = ensure_content_type(headers) response = Rack::Response.new(bodymap, status, headers) rescue Grape::Exceptions::InvalidFormatter => e throw :error, status: 500, message: e.message end end response end private # 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['api.format'])) end end def request @request ||= Rack::Request.new(env) end # store read input in env['api.request.input'] def read_body_input if (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') if (input = env['rack.input']) input.rewind body = env['api.request.input'] = input.read begin read_rack_input(body) if body && body.length > 0 ensure input.rewind end end end end # store parsed input in env['api.request.body'] def read_rack_input(body) fmt = mime_types[request.media_type] if request.media_type fmt ||= options[:default_format] if content_type_for(fmt) parser = Grape::Parser::Base.parser_for fmt, options if parser begin body = (env['api.request.body'] = parser.call(body, env)) if body.is_a?(Hash) if env['rack.request.form_hash'] env['rack.request.form_hash'] = env['rack.request.form_hash'].merge(body) else env['rack.request.form_hash'] = body end env['rack.request.form_input'] = env['rack.input'] end rescue Grape::Exceptions::Base => e raise e rescue StandardError => e throw :error, status: 400, message: e.message end else env['api.request.body'] = body end else throw :error, status: 406, message: "The requested content-type '#{request.media_type}' is not supported." 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['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 } .map { |mime, _| mime.sub(vendor_prefix_pattern, '') } end end end end grape-0.13.0/lib/grape/middleware/versioner/0000755000004100000410000000000012563420522020717 5ustar www-datawww-datagrape-0.13.0/lib/grape/middleware/versioner/path.rb0000644000004100000410000000272512563420522022206 0ustar www-datawww-datarequire '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 if prefix && path.index(prefix) == 0 path.sub!(prefix, '') path = Rack::Mount::Utils.normalize_path(path) end pieces = path.split('/') potential_version = pieces[1] if potential_version =~ options[:pattern] if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } throw :error, status: 404, message: '404 API Version Not Found' end env['api.version'] = potential_version end end private def prefix Rack::Mount::Utils.normalize_path(options[:prefix].to_s) if options[:prefix] end end end end end grape-0.13.0/lib/grape/middleware/versioner/param.rb0000644000004100000410000000277212563420522022354 0ustar www-datawww-datarequire '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 { parameter: 'apiver' } end def before paramkey = options[:parameter] potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey] unless potential_version.nil? if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } end env['api.version'] = potential_version env['rack.request.query_hash'].delete(paramkey) if env.key? 'rack.request.query_hash' end end end end end end grape-0.13.0/lib/grape/middleware/versioner/header.rb0000644000004100000410000001212512563420522022475 0ustar www-datawww-datarequire 'grape/middleware/base' 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-v1+json # # The following rack env variables are set: # # env['api.type'] => 'application' # env['api.subtype'] => 'vnd.mycompany-v1+json' # env['api.vendor] => 'mycompany' # 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 Rack::Mount to attempt the next matched # route. class Header < Base def before header = rack_accept_header if strict? # If no Accept header: if header.qvalues.empty? fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must be set.', error_headers) end # Remove any acceptable content types with ranges. header.qvalues.reject! do |media_type, _| Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' } end # If all Accept headers included a range: if header.qvalues.empty? fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must not contain ranges ("*").', error_headers) end end media_type = header.best_of available_media_types if media_type type, subtype = Rack::Accept::Header.parse_media_type media_type env['api.type'] = type env['api.subtype'] = subtype if /\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/ =~ subtype env['api.vendor'] = Regexp.last_match[1] env['api.version'] = Regexp.last_match[2] env['api.format'] = Regexp.last_match[3] # weird that Grape::Middleware::Formatter also does this end # If none of the available content types are acceptable: elsif strict? fail Grape::Exceptions::InvalidAcceptHeader.new('406 Not Acceptable', error_headers) # If all acceptable content types specify a vendor or version that doesn't exist: elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) } fail Grape::Exceptions::InvalidAcceptHeader.new('API vendor or version not found.', error_headers) end end private 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 rack_accept_header Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT] rescue RuntimeError => e raise Grape::Exceptions::InvalidAcceptHeader.new(e.message, error_headers) end def versions options[:versions] || [] end def vendor options[:version_options] && options[:version_options][:vendor] 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 [Rack::Mount](https://github.com/josh/rack-mount) 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 # @param [String] media_type a content type # @return [Boolean] whether the content type sets a vendor def has_vendor?(media_type) _, subtype = Rack::Accept::Header.parse_media_type media_type subtype[/\Avnd\.[a-z0-9*.]+/] 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[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/] end end end end end grape-0.13.0/lib/grape/middleware/versioner/accept_version_header.rb0000644000004100000410000000424312563420522025563 0ustar www-datawww-datarequire '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 Rack::Mount 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 unless potential_version.empty? # If the requested version is not supported: unless versions.any? { |v| v.to_s == potential_version } throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' end env['api.version'] = potential_version end 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 [Rack::Mount](https://github.com/josh/rack-mount) 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-0.13.0/lib/grape/middleware/auth/0000755000004100000410000000000012563420522017644 5ustar www-datawww-datagrape-0.13.0/lib/grape/middleware/auth/strategy_info.rb0000644000004100000410000000046512563420522023053 0ustar www-datawww-datamodule 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-0.13.0/lib/grape/middleware/auth/strategies.rb0000644000004100000410000000122012563420522022336 0ustar www-datawww-datamodule 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-0.13.0/lib/grape/middleware/auth/base.rb0000644000004100000410000000176312563420522021112 0ustar www-datawww-datarequire '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['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-0.13.0/lib/grape/middleware/auth/dsl.rb0000644000004100000410000000235312563420522020756 0ustar www-datawww-datarequire '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, { type: type.to_sym, proc: block }.merge(options)) 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-0.13.0/lib/grape/middleware/filter.rb0000644000004100000410000000073712563420522020524 0ustar www-datawww-datamodule 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: lambda { do_something }, after: lambda { 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-0.13.0/lib/grape/middleware/globals.rb0000644000004100000410000000053712563420522020660 0ustar www-datawww-datarequire 'grape/middleware/base' module Grape module Middleware class Globals < Base def before request = Grape::Request.new(@env) @env['grape.request'] = request @env['grape.request.headers'] = request.headers @env['grape.request.params'] = request.params if @env['rack.input'] end end end end grape-0.13.0/lib/grape/middleware/versioner.rb0000644000004100000410000000157212563420522021251 0ustar www-datawww-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 fail Grape::Exceptions::InvalidVersionerOption.new(strategy) end end end end end grape-0.13.0/lib/grape/middleware/base.rb0000644000004100000410000000316612563420522020150 0ustar www-datawww-datamodule Grape module Middleware class Base attr_reader :app, :env, :options # @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) end def default_options {} end def call(env) dup.call!(env) end def call!(env) @env = env before @app_response = @app.call(@env) after || @app_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['api.format'] || options[:format]) || 'text/html' end def mime_types content_types.each_with_object({}) do |(k, v), types_without_params| types_without_params[k] = v.split(';').first end.invert end end end end grape-0.13.0/lib/grape/middleware/error.rb0000644000004100000410000000654412563420522020372 0ustar www-datawww-datarequire '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, formatters: {}, error_formatters: {}, rescue_all: false, # true to rescue all exceptions rescue_subclasses: true, # rescue subclasses of exceptions listed rescue_options: { backtrace: false }, # true to display backtrace 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 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 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] handler end def rescuable?(klass) options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, _handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass) end def exec_handler(e, &handler) if handler.lambda? && handler.arity == 0 instance_exec(&handler) else instance_exec(e, &handler) end end def error!(message, status = options[:default_status], headers = {}, backtrace = []) headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }.merge(headers) rack_response(format_message(message, backtrace), status, headers) end def handle_error(e) error_response(message: e.message, backtrace: e.backtrace) 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] || [] rack_response(format_message(message, backtrace), 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) format = env['api.format'] || options[:format] formatter = Grape::ErrorFormatter::Base.formatter_for(format, options) throw :error, status: 406, message: "The requested format '#{format}' is not supported." unless formatter formatter.call(message, backtrace, options, env) end end end end grape-0.13.0/lib/grape/namespace.rb0000644000004100000410000000230112563420522017043 0ustar www-datawww-datamodule Grape # A container for endpoints or other namespaces, which allows for both # logical grouping of endpoints as well as sharing commonconfiguration. # 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) Rack::Mount::Utils.normalize_path(joined_space(settings)) end end end grape-0.13.0/lib/grape/exceptions/0000755000004100000410000000000012563420522016747 5ustar www-datawww-datagrape-0.13.0/lib/grape/exceptions/missing_option.rb0000644000004100000410000000032512563420522022335 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class MissingOption < Base def initialize(option) super(message: compose_message('missing_option', option: option)) end end end end grape-0.13.0/lib/grape/exceptions/validation.rb0000644000004100000410000000120412563420522021423 0ustar www-datawww-datarequire 'grape/exceptions/base' module Grape module Exceptions class Validation < Grape::Exceptions::Base attr_accessor :params def initialize(args = {}) fail 'Params are missing:' unless args.key? :params @params = args[:params] args[:message] = translate_message(args[:message_key]) if args.key? :message_key super 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-0.13.0/lib/grape/exceptions/missing_group_type.rb0000644000004100000410000000031112563420522023215 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class MissingGroupTypeError < Base def initialize super(message: compose_message('missing_group_type')) end end end end grape-0.13.0/lib/grape/exceptions/invalid_versioner_option.rb0000644000004100000410000000035612563420522024412 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class InvalidVersionerOption < Base def initialize(strategy) super(message: compose_message('invalid_versioner_option', strategy: strategy)) end end end end grape-0.13.0/lib/grape/exceptions/missing_vendor_option.rb0000644000004100000410000000031212563420522023706 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class MissingVendorOption < Base def initialize super(message: compose_message('missing_vendor_option')) end end end end grape-0.13.0/lib/grape/exceptions/incompatible_option_values.rb0000644000004100000410000000047212563420522024714 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/lib/grape/exceptions/validation_errors.rb0000644000004100000410000000251212563420522023022 0ustar www-datawww-datarequire 'grape/exceptions/base' module Grape module Exceptions class ValidationErrors < Grape::Exceptions::Base include Enumerable attr_reader :errors def initialize(args = {}) @errors = {} args[:errors].each do |validation_error| @errors[validation_error.params] ||= [] @errors[validation_error.params] << validation_error end super message: full_messages.join(', '), status: 400, headers: args[: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-0.13.0/lib/grape/exceptions/invalid_with_option_for_represent.rb0000644000004100000410000000034012563420522026277 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class InvalidWithOptionForRepresent < Base def initialize super(message: compose_message('invalid_with_option_for_represent')) end end end end grape-0.13.0/lib/grape/exceptions/invalid_accept_header.rb0000644000004100000410000000041512563420522023551 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/lib/grape/exceptions/unknown_parameter.rb0000644000004100000410000000033012563420522023027 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class UnknownParameter < Base def initialize(param) super(message: compose_message('unknown_parameter', param: param)) end end end end grape-0.13.0/lib/grape/exceptions/unknown_options.rb0000644000004100000410000000033212563420522022544 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class UnknownOptions < Base def initialize(options) super(message: compose_message('unknown_options', options: options)) end end end end grape-0.13.0/lib/grape/exceptions/invalid_formatter.rb0000644000004100000410000000037112563420522023006 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/lib/grape/exceptions/unknown_validator.rb0000644000004100000410000000036312563420522023042 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/lib/grape/exceptions/invalid_message_body.rb0000644000004100000410000000037412563420522023447 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/lib/grape/exceptions/base.rb0000644000004100000410000000442612563420522020214 0ustar www-datawww-datamodule Grape module Exceptions class Base < StandardError BASE_MESSAGES_KEY = 'grape.errors.messages' BASE_ATTRIBUTES_KEY = 'grape.errors.attributes' FALLBACK_LOCALE = :en attr_reader :status, :message, :headers def initialize(args = {}) @status = args[:status] || nil @message = args[:message] || nil @headers = args[:headers] || nil 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", attributes) end def summary(key, attributes) translate_message("#{key}.summary", attributes) end def resolution(key, attributes) translate_message("#{key}.resolution", attributes) end def translate_attributes(keys, options = {}) keys.map do |key| translate("#{BASE_ATTRIBUTES_KEY}.#{key}", { default: key }.merge(options)) end.join(', ') end def translate_attribute(key, options = {}) translate("#{BASE_ATTRIBUTES_KEY}.#{key}", { default: key }.merge(options)) end def translate_message(key, options = {}) translate("#{BASE_MESSAGES_KEY}.#{key}", { default: '' }.merge(options)) end def translate(key, options = {}) message = ::I18n.translate(key, options) message.present? ? message : ::I18n.translate(key, options.merge(locale: FALLBACK_LOCALE)) end end end end grape-0.13.0/lib/grape/exceptions/missing_mime_type.rb0000644000004100000410000000034612563420522023020 0ustar www-datawww-data# encoding: utf-8 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-0.13.0/lib/grape/exceptions/unsupported_group_type.rb0000644000004100000410000000032112563420522024135 0ustar www-datawww-data# encoding: utf-8 module Grape module Exceptions class UnsupportedGroupTypeError < Base def initialize super(message: compose_message('unsupported_group_type')) end end end end grape-0.13.0/lib/grape/formatter/0000755000004100000410000000000012563420522016571 5ustar www-datawww-datagrape-0.13.0/lib/grape/formatter/xml.rb0000644000004100000410000000042412563420522017716 0ustar www-datawww-datamodule Grape module Formatter module Xml class << self def call(object, _env) return object.to_xml if object.respond_to?(:to_xml) fail Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') end end end end end grape-0.13.0/lib/grape/formatter/json.rb0000644000004100000410000000035412563420522020071 0ustar www-datawww-datamodule Grape module Formatter module Json class << self def call(object, _env) return object.to_json if object.respond_to?(:to_json) MultiJson.dump(object) end end end end end grape-0.13.0/lib/grape/formatter/serializable_hash.rb0000644000004100000410000000206712563420522022574 0ustar www-datawww-datamodule Grape module Formatter module SerializableHash class << self def call(object, _env) return object if object.is_a?(String) return MultiJson.dump(serialize(object)) if serializable?(object) return object.to_json if object.respond_to?(:to_json) MultiJson.dump(object) end private def serializable?(object) object.respond_to?(:serializable_hash) || object.is_a?(Array) && !object.map { |o| o.respond_to? :serializable_hash }.include?(false) || object.is_a?(Hash) end def serialize(object) if object.respond_to? :serializable_hash object.serializable_hash elsif object.is_a?(Array) && !object.map { |o| o.respond_to? :serializable_hash }.include?(false) object.map(&:serializable_hash) elsif object.is_a?(Hash) object.inject({}) do |h, (k, v)| h[k] = serialize(v) h end else object end end end end end end grape-0.13.0/lib/grape/formatter/txt.rb0000644000004100000410000000031612563420522017735 0ustar www-datawww-datamodule 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-0.13.0/lib/grape/formatter/base.rb0000644000004100000410000000133412563420522020031 0ustar www-datawww-datamodule Grape module Formatter module Base class << self FORMATTERS = { json: Grape::Formatter::Json, jsonapi: Grape::Formatter::Json, serializable_hash: Grape::Formatter::SerializableHash, txt: Grape::Formatter::Txt, xml: Grape::Formatter::Xml } def formatters(options) FORMATTERS.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 end grape-0.13.0/lib/grape/path.rb0000644000004100000410000000320112563420522016043 0ustar www-datawww-datamodule Grape # Represents a path to an endpoint. class Path def self.prepare(raw_path, namespace, settings) Path.new(raw_path, namespace, settings).path_with_suffix 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? !!(settings[:format] && settings[:content_types].size == 1) end def uses_path_versioning? !!(settings[:version] && settings[:version_options][:using] == :path) end def has_namespace? namespace && namespace.to_s =~ /^\S/ && namespace != '/' end def has_path? raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/' end def suffix if uses_specific_format? "(.#{settings[:format]})" elsif !uses_path_versioning? || (has_namespace? || has_path?) '(.:format)' else '(/.:format)' end end def path Rack::Mount::Utils.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-0.13.0/lib/grape/parser/0000755000004100000410000000000012563420522016062 5ustar www-datawww-datagrape-0.13.0/lib/grape/parser/xml.rb0000644000004100000410000000055412563420522017213 0ustar www-datawww-datamodule Grape module Parser module Xml class << self def call(object, _env) MultiXml.parse(object) rescue MultiXml::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-0.13.0/lib/grape/parser/json.rb0000644000004100000410000000056012563420522017361 0ustar www-datawww-datamodule Grape module Parser module Json class << self def call(object, _env) MultiJson.load(object) rescue MultiJson::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-0.13.0/lib/grape/parser/base.rb0000644000004100000410000000110512563420522017316 0ustar www-datawww-datamodule Grape module Parser module Base class << self PARSERS = { json: Grape::Parser::Json, jsonapi: Grape::Parser::Json, xml: Grape::Parser::Xml } def parsers(options) PARSERS.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 end grape-0.13.0/lib/grape/api/0000755000004100000410000000000012563420522015337 5ustar www-datawww-datagrape-0.13.0/lib/grape/api/helpers.rb0000644000004100000410000000015412563420522017326 0ustar www-datawww-datamodule Grape class API module Helpers include Grape::DSL::Helpers::BaseHelper end end end grape-0.13.0/lib/grape/api.rb0000644000004100000410000001610312563420522015665 0ustar www-datawww-datamodule 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! @route_set = Rack::Mount::RouteSet.new @endpoints = [] @routes = nil 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 # Create a scope without affecting the URL. # # @param _name [Symbol] Purely placebo, just allows to name the scope to # make the code more readable. def scope(_name = nil, &block) within_namespace do nest(block) end end # (see #cascade?) def cascade(value = nil) if value.nil? inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !!namespace_inheritable(:cascade) : true else namespace_inheritable(:cascade, value) end 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 endpoints.each(&:reset_routes!) @routes = nil end end # Builds the routes from the defined endpoints, effectively compiling # this API into a usable form. def initialize @route_set = Rack::Mount::RouteSet.new add_head_not_allowed_methods_and_options_methods self.class.endpoints.each do |endpoint| endpoint.mount_in(@route_set) end @route_set.freeze end # Handle a request. See Rack documentation for what `env` is. def call(env) result = @route_set.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, Rack::Mount 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 methods_per_path = {} self.class.endpoints.each do |endpoint| routes = endpoint.routes routes.each do |route| methods_per_path[route.route_path] ||= [] methods_per_path[route.route_path] << route.route_method 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 methods_per_path.each do |path, 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 = ([Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ') unless self.class.namespace_inheritable(:do_not_route_options) unless allowed_methods.include?(Grape::Http::Headers::OPTIONS) self.class.options(path, {}) do header 'Allow', allow_header status 204 '' end end end 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) self.class.route(not_allowed_methods, path) do header 'Allow', allow_header status 405 '' end end end end 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-0.13.0/lib/grape/endpoint.rb0000644000004100000410000002700212563420522016734 0ustar www-datawww-datamodule 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 attr_accessor :block, :source, :options attr_reader :env, :request, :headers, :params include Grape::DSL::InsideRoute class << self def before_each(new_setup = false, &block) if new_setup == false if block_given? @before_each = block else return @before_each end else @before_each = new_setup end 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 instance_methods.include?(method_name.to_sym) || instance_methods.include?(method_name.to_s) fail 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 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] ||= {} if block_given? @source = block @block = self.class.generate_api_method(method_name, &block) end end def require_option(options, key) fail 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 : prepare_routes end def reset_routes! endpoints.map(&:reset_routes!) if endpoints @namespace = nil @routes = nil end def mount_in(route_set) if endpoints endpoints.each do |e| e.mount_in(route_set) end else @routes = nil routes.each do |route| methods = [route.route_method] if !namespace_inheritable(:do_not_route_head) && route.route_method == Grape::Http::Headers::GET methods << Grape::Http::Headers::HEAD end methods.each do |method| route_set.add_route(self, { path_info: route.route_compiled, request_method: method }, route_info: route) end end end 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_routes_path_params(path) path_params = {} # named parameters in the api path regex = Rack::Mount::RegexpWithNamedGroups.new(path) named_params = regex.named_captures.map { |nc| nc[0] } - %w(version format) named_params.each { |named_param| path_params[named_param] = '' } # route parameters declared via desc or appended to the api declaration route_params = options[:route_options][:params] path_params.merge! route_params if route_params path_params end def prepare_routes options[:method].map do |method| options[:path].map do |path| prepared_path = prepare_path(path) anchor = options[:route_options].fetch(:anchor, true) path = compile_path(prepared_path, anchor && !options[:app], prepare_routes_requirements) request_method = (method.to_s.upcase unless method == :any) Route.new(options[:route_options].clone.merge( prefix: namespace_inheritable(:root_prefix), version: namespace_inheritable(:version) ? namespace_inheritable(:version).join('|') : nil, namespace: namespace, method: request_method, path: prepared_path, params: prepare_routes_path_params(path), compiled: path, settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations) )) end end.flatten 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 compile_path(prepared_path, anchor = true, requirements = {}) endpoint_options = {} endpoint_options[:version] = /#{namespace_inheritable(:version).join('|')}/ if namespace_inheritable(:version) endpoint_options.merge!(requirements) Rack::Mount::Strexp.compile(prepared_path, endpoint_options, %w( / . ? ), anchor) end def call(env) dup.call!(env) end def call!(env) extend helpers env['api.endpoint'] = self if options[:app] options[:app].call(env) else builder = build_middleware builder.run ->(arg) { run(arg) } builder.call(env) end 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(env) ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do @env = env @header = {} @request = Grape::Request.new(env) @params = @request.params @headers = @request.headers cookies.read(@request) self.class.before_each.call(self) if self.class.before_each run_filters befores, :before run_filters before_validations, :before_validation # Retrieve validations from this namespace and all parent namespaces. validation_errors = [] # require 'pry-byebug'; binding.pry route_setting(:saved_validations).each do |validator| begin validator.validate!(params) rescue Grape::Exceptions::Validation => e validation_errors << e end end if validation_errors.any? fail Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header end run_filters after_validations, :after_validation response_object = @block ? @block.call(self) : nil run_filters afters, :after cookies.write(header) # The Body commonly is an Array of Strings, the application instance itself, or a File-like object. response_object = file || [body || response_object] [status, header, response_object] end end def build_middleware b = Rack::Builder.new b.use Rack::Head b.use Grape::Middleware::Error, format: namespace_inheritable(:format), content_types: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:content_types)), default_status: namespace_inheritable(:default_error_status), rescue_all: namespace_inheritable(:rescue_all), default_error_formatter: namespace_inheritable(:default_error_formatter), error_formatters: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:error_formatters)), rescue_options: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:rescue_options)) || {}, rescue_handlers: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:rescue_handlers)) || {}, base_only_rescue_handlers: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:base_only_rescue_handlers)) || {}, all_rescue_handler: namespace_inheritable(:all_rescue_handler) (namespace_stackable(:middleware) || []).each do |m| m = m.dup block = m.pop if m.last.is_a?(Proc) if block b.use(*m, &block) else b.use(*m) end end if namespace_inheritable(:version) b.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) end b.use Grape::Middleware::Formatter, format: namespace_inheritable(:format), default_format: namespace_inheritable(:default_format) || :txt, content_types: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:content_types)), formatters: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:formatters)), parsers: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:parsers)) b end def helpers mod = Module.new (namespace_stackable(:helpers) || []).each do |mod_to_include| mod.send :include, mod_to_include end mod end def run_filters(filters, type = :other) ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do (filters || []).each do |filter| instance_eval(&filter) end end 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 end end grape-0.13.0/lib/grape/version.rb0000644000004100000410000000011012563420522016570 0ustar www-datawww-datamodule Grape # The current version of Grape. VERSION = '0.13.0' end grape-0.13.0/lib/grape/validations/0000755000004100000410000000000012563420522017103 5ustar www-datawww-datagrape-0.13.0/lib/grape/validations/params_scope.rb0000644000004100000410000002400012563420522022100 0ustar www-datawww-datamodule Grape module Validations class ParamsScope attr_accessor :element, :parent 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 :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] @dependent_on = opts[:dependent_on] @declared_params = [] 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).respond_to?(:all?) && params(parameters).all?(&:blank?) return false if @dependent_on && params(parameters).try(:[], @dependent_on).blank? return true if parent.nil? parent.should_validate?(parameters) end # @return [String] the proper attribute name, with nesting considered. def full_name(name) case when nested? # Find our containing element's name, and append ours. "#{@parent.full_name(@element)}[#{name}]" when lateral? # Find the name of the element as if it was at the # same nesting level as our parent. @parent.full_name(name) else # We must be the root scope, so no prefix needed. name.to_s end 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) @declared_params.concat attrs 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] fail 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 fail Grape::Exceptions::MissingGroupTypeError.new if type.nil? fail Grape::Exceptions::UnsupportedGroupTypeError.new unless [Array, Hash].include?(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: Hash, dependent_on: options[:dependent_on], &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).concat @declared_params end end def validates(attrs, validations) doc_attrs = { required: validations.keys.include?(:presence) } # special case (type = coerce) validations[:coerce] = validations.delete(:type) if validations.key?(:type) coerce_type = validations[:coerce] 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) values = validations[:values] doc_attrs[:values] = values if values coerce_type = guess_coerce_type(coerce_type, values) # default value should be present in values array, if both exist and are not procs check_incompatible_option_values(values, default) # type should be compatible with values array, if both exist validate_value_coercion(coerce_type, values) 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) # Validate for presence before any other validators if validations.key?(:presence) && validations[:presence] validate('presence', validations[:presence], attrs, doc_attrs) validations.delete(:presence) end # Before we run the rest of the validators, lets handle # whatever coercion so that we are working with correctly # type casted values if validations.key? :coerce validate('coerce', validations[:coerce], attrs, doc_attrs) validations.delete(:coerce) end validations.each do |type, options| validate(type, options, attrs, doc_attrs) end end def guess_coerce_type(coerce_type, values) return coerce_type if !values || values.is_a?(Proc) return values.first.class if coerce_type == Array && (values.is_a?(Range) || !values.empty?) coerce_type end def check_incompatible_option_values(values, default) return unless values && default return if values.is_a?(Proc) || default.is_a?(Proc) return if values.include?(default) fail Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) end def validate(type, options, attrs, doc_attrs) validator_class = Validations.validators[type.to_s] if validator_class value = validator_class.new(attrs, options, doc_attrs[:required], self) @api.namespace_stackable(:validations, value) else fail Grape::Exceptions::UnknownValidator.new(type) end end def validate_value_coercion(coerce_type, values) return unless coerce_type && values return if values.is_a?(Proc) coerce_type = coerce_type.first if coerce_type.is_a?(Array) 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 if value_types.any? { |v| !v.is_a?(coerce_type) } fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) end end end end end grape-0.13.0/lib/grape/validations/validators/0000755000004100000410000000000012563420522021253 5ustar www-datawww-datagrape-0.13.0/lib/grape/validations/validators/default.rb0000644000004100000410000000125612563420522023230 0ustar www-datawww-datamodule Grape module Validations class DefaultValidator < Base def initialize(attrs, options, required, scope) @default = options super end def validate_param!(attr_name, params) params[attr_name] = @default.is_a?(Proc) ? @default.call : @default unless params.key?(attr_name) end def validate!(params) return unless @scope.should_validate?(params) attrs = AttributesIterator.new(self, @scope, params) attrs.each do |resource_params, attr_name| if resource_params[attr_name].nil? validate_param!(attr_name, resource_params) end end end end end end grape-0.13.0/lib/grape/validations/validators/values.rb0000644000004100000410000000134512563420522023102 0ustar www-datawww-datamodule Grape module Validations class ValuesValidator < Base def initialize(attrs, options, required, scope) @values = options super end def validate_param!(attr_name, params) return unless params[attr_name] || required_for_root_scope? values = @values.is_a?(Proc) ? @values.call : @values param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) unless param_array.all? { |param| values.include?(param) } fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :values end end private def required_for_root_scope? @required && @scope.root? end end end end grape-0.13.0/lib/grape/validations/validators/exactly_one_of.rb0000644000004100000410000000107612563420522024602 0ustar www-datawww-datamodule Grape module Validations require 'grape/validations/validators/mutual_exclusion' class ExactlyOneOfValidator < MutualExclusionValidator def validate!(params) super if scope_requires_params && none_of_restricted_params_is_present fail Grape::Exceptions::Validation, params: all_keys, message_key: :exactly_one end params end private def none_of_restricted_params_is_present scoped_params.any? { |resource_params| keys_in_common(resource_params).empty? } end end end end grape-0.13.0/lib/grape/validations/validators/all_or_none.rb0000644000004100000410000000112712563420522024070 0ustar www-datawww-datamodule Grape module Validations require 'grape/validations/validators/multiple_params_base' class AllOrNoneOfValidator < MultipleParamsBase def validate!(params) super if scope_requires_params && only_subset_present fail Grape::Exceptions::Validation, params: all_keys, message_key: :all_or_none end params end private def only_subset_present scoped_params.any? { |resource_params| keys_in_common(resource_params).length > 0 && keys_in_common(resource_params).length < attrs.length } end end end end grape-0.13.0/lib/grape/validations/validators/coerce.rb0000644000004100000410000000470312563420522023044 0ustar www-datawww-datamodule Grape class API Boolean = Virtus::Attribute::Boolean # rubocop:disable ConstantName end module Validations class CoerceValidator < Base def validate_param!(attr_name, params) fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :coerce unless params.is_a? Hash new_value = coerce_value(@option, params[attr_name]) if valid_type?(new_value) params[attr_name] = new_value else fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :coerce end end class InvalidValue; end private def _valid_array_type?(type, values) values.all? do |val| _valid_single_type?(type, val) end end def _valid_single_type?(klass, val) # allow nil, to ignore when a parameter is absent return true if val.nil? if klass == Virtus::Attribute::Boolean val.is_a?(TrueClass) || val.is_a?(FalseClass) || (val.is_a?(String) && val.empty?) elsif klass == Rack::Multipart::UploadedFile val.is_a?(Hashie::Mash) && val.key?(:tempfile) elsif [DateTime, Date, Numeric].any? { |vclass| vclass >= klass } return true if val.is_a?(String) && val.empty? val.is_a?(klass) else val.is_a?(klass) end end def valid_type?(val) if val.instance_of?(InvalidValue) false elsif @option.is_a?(Array) || @option.is_a?(Set) _valid_array_type?(@option.first, val) else _valid_single_type?(@option, val) end end def coerce_value(type, val) # Don't coerce things other than nil to Arrays or Hashes return val || [] if type == Array return val || Set.new if type == Set return val || {} if type == Hash # To support custom types that Virtus can't easily coerce, pass in an # explicit coercer. Custom types must implement a `parse` class method. converter_options = {} if ParameterTypes.custom_type?(type) converter_options[:coercer] = type.method(:parse) end converter = Virtus::Attribute.build(type, converter_options) converter.coerce(val) # not the prettiest but some invalid coercion can currently trigger # errors in Virtus (see coerce_spec.rb:75) rescue InvalidValue.new end end end end grape-0.13.0/lib/grape/validations/validators/multiple_params_base.rb0000644000004100000410000000110012563420522025760 0ustar www-datawww-datamodule Grape module Validations class MultipleParamsBase < Base attr_reader :scoped_params def validate!(params) @scoped_params = [@scope.params(params)].flatten params end private def scope_requires_params @scope.required? || scoped_params.any?(&:any?) end def keys_in_common(resource_params) return [] unless resource_params.is_a?(Hash) (all_keys & resource_params.stringify_keys.keys).map(&:to_s) end def all_keys attrs.map(&:to_s) end end end end grape-0.13.0/lib/grape/validations/validators/presence.rb0000644000004100000410000000066412563420522023412 0ustar www-datawww-datamodule Grape module Validations class PresenceValidator < Base def validate!(params) return unless @scope.should_validate?(params) super end def validate_param!(attr_name, params) unless params.respond_to?(:key?) && params.key?(attr_name) fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :presence end end end end end grape-0.13.0/lib/grape/validations/validators/base.rb0000644000004100000410000000203012563420522022505 0ustar www-datawww-datamodule Grape module Validations class Base attr_reader :attrs def initialize(attrs, options, required, scope) @attrs = Array(attrs) @option = options @required = required @scope = scope end def validate!(params) attributes = AttributesIterator.new(self, @scope, params) attributes.each do |resource_params, attr_name| if @required || (resource_params.respond_to?(:key?) && resource_params.key?(attr_name)) validate_param!(attr_name, resource_params) end end end def self.convert_to_short_name(klass) ret = klass.name.gsub(/::/, '/') .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') .gsub(/([a-z\d])([A-Z])/, '\1_\2') .tr('-', '_') .downcase File.basename(ret, '_validator') end def self.inherited(klass) short_name = convert_to_short_name(klass) Validations.register_validator(short_name, klass) end end end end grape-0.13.0/lib/grape/validations/validators/at_least_one_of.rb0000644000004100000410000000106312563420522024721 0ustar www-datawww-datamodule Grape module Validations require 'grape/validations/validators/multiple_params_base' class AtLeastOneOfValidator < MultipleParamsBase def validate!(params) super if scope_requires_params && no_exclusive_params_are_present fail Grape::Exceptions::Validation, params: all_keys, message_key: :at_least_one end params end private def no_exclusive_params_are_present scoped_params.any? { |resource_params| keys_in_common(resource_params).empty? } end end end end grape-0.13.0/lib/grape/validations/validators/allow_blank.rb0000644000004100000410000000204612563420522024067 0ustar www-datawww-datamodule Grape module Validations class AllowBlankValidator < Base def validate_param!(attr_name, params) return if @option || !params.is_a?(Hash) value = params[attr_name] value = value.strip if value.respond_to?(:strip) key_exists = params.key?(attr_name) if @scope.root? # root scope. validate if it's a required param. if it's optional, validate only if key exists in hash should_validate = @required || key_exists else # nested scope should_validate = # required param, and scope contains some values (if scoping element contains no values, treat as blank) (@required && params.present?) || # optional param but key inside scoping element exists (!@required && params.key?(attr_name)) end return unless should_validate unless value == false || value.present? fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :blank end end end end end grape-0.13.0/lib/grape/validations/validators/mutual_exclusion.rb0000644000004100000410000000131512563420522025200 0ustar www-datawww-datamodule Grape module Validations require 'grape/validations/validators/multiple_params_base' class MutualExclusionValidator < MultipleParamsBase attr_reader :processing_keys_in_common def validate!(params) super if two_or_more_exclusive_params_are_present fail Grape::Exceptions::Validation, params: processing_keys_in_common, message_key: :mutual_exclusion end params end private def two_or_more_exclusive_params_are_present scoped_params.any? do |resource_params| @processing_keys_in_common = keys_in_common(resource_params) @processing_keys_in_common.length > 1 end end end end end grape-0.13.0/lib/grape/validations/validators/regexp.rb0000644000004100000410000000056212563420522023075 0ustar www-datawww-datamodule Grape module Validations class RegexpValidator < Base def validate_param!(attr_name, params) if params.key?(attr_name) && !params[attr_name].nil? && !(params[attr_name].to_s =~ @option) fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :regexp end end end end end grape-0.13.0/lib/grape/validations/attributes_iterator.rb0000644000004100000410000000073312563420522023532 0ustar www-datawww-datamodule Grape module Validations class AttributesIterator include Enumerable def initialize(validator, scope, params) @attrs = validator.attrs @params = scope.params(params) @params = (@params.is_a?(Array) ? @params : [@params]) end def each @params.each do |resource_params| @attrs.each do |attr_name| yield resource_params, attr_name end end end end end end grape-0.13.0/lib/grape/dsl/0000755000004100000410000000000012563420522015350 5ustar www-datawww-datagrape-0.13.0/lib/grape/dsl/inside_route.rb0000644000004100000410000002430212563420522020367 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module InsideRoute extend ActiveSupport::Concern include Grape::DSL::Settings # A filtering method that will return a hash # consisting only of keys that have been declared by a # `params` statement against the current/target endpoint or parent # namespaces. # # @param params [Hash] The initial hash to filter. Usually this will just be `params` # @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces` # options. `:include_parent_namespaces` defaults to true, hence must be set to false if # you want only to return params declared against the current/target endpoint. def declared(params, options = {}, declared_params = nil) options[:include_missing] = true unless options.key?(:include_missing) options[:include_parent_namespaces] = true unless options.key?(:include_parent_namespaces) if declared_params.nil? declared_params = (!options[:include_parent_namespaces] ? route_setting(:declared_params) : (route_setting(:saved_declared_params) || [])).flatten(1) || [] end unless declared_params fail ArgumentError, 'Tried to filter for declared parameters but none exist.' end if params.is_a? Array params.map do |param| declared(param || {}, options, declared_params) end else declared_params.inject(Hashie::Mash.new) do |hash, key| key = { key => nil } unless key.is_a? Hash key.each_pair do |parent, children| output_key = options[:stringify] ? parent.to_s : parent.to_sym next unless options[:include_missing] || params.key?(parent) hash[output_key] = if children children_params = params[parent] || (children.is_a?(Array) ? [] : {}) declared(children_params, options, Array(children)) else params[parent] end end hash end end end # The API version as specified in the URL. def version env['api.version'] end # End the request and display an error to the # end user with the specified message. # # @param message [String] The message to display. # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set. def error!(message, status = nil, headers = nil) self.status(status || namespace_inheritable(:default_error_status)) throw :error, message: message, status: self.status, headers: headers end # Redirect to a new url. # # @param url [String] The url to be redirect. # @param options [Hash] The options used when redirect. # :permanent, default false. def redirect(url, options = {}) merged_options = { permanent: false }.merge(options) if merged_options[:permanent] status 301 else if env[Grape::Http::Headers::HTTP_VERSION] == 'HTTP/1.1' && request.request_method.to_s.upcase != Grape::Http::Headers::GET status 303 else status 302 end end header 'Location', url body '' end # Set or retrieve the HTTP status code. # # @param status [Integer] The HTTP Status Code to return for this request. def status(status = nil) case status when Symbol if Rack::Utils::SYMBOL_TO_STATUS_CODE.keys.include?(status) @status = Rack::Utils.status_code(status) else fail ArgumentError, "Status code :#{status} is invalid." end when Fixnum @status = status when nil return @status if @status case request.request_method.to_s.upcase when Grape::Http::Headers::POST 201 else 200 end else fail ArgumentError, 'Status code must be Fixnum or Symbol.' end end # Set an individual header or retrieve # all headers that have been set. def header(key = nil, val = nil) if key val ? @header[key.to_s] = val : @header.delete(key.to_s) else @header end end # Set response content-type def content_type(val = nil) if val header(Grape::Http::Headers::CONTENT_TYPE, val) else header[Grape::Http::Headers::CONTENT_TYPE] end end # Set or get a cookie # # @example # cookies[:mycookie] = 'mycookie val' # cookies['mycookie-string'] = 'mycookie string val' # cookies[:more] = { value: '123', expires: Time.at(0) } # cookies.delete :more # def cookies @cookies ||= Cookies.new end # Allows you to define the response body as something other than the # return value. # # @example # get '/body' do # body "Body" # "Not the Body" # end # # GET /body # => "Body" def body(value = nil) if value @body = value elsif value == false @body = '' status 204 else @body end end # Allows you to define the response as a file-like object. # # @example # get '/file' do # file FileStreamer.new(...) # end # # GET /file # => "contents of file" def file(value = nil) if value @file = Grape::Util::FileResponse.new(value) else @file end end # Allows you to define the response as a streamable object. # # If Content-Length and Transfer-Encoding are blank (among other conditions), # Rack assumes this response can be streamed in chunks. # # @example # get '/stream' do # stream FileStreamer.new(...) # end # # GET /stream # => "chunked contents of file" # # See: # * https://github.com/rack/rack/blob/99293fa13d86cd48021630fcc4bd5acc9de5bdc3/lib/rack/chunked.rb # * https://github.com/rack/rack/blob/99293fa13d86cd48021630fcc4bd5acc9de5bdc3/lib/rack/etag.rb def stream(value = nil) header 'Content-Length', nil header 'Transfer-Encoding', nil header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front) file(value) end # Allows you to make use of Grape Entities by setting # the response body to the serializable hash of the # entity provided in the `:with` option. This has the # added benefit of automatically passing along environment # and version information to the serialization, making it # very easy to do conditional exposures. See Entity docs # for more info. # # @example # # get '/users/:id' do # present User.find(params[:id]), # with: API::Entities::User, # admin: current_user.admin? # end def present(*args) options = args.count > 1 ? args.extract_options! : {} key, object = if args.count == 2 && args.first.is_a?(Symbol) args else [nil, args.first] end entity_class = entity_class_for_obj(object, options) root = options.delete(:root) representation = if entity_class entity_representation_for(entity_class, object, options) else object end representation = { root => representation } if root if key representation = (@body || {}).merge(key => representation) elsif entity_class.present? && @body fail ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?('merge') representation = @body.merge(representation) end body representation end # Returns route information for the current request. # # @example # # desc "Returns the route description." # get '/' do # route.route_description # end def route env['rack.routing_args'][:route_info] end # Attempt to locate the Entity class for a given object, if not given # explicitly. This is done by looking for the presence of Klass::Entity, # where Klass is the class of the `object` parameter, or one of its # ancestors. # @param object [Object] the object to locate the Entity class for # @param options [Hash] # @option options :with [Class] the explicit entity class to use # @return [Class] the located Entity class, or nil if none is found def entity_class_for_obj(object, options) entity_class = options.delete(:with) if entity_class.nil? # entity class not explicitely defined, auto-detect from relation#klass or first object in the collection object_class = if object.respond_to?(:klass) object.klass else object.respond_to?(:first) ? object.first.class : object.class end object_class.ancestors.each do |potential| entity_class ||= (Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:representations)) || {})[potential] end entity_class ||= object_class.const_get(:Entity) if object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent) end entity_class end # @return the representation of the given object as done through # the given entity_class. def entity_representation_for(entity_class, object, options) embeds = { env: env } embeds[:version] = env['api.version'] if env['api.version'] entity_class.represent(object, embeds.merge(options)) end end end end grape-0.13.0/lib/grape/dsl/request_response.rb0000644000004100000410000001375212563420522021313 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module RequestResponse extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods # Specify the default format for the API's serializers. # May be `:json` or `:txt` (default). def default_format(new_format = nil) namespace_inheritable(:default_format, new_format.nil? ? nil : new_format.to_sym) end # Specify the format for the API's serializers. # May be `:json`, `:xml`, `:txt`, etc. def format(new_format = nil) if new_format namespace_inheritable(:format, new_format.to_sym) # define the default error formatters namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter::Base.formatter_for(new_format, {})) # define a single mime type mime_type = content_types[new_format.to_sym] fail Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type namespace_stackable(:content_types, new_format.to_sym => mime_type) else namespace_inheritable(:format) end end # Specify a custom formatter for a content-type. def formatter(content_type, new_formatter) namespace_stackable(:formatters, content_type.to_sym => new_formatter) end # Specify a custom parser for a content-type. def parser(content_type, new_parser) namespace_stackable(:parsers, content_type.to_sym => new_parser) end # Specify a default error formatter. def default_error_formatter(new_formatter_name = nil) if new_formatter_name new_formatter = Grape::ErrorFormatter::Base.formatter_for(new_formatter_name, {}) namespace_inheritable(:default_error_formatter, new_formatter) else namespace_inheritable(:default_error_formatter) end end def error_formatter(format, options) if options.is_a?(Hash) && options.key?(:with) formatter = options[:with] else formatter = options end namespace_stackable(:error_formatters, format.to_sym => formatter) end # Specify additional content-types, e.g.: # content_type :xls, 'application/vnd.ms-excel' def content_type(key, val) namespace_stackable(:content_types, key.to_sym => val) end # All available content types. def content_types c_types = Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:content_types)) Grape::ContentTypes.content_types_for c_types end # Specify the default status code for errors. def default_error_status(new_status = nil) namespace_inheritable(:default_error_status, new_status) end # Allows you to rescue certain exceptions that occur to return # a grape error rather than raising all the way to the # server level. # # @example Rescue from custom exceptions # class ExampleAPI < Grape::API # class CustomError < StandardError; end # # rescue_from CustomError # end # # @overload rescue_from(*exception_classes, options = {}) # @param [Array] exception_classes A list of classes that you want to rescue, or # the symbol :all to rescue from all exceptions. # @param [Block] block Execution block to handle the given exception. # @param [Hash] options Options for the rescue usage. # @option options [Boolean] :backtrace Include a backtrace in the rescue response. # @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes # @param [Proc] handler Execution proc to handle the given exception as an # alternative to passing a block. def rescue_from(*args, &block) if args.last.is_a?(Proc) handler = args.pop elsif block_given? handler = block end options = args.last.is_a?(Hash) ? args.pop : {} handler ||= proc { options[:with] } if options.key?(:with) if args.include?(:all) namespace_inheritable(:rescue_all, true) namespace_inheritable :all_rescue_handler, handler else handler_type = case options[:rescue_subclasses] when nil, true :rescue_handlers else :base_only_rescue_handlers end namespace_stackable handler_type, Hash[args.map { |arg| [arg, handler] }] end namespace_stackable(:rescue_options, options) end # Allows you to specify a default representation entity for a # class. This allows you to map your models to their respective # entities once and then simply call `present` with the model. # # @example # class ExampleAPI < Grape::API # represent User, with: Entity::User # # get '/me' do # present current_user # with: Entity::User is assumed # end # end # # Note that Grape will automatically go up the class ancestry to # try to find a representing entity, so if you, for example, define # an entity to represent `Object` then all presented objects will # bubble up and utilize the entity provided on that `represent` call. # # @param model_class [Class] The model class that will be represented. # @option options [Class] :with The entity class that will represent the model. def represent(model_class, options) fail Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with] && options[:with].is_a?(Class) namespace_stackable(:representations, model_class => options[:with]) end end end end end grape-0.13.0/lib/grape/dsl/configuration.rb0000644000004100000410000001010412563420522020540 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module Configuration extend ActiveSupport::Concern module ClassMethods attr_writer :logger include Grape::DSL::Settings # Set or retrive the configured logger. If none was configured, this # method will create a new one, logging to stdout. # @param logger [Object] the new logger to use def logger(logger = nil) if logger global_setting(:logger, logger) else global_setting(:logger) || global_setting(:logger, Logger.new($stdout)) end end # Add a description to the next namespace or function. # @param description [String] descriptive string for this endpoint # or namespace # @param options [Hash] other properties you can set to describe the # endpoint or namespace. Optional. # @option options :detail [String] additional detail about this endpoint # @option options :params [Hash] param types and info. normally, you set # these via the `params` dsl method. # @option options :entity [Grape::Entity] the entity returned upon a # successful call to this action # @option options :http_codes [Array[Array]] possible HTTP codes this # endpoint may return, with their meanings, in a 2d array # @option options :named [String] a specific name to help find this route # @option options :headers [Hash] HTTP headers this method can accept # @yield a block yielding an instance context with methods mapping to # each of the above, except that :entity is also aliased as #success # and :http_codes is aliased as #failure. # # @example # # desc 'create a user' # post '/users' do # # ... # end # # desc 'find a user' do # detail 'locates the user from the given user ID' # failure [ [404, 'Couldn\'t find the given user' ] ] # success User::Entity # end # get '/user/:id' do # # ... # end # def desc(description, options = {}, &config_block) if block_given? config_class = Grape::DSL::Configuration.desc_container config_class.configure do description description end config_class.configure(&config_block) options = config_class.settings else options = options.merge(description: description) end namespace_setting :description, options route_setting :description, options end def description_field(field, value = nil) if value description = route_setting(:description) description ||= route_setting(:description, {}) description[field] = value else description = route_setting(:description) description[field] if description end end def unset_description_field(field) description = route_setting(:description) description.delete(field) if description end end module_function # Merge multiple layers of settings into one hash. def stacked_hash_to_hash(settings) return nil if settings.nil? || settings.blank? settings.each_with_object(ActiveSupport::OrderedHash.new) { |value, result| result.deep_merge!(value) } end # Returns an object which configures itself via an instance-context DSL. def desc_container Module.new do include Grape::Util::StrictHashConfiguration.module( :description, :detail, :params, :entity, :http_codes, :named, :headers ) def config_context.success(*args) entity(*args) end def config_context.failure(*args) http_codes(*args) end end end end end end grape-0.13.0/lib/grape/dsl/api.rb0000644000004100000410000000066312563420522016453 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module API extend ActiveSupport::Concern include Grape::Middleware::Auth::DSL include Grape::DSL::Validations include Grape::DSL::Callbacks include Grape::DSL::Configuration include Grape::DSL::Helpers include Grape::DSL::Middleware include Grape::DSL::RequestResponse include Grape::DSL::Routing end end end grape-0.13.0/lib/grape/dsl/helpers.rb0000644000004100000410000000440512563420522017342 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module Helpers extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods # Add helper methods that will be accessible from any # endpoint within this namespace (and child namespaces). # # When called without a block, all known helpers within this scope # are included. # # @param [Module] new_mod optional module of methods to include # @param [Block] block optional block of methods to include # # @example Define some helpers. # # class ExampleAPI < Grape::API # helpers do # def current_user # User.find_by_id(params[:token]) # end # end # end # def helpers(new_mod = nil, &block) if block_given? || new_mod mod = new_mod || Module.new if new_mod inject_api_helpers_to_mod(new_mod) if new_mod.is_a?(BaseHelper) end if block_given? inject_api_helpers_to_mod(mod) do mod.class_eval(&block) end end namespace_stackable(:helpers, mod) else mod = Module.new namespace_stackable(:helpers).each do |mod_to_include| mod.send :include, mod_to_include end change! mod end end protected def inject_api_helpers_to_mod(mod, &_block) mod.extend(BaseHelper) yield if block_given? mod.api_changed(self) end end # This module extends user defined helpers # to provide some API-specific functionality. module BaseHelper attr_accessor :api def params(name, &block) @named_params ||= {} @named_params[name] = block end def api_changed(new_api) @api = new_api process_named_params end protected def process_named_params if @named_params && @named_params.any? api.namespace_stackable(:named_params, @named_params) end end end end end end grape-0.13.0/lib/grape/dsl/middleware.rb0000644000004100000410000000155312563420522020016 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module Middleware extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods # Apply a custom middleware to the API. Applies # to the current namespace and any children, but # not parents. # # @param middleware_class [Class] The class of the middleware you'd like # to inject. def use(middleware_class, *args, &block) arr = [middleware_class, *args] arr << block if block_given? namespace_stackable(:middleware, arr) end # Retrieve an array of the middleware classes # and arguments that are currently applied to the # application. def middleware namespace_stackable(:middleware) || [] end end end end end grape-0.13.0/lib/grape/dsl/routing.rb0000644000004100000410000001456112563420522017373 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module Routing extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods attr_reader :endpoints, :routes, :route_set # Specify an API version. # # @example API with legacy support. # class MyAPI < Grape::API # version 'v2' # # get '/main' do # {some: 'data'} # end # # version 'v1' do # get '/main' do # {legacy: 'data'} # end # end # end # def version(*args, &block) if args.any? options = args.pop if args.last.is_a? Hash options ||= {} options = { using: :path }.merge(options) fail Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor) @versions = versions | args if block_given? within_namespace do namespace_inheritable(:version, args) namespace_inheritable(:version_options, options) instance_eval(&block) end else namespace_inheritable(:version, args) namespace_inheritable(:version_options, options) end # reset_validations! end @versions.last unless @versions.nil? end # Define a root URL prefix for your entire API. def prefix(prefix = nil) namespace_inheritable(:root_prefix, prefix) end # Do not route HEAD requests to GET requests automatically. def do_not_route_head! namespace_inheritable(:do_not_route_head, true) end # Do not automatically route OPTIONS. def do_not_route_options! namespace_inheritable(:do_not_route_options, true) end def mount(mounts) mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) mounts.each_pair do |app, path| in_setting = inheritable_setting if app.respond_to?(:inheritable_setting, true) mount_path = Rack::Mount::Utils.normalize_path(path) app.top_level_setting.namespace_stackable[:mount_path] = mount_path app.inherit_settings(inheritable_setting) in_setting = app.top_level_setting # app.regenerate_endpoints(in_setting) app.change! change! end endpoints << Grape::Endpoint.new( in_setting, method: :any, path: path, app: app, for: self ) end end # Defines a route that will be recognized # by the Grape API. # # @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted. # @param paths [String] One or more strings representing the URL segment(s) for this route. # # @example Defining a basic route. # class MyAPI < Grape::API # route(:any, '/hello') do # {hello: 'world'} # end # end def route(methods, paths = ['/'], route_options = {}, &block) endpoint_options = { method: methods, path: paths, for: self, route_options: ({ params: Grape::DSL::Configuration.stacked_hash_to_hash(namespace_stackable(:params)) || {} }).deep_merge(route_setting(:description) || {}).deep_merge(route_options || {}) } new_endpoint = Grape::Endpoint.new(inheritable_setting, endpoint_options, &block) endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) } route_end reset_validations! end %w(get post put head delete options patch).each do |meth| define_method meth do |*args, &block| options = args.extract_options! paths = args.first || ['/'] route(meth.upcase, paths, options, &block) end end # Declare a "namespace", which prefixes all subordinate routes with its # name. Any endpoints within a namespace, or group, resource, segment, # etc., will share their parent context as well as any configuration # done in the namespace context. # # @example # # namespace :foo do # get 'bar' do # # defines the endpoint: GET /foo/bar # end # end def namespace(space = nil, options = {}, &block) if space || block_given? within_namespace do previous_namespace_description = @namespace_description @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {}) nest(block) do if space namespace_stackable(:namespace, Namespace.new(space, options)) end end @namespace_description = previous_namespace_description end else Namespace.joined_space_path(namespace_stackable(:namespace)) end end alias_method :group, :namespace alias_method :resource, :namespace alias_method :resources, :namespace alias_method :segment, :namespace # An array of API routes. def routes @routes ||= prepare_routes end # Remove all defined routes. def reset_routes! @routes = nil end # Thie method allows you to quickly define a parameter route segment # in your API. # # @param param [Symbol] The name of the parameter you wish to declare. # @option options [Regexp] You may supply a regular expression that the declared parameter must meet. def route_param(param, options = {}, &block) options = options.dup options[:requirements] = { param.to_sym => options[:requirements] } if options[:requirements].is_a?(Regexp) namespace(":#{param}", options, &block) end # @return array of defined versions def versions @versions ||= [] end end end end end grape-0.13.0/lib/grape/dsl/parameters.rb0000644000004100000410000001577012563420522020052 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL # Defines DSL methods, meant to be applied to a ParamsScope, which define # and describe the parameters accepted by an endpoint, or all endpoints # within a namespace. module Parameters extend ActiveSupport::Concern # Include reusable params rules among current. # You can define reusable params with helpers method. # # @example # # class API < Grape::API # helpers do # params :pagination do # optional :page, type: Integer # optional :per_page, type: Integer # end # end # # desc "Get collection" # params do # use :pagination # end # get do # Collection.page(params[:page]).per(params[:per_page]) # end # end def use(*names) named_params = Grape::DSL::Configuration.stacked_hash_to_hash(@api.namespace_stackable(:named_params)) || {} options = names.last.is_a?(Hash) ? names.pop : {} names.each do |name| params_block = named_params.fetch(name) do fail "Params :#{name} not found!" end instance_exec(options, ¶ms_block) end end alias_method :use_scope, :use alias_method :includes, :use # Require one or more parameters for the current endpoint. # # @param attrs list of parameter names, or, if :using is # passed as an option, which keys to include (:all or :none) from # the :using hash. The last key can be a hash, which specifies # options for the parameters # @option attrs :type [Class] the type to coerce this parameter to before # passing it to the endpoint. See Grape::ParameterTypes for supported # types, or use a class that defines `::parse` as a custom type # @option attrs :desc [String] description to document this parameter # @option attrs :default [Object] default value, if parameter is optional # @option attrs :values [Array] permissable values for this field. If any # other value is given, it will be handled as a validation error # @option attrs :using [Hash[Symbol => Hash]] a hash defining keys and # options, like that returned by Grape::Entity#documentation. The value # of each key is an options hash accepting the same parameters # @option attrs :except [Array[Symbol]] a list of keys to exclude from # the :using Hash. The meaning of this depends on if :all or :none was # passed; :all + :except will make the :except fields optional, whereas # :none + :except will make the :except fields required # # @example # # params do # # Basic usage: require a parameter of a certain type # requires :user_id, type: Integer # # # You don't need to specify type; String is default # requires :foo # # # Multiple params can be specified at once if they share # # the same options. # requires :x, :y, :z, type: Date # # # Nested parameters can be handled as hashes. You must # # pass in a block, within which you can use any of the # # parameters DSL methods. # requires :user, type: Hash do # requires :name, type: String # end # end def requires(*attrs, &block) orig_attrs = attrs.clone opts = attrs.last.is_a?(Hash) ? attrs.pop.clone : {} opts[:presence] = true if opts[:using] require_required_and_optional_fields(attrs.first, opts) else validate_attributes(attrs, opts, &block) block_given? ? new_scope(orig_attrs, &block) : push_declared_params(attrs) end end # Allow, but don't require, one or more parameters for the current # endpoint. # @param (see #requires) # @option (see #requires) def optional(*attrs, &block) orig_attrs = attrs.clone opts = attrs.last.is_a?(Hash) ? attrs.pop.clone : {} type = opts[:type] # check type for optional parameter group if attrs && block_given? fail Grape::Exceptions::MissingGroupTypeError.new if type.nil? fail Grape::Exceptions::UnsupportedGroupTypeError.new unless [Array, Hash].include?(type) end if opts[:using] require_optional_fields(attrs.first, opts) else validate_attributes(attrs, opts, &block) block_given? ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs) end end # Disallow the given parameters to be present in the same request. # @param attrs [*Symbol] parameters to validate def mutually_exclusive(*attrs) validates(attrs, mutual_exclusion: true) end # Require exactly one of the given parameters to be present. # @param (see #mutually_exclusive) def exactly_one_of(*attrs) validates(attrs, exactly_one_of: true) end # Require at least one of the given parameters to be present. # @param (see #mutually_exclusive) def at_least_one_of(*attrs) validates(attrs, at_least_one_of: true) end # Require that either all given params are present, or none are. # @param (see #mutually_exclusive) def all_or_none_of(*attrs) validates(attrs, all_or_none_of: true) end # Define a block of validations which should be applied if and only if # the given parameter is present. The parameters are not nested. # @param attr [Symbol] the parameter which, if present, triggers the # validations # @throws Grape::Exceptions::UnknownParameter if `attr` has not been # defined in this scope yet # @yield a parameter definition DSL def given(attr, &block) fail Grape::Exceptions::UnknownParameter.new(attr) unless declared_param?(attr) new_lateral_scope(dependent_on: attr, &block) end # Test for whether a certain parameter has been defined in this params # block yet. # @returns [Boolean] whether the parameter has been defined def declared_param?(param) # @declared_params also includes hashes of options and such, but those # won't be flattened out. @declared_params.flatten.include?(param) end alias_method :group, :requires # @param params [Hash] initial hash of parameters # @return hash of parameters relevant for the current scope # @api private def params(params) params = @parent.params(params) if @parent if @element if params.is_a?(Array) params = params.flat_map { |el| el[@element] || {} } elsif params.is_a?(Hash) params = params[@element] || {} else params = {} end end params end end end end grape-0.13.0/lib/grape/dsl/callbacks.rb0000644000004100000410000000256312563420522017622 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL # Blocks can be executed before or after every API call, using `before`, `after`, # `before_validation` and `after_validation`. # # Before and after callbacks execute in the following order: # # 1. `before` # 2. `before_validation` # 3. _validations_ # 4. `after_validation` # 5. _the API call_ # 6. `after` # # Steps 4, 5 and 6 only happen if validation succeeds. module Callbacks extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods # Execute the given block before validation, coercion, or any endpoint # code is executed. def before(&block) namespace_stackable(:befores, block) end # Execute the given block after `before`, but prior to validation or # coercion. def before_validation(&block) namespace_stackable(:before_validations, block) end # Execute the given block after validations and coercions, but before # any endpoint code. def after_validation(&block) namespace_stackable(:after_validations, block) end # Execute the given block after the endpoint code has run. def after(&block) namespace_stackable(:afters, block) end end end end end grape-0.13.0/lib/grape/dsl/validations.rb0000644000004100000410000000222612563420522020214 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL module Validations extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods # Clears all defined parameters and validations. def reset_validations! unset_namespace_stackable :declared_params unset_namespace_stackable :validations unset_namespace_stackable :params unset_description_field :params end # Opens a root-level ParamsScope, defining parameter coercions and # validations for the endpoint. # @yield instance context of the new scope def params(&block) Grape::Validations::ParamsScope.new(api: self, type: Hash, &block) end def document_attribute(names, opts) setting = description_field(:params) setting ||= description_field(:params, {}) Array(names).each do |name| setting[name[:full_name].to_s] ||= {} setting[name[:full_name].to_s].merge!(opts) namespace_stackable(:params, name[:full_name].to_s => opts) end end end end end end grape-0.13.0/lib/grape/dsl/settings.rb0000644000004100000410000001031712563420522017537 0ustar www-datawww-datarequire 'active_support/concern' module Grape module DSL # Keeps track of settings (impemented as key-value pairs, grouped by # types), in two contexts: top-level settings which apply globally no # matter where they're defined, and inheritable settings which apply only # in the current scope and scopes nested under it. module Settings extend ActiveSupport::Concern attr_accessor :inheritable_setting, :top_level_setting # Fetch our top-level settings, which apply to all endpoints in the API. def top_level_setting @top_level_setting ||= Grape::Util::InheritableSetting.new end # Fetch our current inheritable settings, which are inherited by # nested scopes but not shared across siblings. def inheritable_setting @inheritable_setting ||= Grape::Util::InheritableSetting.new.tap { |new_settings| new_settings.inherit_from top_level_setting } end # @param type [Symbol] # @param key [Symbol] def unset(type, key) setting = inheritable_setting.send(type) setting.delete key end # @param type [Symbol] # @param key [Symbol] # @param value [Object] will be stored if the value is currently empty # @return either the old value, if it wasn't nil, or the given value def get_or_set(type, key, value) setting = inheritable_setting.send(type) if value.nil? setting[key] else setting[key] = value end end # @param key [Symbol] # @param value [Object] # @return (see #get_or_set) def global_setting(key, value = nil) get_or_set :global, key, value end # @param key [Symbol] def unset_global_setting(key) unset :global, key end # (see #global_setting) def route_setting(key, value = nil) get_or_set :route, key, value end # (see #unset_global_setting) def unset_route_setting(key) unset :route, key end # (see #global_setting) def namespace_setting(key, value = nil) get_or_set :namespace, key, value end # (see #unset_global_setting) def unset_namespace_setting(key) unset :namespace_setting, key end # (see #global_setting) def namespace_inheritable(key, value = nil) get_or_set :namespace_inheritable, key, value end # (see #unset_global_setting) def unset_namespace_inheritable(key) unset :namespace_inheritable, key end # @param key [Symbol] def namespace_inheritable_to_nil(key) inheritable_setting.namespace_inheritable[key] = nil end # (see #global_setting) def namespace_stackable(key, value = nil) get_or_set :namespace_stackable, key, value end # (see #unset_global_setting) def unset_namespace_stackable(key) unset :namespace_stackable, key end # (see #global_setting) def api_class_setting(key, value = nil) get_or_set :api_class, key, value end # (see #unset_global_setting) def unset_api_class_setting(key) unset :api_class_setting, key end # Fork our inheritable settings to a new instance, copied from our # parent's, but separate so we won't modify it. Every call to this # method should have an answering call to #namespace_end. def namespace_start @inheritable_setting = Grape::Util::InheritableSetting.new.tap { |new_settings| new_settings.inherit_from inheritable_setting } end # Set the inheritable settings pointer back up by one level. def namespace_end route_end @inheritable_setting = inheritable_setting.parent end # Stop defining settings for the current route and clear them for the # next, within a namespace. def route_end inheritable_setting.route_end end # Execute the block within a context where our inheritable settings are forked # to a new copy (see #namespace_start). def within_namespace(&_block) namespace_start result = yield if block_given? namespace_end reset_validations! result end end end end grape-0.13.0/lib/grape/presenters/0000755000004100000410000000000012563420522016760 5ustar www-datawww-datagrape-0.13.0/lib/grape/presenters/presenter.rb0000644000004100000410000000022012563420522021306 0ustar www-datawww-datamodule Grape module Presenters class Presenter def self.represent(object, _options = {}) object end end end end grape-0.13.0/lib/grape/http/0000755000004100000410000000000012563420522015545 5ustar www-datawww-datagrape-0.13.0/lib/grape/http/headers.rb0000644000004100000410000000143312563420522017506 0ustar www-datawww-datamodule Grape module Http module Headers # https://github.com/rack/rack/blob/master/lib/rack.rb HTTP_VERSION = 'HTTP_VERSION'.freeze PATH_INFO = 'PATH_INFO'.freeze QUERY_STRING = 'QUERY_STRING'.freeze CONTENT_TYPE = 'Content-Type'.freeze GET = 'GET'.freeze POST = 'POST'.freeze PUT = 'PUT'.freeze PATCH = 'PATCH'.freeze DELETE = 'DELETE'.freeze HEAD = 'HEAD'.freeze OPTIONS = 'OPTIONS'.freeze HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'.freeze X_CASCADE = 'X-Cascade'.freeze HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze FORMAT = 'format'.freeze end end end grape-0.13.0/lib/grape/http/request.rb0000644000004100000410000000145712563420522017571 0ustar www-datawww-datamodule Grape class Request < Rack::Request ROUTING_ARGS = 'rack.routing_args' HTTP_PREFIX = 'HTTP_' UNDERSCORE = '_' MINUS = '-' def params @params ||= begin params = Hashie::Mash.new(super) if env[ROUTING_ARGS] args = env[ROUTING_ARGS].dup # preserve version from query string parameters args.delete(:version) args.delete(:route_info) params.deep_merge!(args) end params end end def headers @headers ||= env.dup.inject({}) do |h, (k, v)| if k.to_s.start_with? HTTP_PREFIX k = k[5..-1] k.tr!(UNDERSCORE, MINUS) k.downcase! k.gsub!(/^.|[-_\s]./, &:upcase!) h[k] = v end h end end end end grape-0.13.0/lib/grape/validations.rb0000644000004100000410000000077012563420522017434 0ustar www-datawww-datamodule Grape # Registry to store and locate known Validators. module Validations class << self attr_accessor :validators end self.validators = {} # Register a new validator, so it can be used to validate parameters. # @param short_name [String] all lower-case, no spaces # @param klass [Class] the validator class. Should inherit from # Validations::Base. def self.register_validator(short_name, klass) validators[short_name] = klass end end end grape-0.13.0/lib/grape/util/0000755000004100000410000000000012563420522015543 5ustar www-datawww-datagrape-0.13.0/lib/grape/util/strict_hash_configuration.rb0000644000004100000410000000517212563420522023337 0ustar www-datawww-datamodule Grape module Util module StrictHashConfiguration extend ActiveSupport::Concern module DSL extend ActiveSupport::Concern module ClassMethods def settings config_context.to_hash end def configure(&block) config_context.instance_exec(&block) end end end class SettingsContainer def initialize @settings = {} @contexts = {} end def to_hash @settings.to_hash end end def self.config_class(*args) new_config_class = Class.new(SettingsContainer) args.each do |setting_name| if setting_name.respond_to? :values nested_settings_methods(setting_name, new_config_class) else simple_settings_methods(setting_name, new_config_class) end end new_config_class end def self.simple_settings_methods(setting_name, new_config_class) setting_name_sym = setting_name.to_sym new_config_class.class_eval do define_method setting_name do |new_value| @settings[setting_name_sym] = new_value end end end def self.nested_settings_methods(setting_name, new_config_class) new_config_class.class_eval do setting_name.each_pair do |key, value| define_method "#{key}_context" do @contexts[key] ||= Grape::Util::StrictHashConfiguration.config_class(*value).new end define_method key do |&block| send("#{key}_context").instance_exec(&block) end end define_method 'to_hash' do merge_hash = setting_name.keys.each_with_object({}) { |k, hash| hash[k] = send("#{k}_context").to_hash } @settings.to_hash.merge( merge_hash ) end end end def self.module(*args) new_module = Module.new do extend ActiveSupport::Concern include DSL end new_module.tap do |mod| class_mod = create_class_mod(args) mod.const_set(:ClassMethods, class_mod) end end def self.create_class_mod(args) new_module = Module.new do def config_context @config_context ||= config_class.new end end new_module.tap do |class_mod| new_config_class = config_class(*args) class_mod.send(:define_method, :config_class) do @config_context ||= new_config_class end end end end end end grape-0.13.0/lib/grape/util/stackable_values.rb0000644000004100000410000000234412563420522021403 0ustar www-datawww-datamodule Grape module Util class StackableValues attr_accessor :inherited_values attr_reader :new_values attr_reader :froozen_values def initialize(inherited_values = {}) @inherited_values = inherited_values @new_values = {} @froozen_values = {} end def [](name) return @froozen_values[name] if @froozen_values.key? name value = [@inherited_values[name], @new_values[name]] value.compact! value.flatten!(1) value end def []=(name, value) fail if @froozen_values.key? name @new_values[name] ||= [] @new_values[name].push value end def delete(key) new_values.delete key end attr_writer :new_values def keys (@new_values.keys + @inherited_values.keys).sort.uniq end def to_hash keys.each_with_object({}) do |key, result| result[key] = self[key] end end def freeze_value(key) @froozen_values[key] = self[key].freeze end def initialize_copy(other) super self.inherited_values = other.inherited_values self.new_values = other.new_values.dup end end end end grape-0.13.0/lib/grape/util/content_types.rb0000644000004100000410000000143212563420522020766 0ustar www-datawww-datamodule Grape module ContentTypes # Content types are listed in order of preference. CONTENT_TYPES = ActiveSupport::OrderedHash[ :xml, 'application/xml', :serializable_hash, 'application/json', :json, 'application/json', :binary, 'application/octet-stream', :txt, 'text/plain' ] def self.content_types_for_settings(settings) return nil if settings.nil? || settings.blank? settings.each_with_object(ActiveSupport::OrderedHash.new) { |value, result| result.merge!(value) } end def self.content_types_for(from_settings) if from_settings.present? from_settings else Grape::ContentTypes::CONTENT_TYPES end end end end grape-0.13.0/lib/grape/util/inheritable_values.rb0000644000004100000410000000162312563420522021737 0ustar www-datawww-datamodule Grape module Util class InheritableValues attr_accessor :inherited_values attr_accessor :new_values def initialize(inherited_values = {}) self.inherited_values = inherited_values self.new_values = {} end def [](name) values[name] end def []=(name, value) new_values[name] = value end def delete(key) new_values.delete key end def merge(new_hash) values.merge(new_hash) end def keys (new_values.keys + inherited_values.keys).sort.uniq end def to_hash values.clone end def initialize_copy(other) super self.inherited_values = other.inherited_values self.new_values = other.new_values.dup end protected def values @inherited_values.merge(@new_values) end end end end grape-0.13.0/lib/grape/util/file_response.rb0000644000004100000410000000070612563420522020730 0ustar www-datawww-datamodule Grape module Util # A simple class used to identify responses which represent files and do not # need to be formatted or pre-read by Rack::Response class FileResponse attr_reader :file # @param file [Object] def initialize(file) @file = file end # Equality provided mostly for tests. # # @return [Boolean] def ==(other) file == other.file end end end end grape-0.13.0/lib/grape/util/inheritable_setting.rb0000644000004100000410000000612612563420522022120 0ustar www-datawww-datamodule Grape module Util # A branchable, inheritable settings object which can store both stackable # and inheritable values (see InheritableValues and StackableValues). class InheritableSetting attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable attr_accessor :parent, :point_in_time_copies # Retrieve global settings. def self.global @global ||= {} end # Clear all global settings. # @api private # @note only for testing def self.reset_global! @global = {} end # Instantiate a new settings instance, with blank values. The fresh # instance can then be set to inherit from an existing instance (see # #inherit_from). def initialize self.route = {} self.api_class = {} self.namespace = InheritableValues.new # only inheritable from a parent when # used with a mount, or should every API::Class be a separate namespace by default? self.namespace_inheritable = InheritableValues.new self.namespace_stackable = StackableValues.new self.point_in_time_copies = [] self.parent = nil end # Return the class-level global properties. def global self.class.global end # Set our inherited values to the given parent's current values. Also, # update the inherited values on any settings instances which were forked # from us. # @param parent [InheritableSetting] def inherit_from(parent) return if parent.nil? self.parent = parent namespace_inheritable.inherited_values = parent.namespace_inheritable namespace_stackable.inherited_values = parent.namespace_stackable self.route = parent.route.merge(route) point_in_time_copies.map { |cloned_one| cloned_one.inherit_from parent } end # Create a point-in-time copy of this settings instance, with clones of # all our values. Note that, should this instance's parent be set or # changed via #inherit_from, it will copy that inheritence to any copies # which were made. def point_in_time_copy self.class.new.tap do |new_setting| point_in_time_copies << new_setting new_setting.point_in_time_copies = [] new_setting.namespace = namespace.clone new_setting.namespace_inheritable = namespace_inheritable.clone new_setting.namespace_stackable = namespace_stackable.clone new_setting.route = route.clone new_setting.api_class = api_class new_setting.inherit_from(parent) end end # Resets the instance store of per-route settings. # @api private def route_end @route = {} end # Return a serializable hash of our values. def to_hash { global: global.clone, route: route.clone, namespace: namespace.to_hash, namespace_inheritable: namespace_inheritable.to_hash, namespace_stackable: namespace_stackable.to_hash } end end end end grape-0.13.0/lib/grape/util/parameter_types.rb0000644000004100000410000000275212563420522021302 0ustar www-datawww-datamodule Grape module ParameterTypes # 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 ] # Types representing data structures. STRUCTURES = [ Hash, Array, Set ] # @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 # @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 # 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?(type) !primitive?(type) && !structure?(type) && type.respond_to?(:parse) && type.method(:parse).arity == 1 end end end grape-0.13.0/lib/grape/error_formatter/0000755000004100000410000000000012563420522020002 5ustar www-datawww-datagrape-0.13.0/lib/grape/error_formatter/xml.rb0000644000004100000410000000106112563420522021125 0ustar www-datawww-datamodule Grape module ErrorFormatter module Xml class << self def call(message, backtrace, options = {}, env = nil) message = Grape::ErrorFormatter::Base.present(message, env) result = message.is_a?(Hash) ? message : { message: message } if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty? result = result.merge(backtrace: backtrace) end result.respond_to?(:to_xml) ? result.to_xml(root: :error) : result.to_s end end end end end grape-0.13.0/lib/grape/error_formatter/json.rb0000644000004100000410000000100112563420522021270 0ustar www-datawww-datamodule Grape module ErrorFormatter module Json class << self def call(message, backtrace, options = {}, env = nil) message = Grape::ErrorFormatter::Base.present(message, env) result = message.is_a?(String) ? { error: message } : message if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty? result = result.merge(backtrace: backtrace) end MultiJson.dump(result) end end end end end grape-0.13.0/lib/grape/error_formatter/txt.rb0000644000004100000410000000100712563420522021144 0ustar www-datawww-datamodule Grape module ErrorFormatter module Txt class << self def call(message, backtrace, options = {}, env = nil) message = Grape::ErrorFormatter::Base.present(message, env) result = message.is_a?(Hash) ? MultiJson.dump(message) : message if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty? result += "\r\n " result += backtrace.join("\r\n ") end result end end end end end grape-0.13.0/lib/grape/error_formatter/base.rb0000644000004100000410000000347612563420522021253 0ustar www-datawww-datamodule Grape module ErrorFormatter module Base class << self FORMATTERS = { serializable_hash: Grape::ErrorFormatter::Json, json: Grape::ErrorFormatter::Json, jsonapi: Grape::ErrorFormatter::Json, txt: Grape::ErrorFormatter::Txt, xml: Grape::ErrorFormatter::Xml } def formatters(options) FORMATTERS.merge(options[:error_formatters] || {}) end def formatter_for(api_format, options = {}) spec = formatters(options)[api_format] case spec when nil options[:default_error_formatter] || Grape::ErrorFormatter::Txt when Symbol method(spec) else spec end end end module_function def present(message, env) present_options = {} present_options[:with] = message.delete(:with) if message.is_a?(Hash) presenter = env['api.endpoint'].entity_class_for_obj(message, present_options) unless presenter || env['rack.routing_args'].nil? # env['api.endpoint'].route does not work when the error occurs within a middleware # the Endpoint does not have a valid env at this moment http_codes = env['rack.routing_args'][:route_info].route_http_codes || [] found_code = http_codes.find do |http_code| (http_code[0].to_i == env['api.endpoint'].status) && http_code[2].respond_to?(:represent) end if env['api.endpoint'].request presenter = found_code[2] if found_code end if presenter embeds = { env: env } embeds[:version] = env['api.version'] if env['api.version'] message = presenter.represent(message, embeds).serializable_hash end message end end end end grape-0.13.0/lib/grape/cookies.rb0000644000004100000410000000152112563420522016546 0ustar www-datawww-datamodule Grape class Cookies def initialize @cookies = {} @send_cookies = {} end def read(request) request.cookies.each do |name, value| @cookies[name.to_s] = value end end def write(header) @cookies.select { |key, _value| @send_cookies[key] == true }.each do |name, value| cookie_value = value.is_a?(Hash) ? value : { value: value } Rack::Utils.set_cookie_header! header, name, cookie_value end end def [](name) @cookies[name.to_s] end def []=(name, value) @cookies[name.to_s] = value @send_cookies[name.to_s] = true end def each(&block) @cookies.each(&block) end def delete(name, opts = {}) options = opts.merge(value: 'deleted', expires: Time.at(0)) self.[]=(name, options) end end end grape-0.13.0/lib/grape/route.rb0000644000004100000410000000136212563420522016253 0ustar www-datawww-datamodule Grape # A compiled route for inspection. class Route # @api private def initialize(options = {}) @options = options || {} end # @api private def method_missing(method_id, *arguments) match = /route_([_a-zA-Z]\w*)/.match(method_id.to_s) if match @options[match.captures.last.to_sym] else super end end # Generate a short, human-readable representation of this route. def to_s "version=#{route_version}, method=#{route_method}, path=#{route_path}" end private # This is defined so that certain Ruby methods which attempt to call #to_ary # on objects, e.g. Array#join, will not hit #method_missing. def to_ary nil end end end grape-0.13.0/lib/grape/locale/0000755000004100000410000000000012563420522016025 5ustar www-datawww-datagrape-0.13.0/lib/grape/locale/en.yml0000644000004100000410000000437712563420522017165 0ustar www-datawww-dataen: grape: errors: format: ! '%{attributes} %{message}' messages: coerce: 'is invalid' presence: 'is missing' regexp: 'is invalid' blank: 'is empty' values: 'does not have a valid value' missing_vendor_option: problem: 'missing :vendor option.' summary: 'when version using header, you must specify :vendor option. ' resolution: "eg: version 'v1', using: :header, vendor: 'twitter'" missing_mime_type: problem: 'missing mime type for %{new_format}' resolution: "you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES or add your own with content_type :%{new_format}, 'application/%{new_format}' " invalid_with_option_for_represent: problem: 'You must specify an entity class in the :with option.' resolution: 'eg: represent User, :with => Entity::User' missing_option: 'You must specify :%{option} options.' invalid_formatter: 'cannot convert %{klass} to %{to_format}' invalid_versioner_option: problem: 'Unknown :using for versioner: %{strategy}' resolution: 'available strategy for :using is :path, :header, :param' unknown_validator: 'unknown validator: %{validator_type}' unknown_options: 'unknown options: %{options}' unknown_parameter: 'unknown parameter: %{param}' incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}' mutual_exclusion: 'are mutually exclusive' at_least_one: 'are missing, at least one parameter must be provided' exactly_one: 'are missing, exactly one parameter must be provided' all_or_none: 'provide all or none of parameters' missing_group_type: 'group type is required' unsupported_group_type: 'group type must be Array or Hash' invalid_message_body: problem: "message body does not match declared format" resolution: "when specifying %{body_format} as content-type, you must pass valid %{body_format} in the request's 'body' " invalid_accept_header: problem: 'Invalid accept header' resolution: '%{message}' grape-0.13.0/lib/grape.rb0000644000004100000410000001025612563420522015117 0ustar www-datawww-datarequire 'logger' require 'rack' require 'rack/mount' require 'rack/builder' require 'rack/accept' require 'rack/auth/basic' require 'rack/auth/digest/md5' require 'hashie' require 'set' require 'active_support/version' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/ordered_hash' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/except' require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'multi_json' require 'multi_xml' require 'virtus' require 'i18n' require 'thread' I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__) module Grape extend ActiveSupport::Autoload eager_autoload do autoload :API autoload :Endpoint autoload :Route autoload :Namespace autoload :Path autoload :Cookies autoload :Validations autoload :Request, 'grape/http/request' end module Http extend ActiveSupport::Autoload eager_autoload do autoload :Headers end end module Exceptions extend ActiveSupport::Autoload autoload :Base autoload :Validation autoload :ValidationErrors autoload :MissingVendorOption autoload :MissingMimeType autoload :MissingOption autoload :InvalidFormatter autoload :InvalidVersionerOption autoload :UnknownValidator autoload :UnknownOptions autoload :UnknownParameter autoload :InvalidWithOptionForRepresent autoload :IncompatibleOptionValues autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' autoload :InvalidMessageBody autoload :InvalidAcceptHeader end module ErrorFormatter extend ActiveSupport::Autoload autoload :Base autoload :Json autoload :Txt autoload :Xml end module Formatter extend ActiveSupport::Autoload autoload :Base autoload :Json autoload :SerializableHash autoload :Txt autoload :Xml end module Parser extend ActiveSupport::Autoload autoload :Base autoload :Json autoload :Xml end module Middleware extend ActiveSupport::Autoload autoload :Base autoload :Versioner autoload :Formatter autoload :Error autoload :Globals module Auth extend ActiveSupport::Autoload autoload :Base autoload :DSL autoload :StrategyInfo autoload :Strategies end module Versioner extend ActiveSupport::Autoload autoload :Path autoload :Header autoload :Param autoload :AcceptVersionHeader end end module Util extend ActiveSupport::Autoload autoload :InheritableValues autoload :StackableValues autoload :InheritableSetting autoload :StrictHashConfiguration autoload :FileResponse end module DSL extend ActiveSupport::Autoload eager_autoload do autoload :API autoload :Callbacks autoload :Settings autoload :Configuration autoload :InsideRoute autoload :Helpers autoload :Middleware autoload :Parameters autoload :RequestResponse autoload :Routing autoload :Validations end end class API extend ActiveSupport::Autoload autoload :Helpers end module Presenters extend ActiveSupport::Autoload autoload :Presenter end end require 'grape/util/content_types' require 'grape/util/parameter_types' require 'grape/validations/validators/base' require 'grape/validations/attributes_iterator' require 'grape/validations/validators/allow_blank' require 'grape/validations/validators/at_least_one_of' require 'grape/validations/validators/coerce' require 'grape/validations/validators/default' require 'grape/validations/validators/exactly_one_of' require 'grape/validations/validators/mutual_exclusion' require 'grape/validations/validators/presence' require 'grape/validations/validators/regexp' require 'grape/validations/validators/values' require 'grape/validations/params_scope' require 'grape/validations/validators/all_or_none' require 'grape/version' grape-0.13.0/gemfiles/0000755000004100000410000000000012563420522014515 5ustar www-datawww-datagrape-0.13.0/gemfiles/rails_3.gemfile0000644000004100000410000000035412563420522017405 0ustar www-datawww-data# This file was generated by Appraisal source 'https://rubygems.org' gem 'rails', '3.2.19' group :development, :test do gem 'rubocop', '~> 0.31.0' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' end gemspec :path => '../' grape-0.13.0/gemfiles/rails_4.gemfile0000644000004100000410000000035312563420522017405 0ustar www-datawww-data# This file was generated by Appraisal source 'https://rubygems.org' gem 'rails', '4.1.6' group :development, :test do gem 'rubocop', '~> 0.31.0' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' end gemspec :path => '../' grape-0.13.0/.rubocop.yml0000644000004100000410000000015212563420522015172 0ustar www-datawww-dataAllCops: Exclude: - vendor/**/* - bin/**/* - gemfiles/**/* inherit_from: .rubocop_todo.yml grape-0.13.0/metadata.yml0000644000004100000410000004255012563420522015233 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: grape version: !ruby/object:Gem::Version version: 0.13.0 platform: ruby authors: - Michael Bleigh autorequire: bindir: bin cert_chain: [] date: 2015-08-10 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rack requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.3.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.3.0 - !ruby/object:Gem::Dependency name: rack-mount requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rack-accept requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: activesupport requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: multi_json requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.3.2 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.3.2 - !ruby/object:Gem::Dependency name: multi_xml requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 0.5.2 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 0.5.2 - !ruby/object:Gem::Dependency name: hashie requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 2.1.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 2.1.0 - !ruby/object:Gem::Dependency name: virtus requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.0.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 1.0.0 - !ruby/object:Gem::Dependency name: builder requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: grape-entity requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 0.4.4 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: 0.4.4 - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: maruku requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: yard requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rack-test requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '3.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '3.0' - !ruby/object:Gem::Dependency name: bundler requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: cookiejar requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rack-contrib requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: mime-types requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: appraisal requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' description: A Ruby framework for rapid API development with great conventions. email: - michael@intridea.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - .rspec - .rubocop.yml - .rubocop_todo.yml - .travis.yml - .yardopts - Appraisals - CHANGELOG.md - CONTRIBUTING.md - Gemfile - Guardfile - LICENSE - README.md - RELEASING.md - Rakefile - UPGRADING.md - gemfiles/rails_3.gemfile - gemfiles/rails_4.gemfile - grape.gemspec - grape.png - lib/grape.rb - lib/grape/api.rb - lib/grape/api/helpers.rb - lib/grape/cookies.rb - lib/grape/dsl/api.rb - lib/grape/dsl/callbacks.rb - lib/grape/dsl/configuration.rb - lib/grape/dsl/helpers.rb - lib/grape/dsl/inside_route.rb - lib/grape/dsl/middleware.rb - lib/grape/dsl/parameters.rb - lib/grape/dsl/request_response.rb - lib/grape/dsl/routing.rb - lib/grape/dsl/settings.rb - lib/grape/dsl/validations.rb - lib/grape/endpoint.rb - lib/grape/error_formatter/base.rb - lib/grape/error_formatter/json.rb - lib/grape/error_formatter/txt.rb - lib/grape/error_formatter/xml.rb - lib/grape/exceptions/base.rb - lib/grape/exceptions/incompatible_option_values.rb - lib/grape/exceptions/invalid_accept_header.rb - lib/grape/exceptions/invalid_formatter.rb - lib/grape/exceptions/invalid_message_body.rb - lib/grape/exceptions/invalid_versioner_option.rb - lib/grape/exceptions/invalid_with_option_for_represent.rb - lib/grape/exceptions/missing_group_type.rb - lib/grape/exceptions/missing_mime_type.rb - lib/grape/exceptions/missing_option.rb - lib/grape/exceptions/missing_vendor_option.rb - lib/grape/exceptions/unknown_options.rb - lib/grape/exceptions/unknown_parameter.rb - lib/grape/exceptions/unknown_validator.rb - lib/grape/exceptions/unsupported_group_type.rb - lib/grape/exceptions/validation.rb - lib/grape/exceptions/validation_errors.rb - lib/grape/formatter/base.rb - lib/grape/formatter/json.rb - lib/grape/formatter/serializable_hash.rb - lib/grape/formatter/txt.rb - lib/grape/formatter/xml.rb - lib/grape/http/headers.rb - lib/grape/http/request.rb - lib/grape/locale/en.yml - lib/grape/middleware/auth/base.rb - lib/grape/middleware/auth/dsl.rb - lib/grape/middleware/auth/strategies.rb - lib/grape/middleware/auth/strategy_info.rb - lib/grape/middleware/base.rb - lib/grape/middleware/error.rb - lib/grape/middleware/filter.rb - lib/grape/middleware/formatter.rb - lib/grape/middleware/globals.rb - lib/grape/middleware/versioner.rb - lib/grape/middleware/versioner/accept_version_header.rb - lib/grape/middleware/versioner/header.rb - lib/grape/middleware/versioner/param.rb - lib/grape/middleware/versioner/path.rb - lib/grape/namespace.rb - lib/grape/parser/base.rb - lib/grape/parser/json.rb - lib/grape/parser/xml.rb - lib/grape/path.rb - lib/grape/presenters/presenter.rb - lib/grape/route.rb - lib/grape/util/content_types.rb - lib/grape/util/file_response.rb - lib/grape/util/inheritable_setting.rb - lib/grape/util/inheritable_values.rb - lib/grape/util/parameter_types.rb - lib/grape/util/stackable_values.rb - lib/grape/util/strict_hash_configuration.rb - lib/grape/validations.rb - lib/grape/validations/attributes_iterator.rb - lib/grape/validations/params_scope.rb - lib/grape/validations/validators/all_or_none.rb - lib/grape/validations/validators/allow_blank.rb - lib/grape/validations/validators/at_least_one_of.rb - lib/grape/validations/validators/base.rb - lib/grape/validations/validators/coerce.rb - lib/grape/validations/validators/default.rb - lib/grape/validations/validators/exactly_one_of.rb - lib/grape/validations/validators/multiple_params_base.rb - lib/grape/validations/validators/mutual_exclusion.rb - lib/grape/validations/validators/presence.rb - lib/grape/validations/validators/regexp.rb - lib/grape/validations/validators/values.rb - lib/grape/version.rb - spec/grape/api/custom_validations_spec.rb - spec/grape/api/deeply_included_options_spec.rb - spec/grape/api/nested_helpers_spec.rb - spec/grape/api/shared_helpers_spec.rb - spec/grape/api_spec.rb - spec/grape/dsl/callbacks_spec.rb - spec/grape/dsl/configuration_spec.rb - spec/grape/dsl/helpers_spec.rb - spec/grape/dsl/inside_route_spec.rb - spec/grape/dsl/middleware_spec.rb - spec/grape/dsl/parameters_spec.rb - spec/grape/dsl/request_response_spec.rb - spec/grape/dsl/routing_spec.rb - spec/grape/dsl/settings_spec.rb - spec/grape/dsl/validations_spec.rb - spec/grape/endpoint_spec.rb - spec/grape/entity_spec.rb - spec/grape/exceptions/body_parse_errors_spec.rb - spec/grape/exceptions/invalid_accept_header_spec.rb - spec/grape/exceptions/invalid_formatter_spec.rb - spec/grape/exceptions/invalid_versioner_option_spec.rb - spec/grape/exceptions/missing_mime_type_spec.rb - spec/grape/exceptions/missing_option_spec.rb - spec/grape/exceptions/unknown_options_spec.rb - spec/grape/exceptions/unknown_validator_spec.rb - spec/grape/exceptions/validation_errors_spec.rb - spec/grape/integration/rack_spec.rb - spec/grape/loading_spec.rb - spec/grape/middleware/auth/base_spec.rb - spec/grape/middleware/auth/dsl_spec.rb - spec/grape/middleware/auth/strategies_spec.rb - spec/grape/middleware/base_spec.rb - spec/grape/middleware/error_spec.rb - spec/grape/middleware/exception_spec.rb - spec/grape/middleware/formatter_spec.rb - spec/grape/middleware/globals_spec.rb - spec/grape/middleware/versioner/accept_version_header_spec.rb - spec/grape/middleware/versioner/header_spec.rb - spec/grape/middleware/versioner/param_spec.rb - spec/grape/middleware/versioner/path_spec.rb - spec/grape/middleware/versioner_spec.rb - spec/grape/path_spec.rb - spec/grape/presenters/presenter_spec.rb - spec/grape/util/inheritable_setting_spec.rb - spec/grape/util/inheritable_values_spec.rb - spec/grape/util/parameter_types_spec.rb - spec/grape/util/stackable_values_spec.rb - spec/grape/util/strict_hash_configuration_spec.rb - spec/grape/validations/attributes_iterator_spec.rb - spec/grape/validations/params_scope_spec.rb - spec/grape/validations/validators/all_or_none_spec.rb - spec/grape/validations/validators/allow_blank_spec.rb - spec/grape/validations/validators/at_least_one_of_spec.rb - spec/grape/validations/validators/coerce_spec.rb - spec/grape/validations/validators/default_spec.rb - spec/grape/validations/validators/exactly_one_of_spec.rb - spec/grape/validations/validators/mutual_exclusion_spec.rb - spec/grape/validations/validators/presence_spec.rb - spec/grape/validations/validators/regexp_spec.rb - spec/grape/validations/validators/values_spec.rb - spec/grape/validations/validators/zh-CN.yml - spec/grape/validations_spec.rb - spec/shared/versioning_examples.rb - spec/spec_helper.rb - spec/support/basic_auth_encode_helpers.rb - spec/support/content_type_helpers.rb - spec/support/endpoint_faker.rb - spec/support/file_streamer.rb - spec/support/versioned_helpers.rb homepage: https://github.com/ruby-grape/grape licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.5 signing_key: specification_version: 4 summary: A simple Ruby framework for building REST-like APIs. test_files: - spec/grape/api/custom_validations_spec.rb - spec/grape/api/deeply_included_options_spec.rb - spec/grape/api/nested_helpers_spec.rb - spec/grape/api/shared_helpers_spec.rb - spec/grape/api_spec.rb - spec/grape/dsl/callbacks_spec.rb - spec/grape/dsl/configuration_spec.rb - spec/grape/dsl/helpers_spec.rb - spec/grape/dsl/inside_route_spec.rb - spec/grape/dsl/middleware_spec.rb - spec/grape/dsl/parameters_spec.rb - spec/grape/dsl/request_response_spec.rb - spec/grape/dsl/routing_spec.rb - spec/grape/dsl/settings_spec.rb - spec/grape/dsl/validations_spec.rb - spec/grape/endpoint_spec.rb - spec/grape/entity_spec.rb - spec/grape/exceptions/body_parse_errors_spec.rb - spec/grape/exceptions/invalid_accept_header_spec.rb - spec/grape/exceptions/invalid_formatter_spec.rb - spec/grape/exceptions/invalid_versioner_option_spec.rb - spec/grape/exceptions/missing_mime_type_spec.rb - spec/grape/exceptions/missing_option_spec.rb - spec/grape/exceptions/unknown_options_spec.rb - spec/grape/exceptions/unknown_validator_spec.rb - spec/grape/exceptions/validation_errors_spec.rb - spec/grape/integration/rack_spec.rb - spec/grape/loading_spec.rb - spec/grape/middleware/auth/base_spec.rb - spec/grape/middleware/auth/dsl_spec.rb - spec/grape/middleware/auth/strategies_spec.rb - spec/grape/middleware/base_spec.rb - spec/grape/middleware/error_spec.rb - spec/grape/middleware/exception_spec.rb - spec/grape/middleware/formatter_spec.rb - spec/grape/middleware/globals_spec.rb - spec/grape/middleware/versioner/accept_version_header_spec.rb - spec/grape/middleware/versioner/header_spec.rb - spec/grape/middleware/versioner/param_spec.rb - spec/grape/middleware/versioner/path_spec.rb - spec/grape/middleware/versioner_spec.rb - spec/grape/path_spec.rb - spec/grape/presenters/presenter_spec.rb - spec/grape/util/inheritable_setting_spec.rb - spec/grape/util/inheritable_values_spec.rb - spec/grape/util/parameter_types_spec.rb - spec/grape/util/stackable_values_spec.rb - spec/grape/util/strict_hash_configuration_spec.rb - spec/grape/validations/attributes_iterator_spec.rb - spec/grape/validations/params_scope_spec.rb - spec/grape/validations/validators/all_or_none_spec.rb - spec/grape/validations/validators/allow_blank_spec.rb - spec/grape/validations/validators/at_least_one_of_spec.rb - spec/grape/validations/validators/coerce_spec.rb - spec/grape/validations/validators/default_spec.rb - spec/grape/validations/validators/exactly_one_of_spec.rb - spec/grape/validations/validators/mutual_exclusion_spec.rb - spec/grape/validations/validators/presence_spec.rb - spec/grape/validations/validators/regexp_spec.rb - spec/grape/validations/validators/values_spec.rb - spec/grape/validations/validators/zh-CN.yml - spec/grape/validations_spec.rb - spec/shared/versioning_examples.rb - spec/spec_helper.rb - spec/support/basic_auth_encode_helpers.rb - spec/support/content_type_helpers.rb - spec/support/endpoint_faker.rb - spec/support/file_streamer.rb - spec/support/versioned_helpers.rb has_rdoc: grape-0.13.0/.gitignore0000644000004100000410000000056412563420522014717 0ustar www-datawww-data## MAC OS .DS_Store .com.apple.timemachine.supported ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## REDCAR .redcar ## VIM *.swp *.swo ## RUBYMINE .idea ## PROJECT::GENERAL coverage doc pkg .rvmrc .bundle .yardoc/* dist Gemfile.lock gemfiles/*.lock tmp ## Rubinius .rbx ## Bundler binstubs bin ## ripper-tags and gem-ctags tags ## PROJECT::SPECIFIC .project grape-0.13.0/grape.png0000644000004100000410000001026012563420522014525 0ustar www-datawww-data‰PNG  IHDRX{1#˜ltEXtSoftwareAdobe ImageReadyqÉe<RIDATxÚì]rÓÊÇ'ÔyÇw˜D ˆXf(+ÀþÓÓ3Óc €@œÅÎÀêÙßçôaSnÓ’ÓÑÔ·ŸÎPÌ«ö£ùµÖ¦Ž¿W[͸ •ß¿"TÉ ¾È8‘!·±ÚS‘ö„Ú'‘Äõ3&W@h ²j­½=9µ/õÕ~¬QÇ€ˆ,lª|‹lPeq-P·€Dð*²ÁÖ¾Ä;ˆ+ Q‘=Ÿ¬ÀÚÌ?5¿´ E6S¶`I\¨C@¢ä¼³i’ × uÔß½ , °^©³Ò~`ˆƒ¹ÒsÔ?³g{@ ȈË ÂóúöÓֵ͔0×ö…¯Ð®,~¯ˆãSžæ—?¡:ÛNY`I\sÍÑpÒâûË~|´BÛÙÏ*…<=I¸¼6W€@h¯L".Å”¶FSLY?’XX¯Z,œ0XðÄ_(w^¾|ùÌüû&†îçÏŸ·'ø®´&óñžöÙ‡Ž*níoÜL°¬(€ÈbJíâ@œd[†À¦Û_›ß§>2óÀæfûÿèƒ|A %ÛH¿Oô}ßšûk}{þ:àóƒQ_n¹9òš .Ë»ÎΩíS*â+h}Û¨í;üŠ”ß|P•·“gˆ.VÕr[s³¹ïlk"ûYÙ†ùqÏó?˜ñ¤sû¬ëåÑŒü~¹//‚÷]!ª$<…q?mÓ r>xó{Õxsl§w-oÅvAe²µÏ©¹>n=·ã þÍ•¿©¾/û»«Wì³Z.s:|°ÇwÆÎ¸G£ÆYÚçý—§)[èÎïKÂjÓW~ÖFA\*W¸Ž~=D9½Sj =ÊÿgmuaµéÂ…Ñ9±DõYq{~ ¥€ÀJâ£n‘¡M­QRÇfA¬…•žó™Å§ü½P}ð\V_yÐÐdÔŠuúÍø½oÉBû#Ôྸ’…Ùz´º 7Ê·‰¼ïÓuãZn‰ïZÉ"èK\}$Ú [È3‘U 2ÏS4 °iŠkc„YÜ$â.h\“A¹e‰Te¡-²Äu·m|æóëLÄap¨ ²ØÄõOƒŒüΟÅ5µØ¿…¢»`Ãå16ÿ÷EL ²ؽÓä*‚Hd±¦ÔìëÓøíå6Æ] á\GÌÿëƒe‘H¹Ï^d!°; 9âô6–8iYÏšåÖxªÛ)S=6H°˜­Ë÷&å3¾Á>X}KnJ¥³Œœ‡Ž§³Í¾ƒ“c…£eFÓûË ×Õ‚ËéÍö{n‰º'ß”¯°`a½jXƒÔ‘—V,Îú4ˆÔB0ÆWÚXÿܦ/‡NcÑ 'ÚÜoÓ%—£4JÒÂó¢–è|§î—Ü&¨mlcõÀö-qݲxçTNä_<»qÝ vúÖkîØ¹V‡Ná°xPºXÙÌ‹}MÂ:æ T޶üV\Þ— ‰…öI)§âÐ1hn”¾Û¼¯ƒ°‹!ðj§ý~phOô¼Í¡£º;ù/Yˆ%ù'?rãHð©[°ù:»‹ÕjÍ‹c8’HÐÿOt:wH@jîˆ×gÎKß¡+®;^ú»Ú{˳•ïGæ¬ñ7Ž®Ž|hÅŽêJê5££ÒÇŠ•=ç_Rþ 3 œª^…°`%º h½>s°(jžºJ„âÒþö„Û>–†-›CâqÍ@$bẅ¨vVMmÙðKP÷W\vÒ‚Êüz`Œµæ·œ÷‡¶»ˆÅãØ²b¥îÕAØ«KWà¦.°·5îÛa ãî—óÑ0iúþêËLÚ]}Ñ]åT¸H©Úl!ÌûBÛ•X±A ‡QTóçÛE°qèä©O Ö®#1?¥iSoÙ;}—ìV¡Üb‡É+•B$JXZ°{Í{cÇÖJØLWMª3¶?ek Cµ8ÞÖfòƒx…¼OGRáVˆ6~N*VìØic¿ò<69y_åÖ(’âFú¬\Øv+Ųä}å#ZØ¡ÁD:€òìÛ/>XW—Fd;“CÄ"íüÕ&þé›ÑVYÄ@×eÄrÒŽ] ß'Î ÔÚ.ïêèù ¼‡>_; èÕ±ÜõòUU –TߦŽâÚ¼Q6s¨8톓ÎÄ?»~̀؇SŒ9ÍT­+žj7¶;vq«ñ°À$)§ç¶&ΡŒ6äª<û½©Á/ŸK3 |Öý¬ ýç¡ÚÕ•K+²¡gÇÉìÝ# ßêÜl•惔¡¬Î`æÖîhP 6°³ÈÖì.6 ÇXUzÙ:°K õÎÂU·Ê‘ÉV'VG[ÏõŸ¡xYÚ¹ð·`õÁÓ½Šm,%!!¿mê­ ²Þ;ïcƒ–°ž›û½±`õ‹(~u×µ‘osEÌp…4Z—dAižœ@ƒ‹GtŠe…ƒéÔZ·óÔ¦oÜF½[®)ì:9ñƒ aÆB8Ùé!ïM-Zzs ŒÜ­ÖÖDpY¥p›,ŸjÓ2_BË‚uZ¸a˵ðœÇ†]s÷?ÒP‡‰³4ó"ÈÎ 6ÚêXåë샭o?ñ‘óá2E\Ñ1[Ú»°’GjäkôpL4Š벯“}®Z–kÇõÑ'üÚÄ@,'úìaw‰”e¨|T e[±H·c׋T¹x‹%ÚÔ{áøR%/|¥32ã÷Õšb¿6ÓÄõxmÃm¥I BV4 ü„â(¼Ÿ‘kàÂÑ-pwØe^}ÍÌ5Ÿœh„VY32Ä¡é4E;“ÛÎÄÖkîPîîñzb¯Mì¹IÌ’"R¸X­´wÖ5^|°l}æÓ€Þ)µ;‘Ÿ 6$—æíc£Sµ'à&JÊE06ÆmÑjÉ–l˦Ýqô[ÏVfâ1úNâPN1CÕ6CAÒ¥S]Ç«oêòkQÓñÕæÝi¥œß¡ôµÙ Öü¹I£â3~Né3÷¡‹W—Ó_…Ë*6[yJ3Ÿ±–ìà^±Âáw7;o…uñMR4‹°é‡à¶|¯ØD)ÍüBƹ ™š.¤ YŠÖY²?Žt½5÷Ñœ¤4ÃcÚŽW“ݑۊ]/Ç«KÔ´ÍÜ: \ƒ†Ê÷ÅZÙy'—ÔM ™ò“åDß+Žñűp•&í0ˆ”·†¯°nö >™¹¬aì¶Ýö·WF~r’žY²‹«Ý“ÿ¥¹¿M< S>!°óÙï¶‘¹úc¥âZ™i…á«Ü‡˜³åÔòs¶æÿýº½åfZ‹Kã7²Øæía…qP•y,ïÂÌì¿Eö½íø‹À Â5 Y,uµº}vèS£3,f‘Û€g`U‚yßL0î„ ðÁîo¬—j1ÅÆçèû‹ÅTÅœ²GóRjÛÎ)‚vœÈÆßÂWßi¦¼ªº1éÄ= h 7ÎúØøÃƒGÅÕÌü !ìãAzQz^Æŧ\>dQÅ8Uµ5²ÅÈ¥Â; ,`ÅØAx`D×ĦC`‘[›^+m*nt¯Nå4 [V>-ý}w)¬ •hR¬Ä-ÂWÂ<öÆAŒù\_Ì]\!°ãì5w¬þTV `ûˆ¥Z±-èû§èìçΜ{îÌTÎå ãJWÌs¥wöéBº»CÊõZ"þ~{sˆ°·ZßC1~ƒ]²iñwNšH`¶ • ­bÓ;¬nŒÛ~ ÛfhõÓß¼ït촿Ъ;Xl0)yg”=¹½~l?ôøÎ˜Û)-ì´X:ÏÍNç,|úÎ\p’ ¹?n\?0Õ,Mäˆd,ú—öK¯ÕÈ<õþäÊç즯®Ÿ×æ~k ´üÚA¾oÑ}!°©3Vˆ¶{:z'e¸3ÓbÐöyfƒ©y~Àê?»c;lJ癦ÆïùØi/^ÙÎàÙª ÏÆÃíÄÇäõÏ ŒóºÔK¶g°ïÌ}€ïv®{ZOJ`é>ó@7ËF‡ãs޵`Û)¼‹í5'“Ê @l¯&”×ÛAýÌvF87 67ú~Î}âvadÛ°VŠÛ¬ á pX?¢“ò.‚Pº]®îÖø§Xtg¿&‘Ø) ln êk )¬4žæ3…,”ÂéL®À^qmà"8RÄl‘©_šÆó͵®4B‹´–^zÈÖëk¡¥^c7˜±€î[&¡°Ÿ¡n•uaiÞ~t¦Giè½þÒÃÑ"ËÁ—¥ÁR6èj`¦ø8º>DÕõ6û[eÙMpeÜn–þœ®ê0ò ïl“À1¨½sÉ»k3|kÍúƤU§t° ‰ ßÇ>]:æs~€ª)ºzñJÚ’¥(I|í†ë@à3ˆô:ÆÆtfÀÖxp½ÙEÀ QÍ yeÒ½ø°âPy}6>6…ܦU˜Äomå…ª<Á|V®wØÒZqõrÂ0˜À’/6QñÚÙã?ôÞJˆ+þÄÕx\ zÐÀŠìÍ„D–ü¨17ó“Àçs¼ê€âê3æIð“\,²¾÷²©¸ lzÁšíK?Çv,üÍ ­½ðP*ÊQYrØÔ_Ã’ô‘O¾Éà¹ùíCö9(ˆÓ¬%¬V¼/´S`éËçºKÔ£²¼»àÎJ[=û›¢Z-MäÊ-F¸âÃ+s°Ø…ÆÜ•Æö«°P¹—(†ÉÑ êêz˳砜¡¾ÜàX®4(ô{_÷,nv+‚ iþ'À¬¶Mû9UIEND®B`‚grape-0.13.0/.yardopts0000644000004100000410000000005612563420522014571 0ustar www-datawww-data--markup-provider=redcarpet --markup=markdown grape-0.13.0/CONTRIBUTING.md0000644000004100000410000000670512563420522015163 0ustar www-datawww-dataContributing to Grape ===================== Grape is work of [hundreds of contributors](https://github.com/ruby-grape/grape/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/ruby-grape/grape/pulls), [propose features and discuss issues](https://github.com/ruby-grape/grape/issues). When in doubt, ask a question in the [Grape Google Group](http://groups.google.com/group/ruby-grape). #### Fork the Project Fork the [project on Github](https://github.com/ruby-grape/grape) and check out your copy. ``` git clone https://github.com/contributor/grape.git cd grape git remote add upstream https://github.com/ruby-grape/grape.git ``` #### Create a Topic Branch Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. ``` git checkout master git pull upstream master git checkout -b my-feature-branch ``` #### Bundle Install and Test Ensure that you can build the project and run tests. ``` bundle install bundle exec rake ``` Run tests against all supported versions of Rails. ``` appraisal install appraisal rake spec ``` #### Write Tests Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/grape](spec/grape). We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. #### Write Code Implement your feature or bug fix. Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted. Make sure that `bundle exec rake` completes without errors. #### Write Documentation Document any external behavior in the [README](README.md). #### Update Changelog Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account. #### Commit Changes Make sure git knows your name and email address: ``` git config --global user.name "Your Name" git config --global user.email "contributor@example.com" ``` Writing good commit logs is important. A commit log should describe what changed and why. ``` git add ... git commit ``` #### Push ``` git push origin my-feature-branch ``` #### Make a Pull Request Go to https://github.com/contributor/grape and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. #### Rebase If you've been working on a change for a while, rebase with upstream/master. ``` git fetch upstream git rebase upstream/master git push origin my-feature-branch -f ``` #### Update CHANGELOG Again Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. ``` * [#123](https://github.com/ruby-grape/grape/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). ``` Amend your previous commit and force push the changes. ``` git commit --amend git push origin my-feature-branch -f ``` #### Check on Your Pull Request Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! #### Thank You Please do know that we really appreciate and value your time and work. We love you, really. grape-0.13.0/LICENSE0000644000004100000410000000207212563420522013730 0ustar www-datawww-dataCopyright (c) 2010-2015 Michael Bleigh and Intridea, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. grape-0.13.0/RELEASING.md0000644000004100000410000000573412563420522014566 0ustar www-datawww-dataReleasing Grape =============== There're no particular rules about when to release Grape. Release bug fixes frequently, features not so frequently and breaking API changes rarely. ### Release Run tests, check that all tests succeed locally. ``` bundle install rake ``` Check that the last build succeeded in [Travis CI](https://travis-ci.org/ruby-grape/grape) for all supported platforms. Those with r/w permissions to the [master Grape repository](https://github.com/ruby-grape/grape) generally have large Grape-based projects. Point one to Grape HEAD and run all your API tests to catch any obvious regressions. ``` gem grape, github: 'ruby-grape/grape' ``` Increment the version, modify [lib/grape/version.rb](lib/grape/version.rb). * Increment the third number if the release has bug fixes and/or very minor features, only (eg. change `0.5.1` to `0.5.2`). * Increment the second number if the release contains major features or breaking API changes (eg. change `0.5.1` to `0.6.0`). Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the documentation for a stable release. Remove references to the previous release of Grape. Keep the file open, you'll have to undo this change after the release. ``` ## Stable Release You're reading the documentation for the stable release of Grape, 0.6.0. ``` Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version. ``` 0.6.0 (9/16/2013) ================= ``` Remove the line with "Your contribution here.", since there will be no more contributions to this release. Commit your changes. ``` git add README.md CHANGELOG.md lib/grape/version.rb git commit -m "Preparing for release, 0.6.0." git push origin master ``` Release. ``` $ rake release grape 0.6.0 built to pkg/grape-0.6.0.gem. Tagged v0.6.0. Pushed git commits and tags. Pushed grape 0.6.0 to rubygems.org. ``` ### Prepare for the Next Version Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the next release. ``` ## Stable Release You're reading the documentation for the next release of Grape, which should be 0.6.1. The current stable release is [0.6.0](https://github.com/ruby-grape/grape/blob/v0.6.0/README.md). ``` Add the next release to [CHANGELOG.md](CHANGELOG.md). ``` Next Release ============ * Your contribution here. ``` Bump the minor version in lib/grape/version.rb. ```ruby module Grape VERSION = '0.6.1' end ``` Comit your changes. ``` git add CHANGELOG.md README.md lib/grape/version.rb git commit -m "Preparing for next development iteration, 0.6.1." git push origin master ``` ### Make an Announcement Make an announcement on the [ruby-grape@googlegroups.com](mailto:ruby-grape@googlegroups.com) mailing list. The general format is as follows. ``` Grape 0.6.0 has been released. There were 8 contributors to this release, not counting documentation. Please note the breaking API change in ... [copy/paste CHANGELOG here] ``` grape-0.13.0/Appraisals0000644000004100000410000000014412563420522014743 0ustar www-datawww-dataappraise "rails-3" do gem "rails", "3.2.19" end appraise "rails-4" do gem "rails", "4.1.6" end grape-0.13.0/CHANGELOG.md0000644000004100000410000012721512563420522014543 0ustar www-datawww-data0.13.0 (8/10/2015) ================== #### Features * [#1039](https://github.com/ruby-grape/grape/pull/1039): Added support for custom parameter types - [@rnubel](https://github.com/rnubel). * [#1047](https://github.com/ruby-grape/grape/pull/1047): Adds `given` to DSL::Parameters, allowing for dependent params - [@rnubel](https://github.com/rnubel). * [#1064](https://github.com/ruby-grape/grape/pull/1064): Add public `Grape::Exception::ValidationErrors#full_messages` - [@romanlehnert](https://github.com/romanlehnert). * [#1079](https://github.com/ruby-grape/grape/pull/1079): Added `stream` method to take advantage of `Rack::Chunked` [@zbelzer](https://github.com/zbelzer). * [#1086](https://github.com/ruby-grape/grape/pull/1086): Added `ActiveSupport::Notifications` instrumentation - [@wagenet](https://github.com/wagenet). #### Fixes * [#1062](https://github.com/ruby-grape/grape/issues/1062): Fix: `Grape::Exceptions::ValidationErrors` will include headers set by `header` - [@yairgo](https://github.com/yairgo). * [#1038](https://github.com/ruby-grape/grape/pull/1038): Avoid dup-ing the `String` class when used in inherited params - [@rnubel](https://github.com/rnubel). * [#1042](https://github.com/ruby-grape/grape/issues/1042): Fix coercion of complex arrays - [@dim](https://github.com/dim). * [#1045](https://github.com/ruby-grape/grape/pull/1045): Do not convert `Rack::Response` to `Rack::Response` in middleware - [@dmitry](https://github.com/dmitry). * [#1048](https://github.com/ruby-grape/grape/pull/1048): Only dup `InheritableValues`, remove support for `deep_dup` - [@toddmazierski](https://github.com/toddmazierski/). * [#1052](https://github.com/ruby-grape/grape/pull/1052): Reset `description[:params]` when resetting validations - [@marshall-lee](https://github.com/marshall-lee). * [#1088](https://github.com/ruby-grape/grape/pull/1088): Support ActiveSupport 3.x by explicitly requiring `Hash#except` - [@wagenet](https://github.com/wagenet). * [#1096](https://github.com/ruby-grape/grape/pull/1096): Fix coercion on booleans - [@towanda](https://github.com/towanda). 0.12.0 (6/18/2015) ================== #### Features * [#995](https://github.com/ruby-grape/grape/issues/995): Added support for coercion to Set or Set[Other] - [@jordansexton](https://github.com/jordansexton) [@u2](https://github.com/u2). * [#980](https://github.com/ruby-grape/grape/issues/980): Grape is now eager-loaded - [@u2](https://github.com/u2). * [#956](https://github.com/ruby-grape/grape/issues/956): Support `present` with `Grape::Presenters::Presenter` - [@u2](https://github.com/u2). * [#974](https://github.com/ruby-grape/grape/pull/974): Added `error!` to `rescue_from` blocks - [@whatasunnyday](https://github.com/whatasunnyday). * [#950](https://github.com/ruby-grape/grape/pull/950): Status method can now accept one of Rack::Utils status code symbols (:ok, :found, :bad_request, etc.) - [@dabrorius](https://github.com/dabrorius). * [#952](https://github.com/ruby-grape/grape/pull/952): Status method now raises error when called with invalid status code - [@dabrorius](https://github.com/dabrorius). * [#957](https://github.com/ruby-grape/grape/pull/957): Regexp validator now supports `allow_blank`, `nil` value behavior changed - [@calfzhou](https://giihub.com/calfzhou). * [#962](https://github.com/ruby-grape/grape/pull/962): The `default` attribute with `false` value is documented now - [@ajvondrak](https://github.com/ajvondrak). * [#1026](https://github.com/ruby-grape/grape/pull/1026): Added `file` method, explicitly setting a file-like response object - [@dblock](https://github.com/dblock). #### Fixes * [#994](https://github.com/ruby-grape/grape/pull/994): Fixed optional Array params default to Hash - [@u2](https://github.com/u2). * [#988](https://github.com/ruby-grape/grape/pull/988): Fixed duplicate identical endpoints - [@u2](https://github.com/u2). * [#936](https://github.com/ruby-grape/grape/pull/936): Fixed default params processing for optional groups - [@dm1try](https://github.com/dm1try). * [#942](https://github.com/ruby-grape/grape/pull/942): Fixed forced presence for optional params when based on a reused entity that was also required in another context - [@croeck](https://github.com/croeck). * [#1001](https://github.com/ruby-grape/grape/pull/1001): Fixed calling endpoint with specified format with format in its path - [@hodak](https://github.com/hodak). * [#1005](https://github.com/ruby-grape/grape/pull/1005): Fixed the Grape::Middleware::Globals - [@urkle](https://github.com/urkle). * [#1012](https://github.com/ruby-grape/grape/pull/1012): Fixed `allow_blank: false` with a Boolean value of `false` - [@mfunaro](https://github.com/mfunaro). * [#1023](https://github.com/ruby-grape/grape/issues/1023): Fixes unexpected behavior with `present` and an object that responds to `merge` but isn't a Hash - [@dblock](https://github.com/dblock). * [#1017](https://github.com/ruby-grape/grape/pull/1017): Fixed `undefined method stringify_keys` with nested mutual exclusive params - [@quickpay](https://github.com/quickpay). 0.11.0 (2/23/2015) ================== * [#925](https://github.com/ruby-grape/grape/pull/925): Fixed `toplevel constant DateTime referenced by Virtus::Attribute::DateTime` - [@u2](https://github.com/u2). * [#916](https://github.com/ruby-grape/grape/pull/916): Added `DateTime/Date/Numeric/Boolean` type support `allow_blank` - [@u2](https://github.com/u2). * [#871](https://github.com/ruby-grape/grape/pull/871): Fixed `Grape::Middleware::Base#response` - [@galathius](https://github.com/galathius). * [#559](https://github.com/ruby-grape/grape/issues/559): Added support for Rack 1.6.0, which parses requests larger than 128KB - [@myitcv](https://github.com/myitcv). * [#876](https://github.com/ruby-grape/grape/pull/876): Call to `declared(params)` now returns a `Hashie::Mash` - [@rodzyn](https://github.com/rodzyn). * [#879](https://github.com/ruby-grape/grape/pull/879): The `route_info` value is no longer included in `params` Hash - [@rodzyn](https://github.com/rodzyn). * [#881](https://github.com/ruby-grape/grape/issues/881): Fixed `Grape::Validations::ValuesValidator` support for `Range` type - [@ajvondrak](https://github.com/ajvondrak). * [#901](https://github.com/ruby-grape/grape/pull/901): Fix: callbacks defined in a version block are only called for the routes defined in that block - [@kushkella](https://github.com/kushkella). * [#886](https://github.com/ruby-grape/grape/pull/886): Group of parameters made to require an explicit type of Hash or Array - [@jrichter1](https://github.com/jrichter1). * [#912](https://github.com/ruby-grape/grape/pull/912): Extended the `:using` feature for param documentation to `optional` fields - [@croeck](https://github.com/croeck). * [#906](https://github.com/ruby-grape/grape/pull/906): Fix: invalid body parse errors are not rescued by handlers - [@croeck](https://github.com/croeck). * [#913](https://github.com/ruby-grape/grape/pull/913): Fix: Invalid accept headers are not processed by rescue handlers - [@croeck](https://github.com/croeck). * [#913](https://github.com/ruby-grape/grape/pull/913): Fix: Invalid accept headers cause internal processing errors (500) when http_codes are defined - [@croeck](https://github.com/croeck). * [#917](https://github.com/ruby-grape/grape/pull/917): Use HTTPS for rubygems.org - [@O-I](https://github.com/O-I). 0.10.1 (12/28/2014) =================== * [#868](https://github.com/ruby-grape/grape/pull/868), [#862](https://github.com/ruby-grape/grape/pull/862), [#861](https://github.com/ruby-grape/grape/pull/861): Fixed `version`, `prefix`, and other settings being overridden or changing scope when mounting API - [@yesmeck](https://github.com/yesmeck). * [#864](https://github.com/ruby-grape/grape/pull/864): Fixed `declared(params, include_missing: false)` now returning attributes with `nil` and `false` values - [@ppadron](https://github.com/ppadron). 0.10.0 (12/19/2014) =================== * [#803](https://github.com/ruby-grape/grape/pull/803), [#820](https://github.com/ruby-grape/grape/pull/820): Added `all_or_none_of` parameter validator - [@loveltyoic](https://github.com/loveltyoic), [@natecj](https://github.com/natecj). * [#774](https://github.com/ruby-grape/grape/pull/774): Extended `mutually_exclusive`, `exactly_one_of`, `at_least_one_of` to work inside any kind of group: `requires` or `optional`, `Hash` or `Array` - [@ShPakvel](https://github.com/ShPakvel). * [#743](https://github.com/ruby-grape/grape/pull/743): Added `allow_blank` parameter validator to validate non-empty strings - [@elado](https://github.com/elado). * [#745](https://github.com/ruby-grape/grape/pull/745): Removed `atom+xml`, `rss+xml`, and `jsonapi` content-types - [@akabraham](https://github.com/akabraham). * [#745](https://github.com/ruby-grape/grape/pull/745): Added `:binary, application/octet-stream` content-type - [@akabraham](https://github.com/akabraham). * [#757](https://github.com/ruby-grape/grape/pull/757): Changed `desc` can now be used with a block syntax - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#779](https://github.com/ruby-grape/grape/pull/779): Fixed using `values` with a `default` proc - [@ShPakvel](https://github.com/ShPakvel). * [#799](https://github.com/ruby-grape/grape/pull/799): Fixed custom validators with required `Hash`, `Array` types - [@bwalex](https://github.com/bwalex). * [#784](https://github.com/ruby-grape/grape/pull/784): Fixed `present` to not overwrite the previously added contents of the response body whebn called more than once - [@mfunaro](https://github.com/mfunaro). * [#809](https://github.com/ruby-grape/grape/pull/809): Removed automatic `(.:format)` suffix on paths if you're using only one format (e.g., with `format :json`, `/path` will respond with JSON but `/path.xml` will be a 404) - [@ajvondrak](https://github.com/ajvondrak). * [#816](https://github.com/ruby-grape/grape/pull/816): Added ability to filter out missing params if params is a nested hash with `declared(params, include_missing: false)` - [@georgimitev](https://github.com/georgimitev). * [#819](https://github.com/ruby-grape/grape/pull/819): Allowed both `desc` and `description` in the params DSL - [@mzikherman](https://github.com/mzikherman). * [#821](https://github.com/ruby-grape/grape/pull/821): Fixed passing string value when hash is expected in params - [@rebelact](https://github.com/rebelact). * [#824](https://github.com/ruby-grape/grape/pull/824): Validate array params against list of acceptable values - [@dnd](https://github.com/dnd). * [#813](https://github.com/ruby-grape/grape/pull/813): Routing methods dsl refactored to get rid of explicit `paths` parameter - [@AlexYankee](https://github.com/AlexYankee). * [#826](https://github.com/ruby-grape/grape/pull/826): Find `coerce_type` for `Array` when not specified - [@manovotn](https://github.com/manovotn). * [#645](https://github.com/ruby-grape/grape/issues/645): Invoking `body false` will return `204 No Content` - [@dblock](https://github.com/dblock). * [#801](https://github.com/ruby-grape/grape/issues/801): Only evaluate permitted parameter `values` and `default` lazily on each request when declared as a proc - [@dblock](https://github.com/dblock). * [#679](https://github.com/ruby-grape/grape/issues/679): Fixed `OPTIONS` method returning 404 when combined with `prefix`- [@dblock](https://github.com/dblock). * [#679](https://github.com/ruby-grape/grape/issues/679): Fixed unsupported methods returning 404 instead of 405 when combined with `prefix`- [@dblock](https://github.com/dblock). 0.9.0 (8/27/2014) ================= #### Features * [#691](https://github.com/ruby-grape/grape/issues/691): Added `at_least_one_of` parameter validator - [@dblock](https://github.com/dblock). * [#698](https://github.com/ruby-grape/grape/pull/698): `error!` sets `status` for `Endpoint` too - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Added support for Auth-Middleware extension - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Removed `Grape::Middleware::Auth::Basic` - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Removed `Grape::Middleware::Auth::Digest` - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Removed `Grape::Middleware::Auth::OAuth2` - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#719](https://github.com/ruby-grape/grape/pull/719): Allow passing options hash to a custom validator - [@elado](https://github.com/elado). * [#716](https://github.com/ruby-grape/grape/pull/716): Calling `content-type` will now return the current content-type - [@dblock](https://github.com/dblock). * [#705](https://github.com/ruby-grape/grape/pull/705): Errors can now be presented with a `Grape::Entity` class - [@dspaeth-faber](https://github.com/dspaeth-faber). #### Fixes * [#687](https://github.com/ruby-grape/grape/pull/687): Fix: `mutually_exclusive` and `exactly_one_of` validation error messages now label parameters as strings, consistently with `requires` and `optional` - [@dblock](https://github.com/dblock). 0.8.0 (7/10/2014) ================= #### Features * [#639](https://github.com/ruby-grape/grape/pull/639): Added support for blocks with reusable params - [@mibon](https://github.com/mibon). * [#637](https://github.com/ruby-grape/grape/pull/637): Added support for `exactly_one_of` parameter validation - [@Morred](https://github.com/Morred). * [#626](https://github.com/ruby-grape/grape/pull/626): Added support for `mutually_exclusive` parameters - [@oliverbarnes](https://github.com/oliverbarnes). * [#617](https://github.com/ruby-grape/grape/pull/617): Running tests on Ruby 2.1.1, Rubinius 2.1 and 2.2, Ruby and JRuby HEAD - [@dblock](https://github.com/dblock). * [#397](https://github.com/ruby-grape/grape/pull/397): Adds `Grape::Endpoint.before_each` to allow easy helper stubbing - [@mbleigh](https://github.com/mbleigh). * [#673](https://github.com/ruby-grape/grape/pull/673): Avoid requiring non-existent fields when using Grape::Entity documentation - [@qqshfox](https://github.com/qqshfox). #### Fixes * [#671](https://github.com/ruby-grape/grape/pull/671): Allow required param with predefined set of values to be nil inside optional group - [@dm1try](https://github.com/dm1try). * [#651](https://github.com/ruby-grape/grape/pull/651): The `rescue_from` keyword now properly defaults to rescuing subclasses of exceptions - [@xevix](https://github.com/xevix). * [#614](https://github.com/ruby-grape/grape/pull/614): Params with `nil` value are now refused by `RegexpValidator` - [@dm1try](https://github.com/dm1try). * [#494](https://github.com/ruby-grape/grape/issues/494): Fixed performance issue with requests carrying a large payload - [@dblock](https://github.com/dblock). * [#619](https://github.com/ruby-grape/grape/pull/619): Convert specs to RSpec 3 syntax with Transpec - [@danielspector](https://github.com/danielspector). * [#632](https://github.com/ruby-grape/grape/pull/632): `Grape::Endpoint#present` causes ActiveRecord to make an extra query during entity's detection - [@fixme](https://github.com/fixme). 0.7.0 (4/2/2014) ================= #### Features * [#558](https://github.com/ruby-grape/grape/pull/558): Support lambda-based values for params - [@wpschallenger](https://github.com/wpschallenger). * [#510](https://github.com/ruby-grape/grape/pull/510): Support lambda-based default values for params - [@myitcv](https://github.com/myitcv). * [#511](https://github.com/ruby-grape/grape/pull/511): Added `required` option for OAuth2 middleware - [@bcm](https://github.com/bcm). * [#520](https://github.com/ruby-grape/grape/pull/520): Use `default_error_status` to specify the default status code returned from `error!` - [@salimane](https://github.com/salimane). * [#525](https://github.com/ruby-grape/grape/pull/525): The default status code returned from `error!` has been changed from 403 to 500 - [@dblock](https://github.com/dblock). * [#526](https://github.com/ruby-grape/grape/pull/526): Allowed specifying headers in `error!` - [@dblock](https://github.com/dblock). * [#527](https://github.com/ruby-grape/grape/pull/527): The `before_validation` callback is now a distinct one - [@myitcv](https://github.com/myitcv). * [#530](https://github.com/ruby-grape/grape/pull/530): Added ability to restrict `declared(params)` to the local endpoint with `include_parent_namespaces: false` - [@myitcv](https://github.com/myitcv). * [#531](https://github.com/ruby-grape/grape/pull/531): Helpers are now available to auth middleware, executing in the context of the endpoint - [@joelvh](https://github.com/joelvh). * [#540](https://github.com/ruby-grape/grape/pull/540): Ruby 2.1.0 is now supported - [@salimane](https://github.com/salimane). * [#544](https://github.com/ruby-grape/grape/pull/544): The `rescue_from` keyword now handles subclasses of exceptions by default - [@xevix](https://github.com/xevix). * [#545](https://github.com/ruby-grape/grape/pull/545): Added `type` (`Array` or `Hash`) support to `requires`, `optional` and `group` - [@bwalex](https://github.com/bwalex). * [#550](https://github.com/ruby-grape/grape/pull/550): Added possibility to define reusable params - [@dm1try](https://github.com/dm1try). * [#560](https://github.com/ruby-grape/grape/pull/560): Use `Grape::Entity` documentation to define required and optional parameters with `requires using:` - [@reynardmh](https://github.com/reynardmh). * [#572](https://github.com/ruby-grape/grape/pull/572): Added `documentation` support to `requires`, `optional` and `group` parameters - [@johnallen3d](https://github.com/johnallen3d). #### Fixes * [#600](https://github.com/ruby-grape/grape/pull/600): Don't use an `Entity` constant that is available in the namespace as presenter - [@fuksito](https://github.com/fuksito). * [#590](https://github.com/ruby-grape/grape/pull/590): Fix issue where endpoint param of type `Integer` cannot set values array - [@xevix](https://github.com/xevix). * [#586](https://github.com/ruby-grape/grape/pull/586): Do not repeat the same validation error messages - [@kiela](https://github.com/kiela). * [#508](https://github.com/ruby-grape/grape/pull/508): Allow parameters, such as content encoding, in `content_type` - [@dm1try](https://github.com/dm1try). * [#492](https://github.com/ruby-grape/grape/pull/492): Don't allow to have nil value when a param is required and has a list of allowed values - [@Antti](https://github.com/Antti). * [#495](https://github.com/ruby-grape/grape/pull/495): Fixed `ParamsScope#params` for parameters nested inside arrays - [@asross](https://github.com/asross). * [#498](https://github.com/ruby-grape/grape/pull/498): Dry'ed up options and headers logic, allow headers to be passed to OPTIONS requests - [@karlfreeman](https://github.com/karlfreeman). * [#500](https://github.com/ruby-grape/grape/pull/500): Skip entity auto-detection when explicitely passed - [@yaneq](https://github.com/yaneq). * [#503](https://github.com/ruby-grape/grape/pull/503): Calling declared(params) from child namespace fails to include parent namespace defined params - [@myitcv](https://github.com/myitcv). * [#512](https://github.com/ruby-grape/grape/pull/512): Don't create `Grape::Request` multiple times - [@dblock](https://github.com/dblock). * [#538](https://github.com/ruby-grape/grape/pull/538): Fixed default values for grouped params - [@dm1try](https://github.com/dm1try). * [#549](https://github.com/ruby-grape/grape/pull/549): Fixed handling of invalid version headers to return 406 if a header cannot be parsed - [@bwalex](https://github.com/bwalex). * [#557](https://github.com/ruby-grape/grape/pull/557): Pass `content_types` option to `Grape::Middleware::Error` to fix the content-type header for custom formats. - [@bernd](https://github.com/bernd). * [#585](https://github.com/ruby-grape/grape/pull/585): Fix after boot thread-safety issue - [@etehtsea](https://github.com/etehtsea). * [#587](https://github.com/ruby-grape/grape/pull/587): Fix oauth2 middleware compatibility with [draft-ietf-oauth-v2-31](http://tools.ietf.org/html/draft-ietf-oauth-v2-31) spec - [@etehtsea](https://github.com/etehtsea). * [#610](https://github.com/ruby-grape/grape/pull/610): Fixed group keyword was not working with type parameter - [@klausmeyer](https://github.com/klausmeyer/). 0.6.1 (10/19/2013) ================== #### Features * [#475](https://github.com/ruby-grape/grape/pull/475): Added support for the `:jsonapi`, `application/vnd.api+json` media type registered at http://jsonapi.org - [@bcm](https://github.com/bcm). * [#471](https://github.com/ruby-grape/grape/issues/471): Added parameter validator for a list of allowed values - [@vickychijwani](https://github.com/vickychijwani). * [#488](https://github.com/ruby-grape/grape/issues/488): Upgraded to Virtus 1.0 - [@dblock](https://github.com/dblock). #### Fixes * [#477](https://github.com/ruby-grape/grape/pull/477): Fixed `default_error_formatter` which takes a format symbol - [@vad4msiu](https://github.com/vad4msiu). #### Development * Implemented Rubocop, a Ruby code static code analyzer - [@dblock](https://github.com/dblock). 0.6.0 (9/16/2013) ================= #### Features * Grape is no longer tested against Ruby 1.8.7. * [#442](https://github.com/ruby-grape/grape/issues/442): Enable incrementally building on top of a previous API version - [@dblock](https://github.com/dblock). * [#442](https://github.com/ruby-grape/grape/issues/442): API `version` can now take an array of multiple versions - [@dblock](https://github.com/dblock). * [#444](https://github.com/ruby-grape/grape/issues/444): Added `:en` as fallback locale for I18n - [@aew](https://github.com/aew). * [#448](https://github.com/ruby-grape/grape/pull/448): Adding POST style parameters for DELETE requests - [@dquimper](https://github.com/dquimper). * [#450](https://github.com/ruby-grape/grape/pull/450): Added option to pass an exception handler lambda as an argument to `rescue_from` - [@robertopedroso](https://github.com/robertopedroso). * [#443](https://github.com/ruby-grape/grape/pull/443): Let `requires` and `optional` take blocks that initialize new scopes - [@asross](https://github.com/asross). * [#452](https://github.com/ruby-grape/grape/pull/452): Added `with` as a hash option to specify handlers for `rescue_from` and `error_formatter` [@robertopedroso](https://github.com/robertopedroso). * [#433](https://github.com/ruby-grape/grape/issues/433), [#462](https://github.com/ruby-grape/grape/issues/462): Validation errors are now collected and `Grape::Exceptions::ValidationErrors` is raised - [@stevschmid](https://github.com/stevschmid). #### Fixes * [#428](https://github.com/ruby-grape/grape/issues/428): Removes memoization from `Grape::Request` params to prevent middleware from freezing parameter values before `Formatter` can get them - [@mbleigh](https://github.com/mbleigh). 0.5.0 (6/14/2013) ================= #### Features * [#344](https://github.com/ruby-grape/grape/pull/344): Added `parser :type, nil` which disables input parsing for a given content-type - [@dblock](https://github.com/dblock). * [#381](https://github.com/ruby-grape/grape/issues/381): Added `cascade false` option at API level to remove the `X-Cascade: true` header from the API response - [@dblock](https://github.com/dblock). * [#392](https://github.com/ruby-grape/grape/pull/392): Extracted headers and params from `Endpoint` to `Grape::Request` - [@niedhui](https://github.com/niedhui). * [#376](https://github.com/ruby-grape/grape/pull/376): Added `route_param`, syntax sugar for quick declaration of route parameters - [@mbleigh](https://github.com/mbleigh). * [#390](https://github.com/ruby-grape/grape/pull/390): Added default value for an `optional` parameter - [@oivoodoo](https://github.com/oivoodoo). * [#403](https://github.com/ruby-grape/grape/pull/403): Added support for versioning using the `Accept-Version` header - [@politician](https://github.com/politician). * [#407](https://github.com/ruby-grape/grape/issues/407): Specifying `default_format` will also set the default POST/PUT data parser to the given format - [@dblock](https://github.com/dblock). * [#241](https://github.com/ruby-grape/grape/issues/241): Present with multiple entities using an optional Symbol - [@niedhui](https://github.com/niedhui). #### Fixes * [#378](https://github.com/ruby-grape/grape/pull/378): Fix: stop rescuing all exceptions during formatting - [@kbarrette](https://github.com/kbarrette). * [#380](https://github.com/ruby-grape/grape/pull/380): Fix: `Formatter#read_body_input` when transfer encoding is chunked - [@paulnicholon](https://github.com/paulnicholson). * [#347](https://github.com/ruby-grape/grape/issues/347): Fix: handling non-hash body params - [@paulnicholon](https://github.com/paulnicholson). * [#394](https://github.com/ruby-grape/grape/pull/394): Fix: path version no longer overwrites a `version` parameter - [@tmornini](https://github.com/tmornini). * [#412](https://github.com/ruby-grape/grape/issues/412): Fix: specifying `content_type` will also override the selection of the data formatter - [@dblock](https://github.com/dblock). * [#383](https://github.com/ruby-grape/grape/issues/383): Fix: Mounted APIs aren't inheriting settings (including `before` and `after` filters) - [@seanmoon](https://github.com/seanmoon). * [#408](https://github.com/ruby-grape/grape/pull/408): Fix: Goliath passes request header keys as symbols not strings - [@bobek](https://github.com/bobek). * [#417](https://github.com/ruby-grape/grape/issues/417): Fix: Rails 4 does not rewind input, causes POSTed data to be empty - [@dblock](https://github.com/dblock). * [#423](https://github.com/ruby-grape/grape/pull/423): Fix: `Grape::Endpoint#declared` now correctly handles nested params (ie. declared with `group`) - [@jbarreneche](https://github.com/jbarreneche). * [#427](https://github.com/ruby-grape/grape/issues/427): Fix: `declared(params)` breaks when `params` contains array - [@timhabermaas](https://github.com/timhabermaas) 0.4.1 (4/1/2013) ================ * [#375](https://github.com/ruby-grape/grape/pull/375): Fix: throwing an `:error` inside a middleware doesn't respect the `format` settings - [@dblock](https://github.com/dblock). 0.4.0 (3/17/2013) ================= * [#356](https://github.com/ruby-grape/grape/pull/356): Fix: presenting collections other than `Array` (eg. `ActiveRecord::Relation`) - [@zimbatm](https://github.com/zimbatm). * [#352](https://github.com/ruby-grape/grape/pull/352): Fix: using `Rack::JSONP` with `Grape::Entity` responses - [@deckchair](https://github.com/deckchair). * [#347](https://github.com/ruby-grape/grape/issues/347): Grape will accept any valid JSON as PUT or POST, including strings, symbols and arrays - [@qqshfox](https://github.com/qqshfox), [@dblock](https://github.com/dblock). * [#347](https://github.com/ruby-grape/grape/issues/347): JSON format APIs always return valid JSON, eg. strings are now returned as `"string"` and no longer `string` - [@dblock](https://github.com/dblock). * Raw body input from POST and PUT requests (`env['rack.input'].read`) is now available in `api.request.input` - [@dblock](https://github.com/dblock). * Parsed body input from POST and PUT requests is now available in `api.request.body` - [@dblock](https://github.com/dblock). * [#343](https://github.com/ruby-grape/grape/pull/343): Fix: return `Content-Type: text/plain` with error 405 - [@gustavosaume](https://github.com/gustavosaume), [@wyattisimo](https://github.com/wyattisimo). * [#357](https://github.com/ruby-grape/grape/pull/357): Grape now requires Rack 1.3.0 or newer - [@jhecking](https://github.com/jhecking). * [#320](https://github.com/ruby-grape/grape/issues/320): API `namespace` now supports `requirements` - [@niedhui](https://github.com/niedhui). * [#353](https://github.com/ruby-grape/grape/issues/353): Revert to standard Ruby logger formatter, `require active_support/all` if you want old behavior - [@rhunter](https://github.com/rhunter), [@dblock](https://github.com/dblock). * Fix: `undefined method 'call' for nil:NilClass` for an API method implementation without a block, now returns an empty string - [@dblock](https://github.com/dblock). 0.3.2 (2/28/2013) ================= * [#355](https://github.com/ruby-grape/grape/issues/355): Relax dependency constraint on Hashie - [@reset](https://github.com/reset). 0.3.1 (2/25/2013) ================= * [#351](https://github.com/ruby-grape/grape/issues/351): Compatibility with Ruby 2.0 - [@mbleigh](https://github.com/mbleigh). 0.3.0 (02/21/2013) ================== * [#294](https://github.com/ruby-grape/grape/issues/294): Extracted `Grape::Entity` into a [grape-entity](https://github.com/agileanimal/grape-entity) gem - [@agileanimal](https://github.com/agileanimal). * [#340](https://github.com/ruby-grape/grape/pull/339), [#342](https://github.com/ruby-grape/grape/pull/342): Added `:cascade` option to `version` to allow disabling of rack/mount cascade behavior - [@dieb](https://github.com/dieb). * [#333](https://github.com/ruby-grape/grape/pull/333): Added support for validation of arrays in `params` - [@flyerhzm](https://github.com/flyerhzm). * [#306](https://github.com/ruby-grape/grape/issues/306): Added I18n support for all Grape exceptions - [@niedhui](https://github.com/niedhui). * [#309](https://github.com/ruby-grape/grape/pull/309): Added XML support to the entity presenter - [@johnnyiller](https://github.com/johnnyiller), [@dblock](http://github.com/dblock). * [#131](https://github.com/ruby-grape/grape/issues/131): Added instructions for Grape API reloading in Rails - [@jyn](http://github.com/jyn), [@dblock](http://github.com/dblock). * [#317](https://github.com/ruby-grape/grape/issues/317): Added `headers` that returns a hash of parsed HTTP request headers - [@dblock](http://github.com/dblock). * [#332](https://github.com/ruby-grape/grape/pull/332): `Grape::Exceptions::Validation` now contains full nested parameter names - [@alovak](https://github.com/alovak). * [#328](https://github.com/ruby-grape/grape/issues/328): API version can now be specified as both String and Symbol - [@dblock](http://github.com/dblock). * [#190](https://github.com/ruby-grape/grape/issues/190): When you add a `GET` route for a resource, a route for the `HEAD` method will also be added automatically. You can disable this behavior with `do_not_route_head!` - [@dblock](http://github.com/dblock). * Added `do_not_route_options!`, which disables the automatic creation of the `OPTIONS` route - [@dblock](http://github.com/dblock). * [#309](https://github.com/ruby-grape/grape/pull/309): An XML format API will return an error instead of returning a string representation of the response if the latter cannot be converted to XML - [@dblock](http://github.com/dblock). * A formatter that raises an exception will cause the API to return a 500 error - [@dblock](http://github.com/dblock). * [#322](https://github.com/ruby-grape/grape/issues/322): When returning a 406 status, Grape will include the requested format or content-type in the response body - [@dblock](http://github.com/dblock). * [#60](https://github.com/ruby-grape/grape/issues/60): Fix: mounting of a Grape API onto a path - [@dblock](http://github.com/dblock). * [#335](https://github.com/ruby-grape/grape/pull/335): Fix: request body parameters from a `PATCH` request not available in `params` - [@FreakenK](http://github.com/FreakenK). 0.2.6 (01/11/2013) ================== * Fix: support content-type with character set when parsing POST and PUT input - [@dblock](http://github.com/dblock). * Fix: CVE-2013-0175, multi_xml parse vulnerability, require multi_xml 0.5.2 - [@dblock](http://github.com/dblock). 0.2.5 (01/10/2013) ================== * Added support for custom parsers via `parser`, in addition to built-in multipart, JSON and XML parsers - [@dblock](http://github.com/dblock). * Removed `body_params`, data sent via a POST or PUT with a supported content-type is merged into `params` - [@dblock](http://github.com/dblock). * Setting `format` will automatically remove other content-types by calling `content_type` - [@dblock](http://github.com/dblock). * Setting `content_type` will prevent any input data other than the matching content-type or any Rack-supported form and parseable media types (`application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`) from being parsed - [@dblock](http://github.com/dblock). * [#305](https://github.com/ruby-grape/grape/issues/305): Fix: presenting arrays of objects via `represent` or when auto-detecting an `Entity` constant in the objects being presented - [@brandonweiss](https://github.com/brandonweiss). * [#306](https://github.com/ruby-grape/grape/issues/306): Added i18n support for validation error messages - [@niedhui](https://github.com/niedhui). 0.2.4 (01/06/2013) ================== * [#297](https://github.com/ruby-grape/grape/issues/297): Added `default_error_formatter` - [@dblock](https://github.com/dblock). * [#297](https://github.com/ruby-grape/grape/issues/297): Setting `format` will automatically set `default_error_formatter` - [@dblock](https://github.com/dblock). * [#295](https://github.com/ruby-grape/grape/issues/295): Storing original API source block in endpoint's `source` attribute - [@dblock](https://github.com/dblock). * [#293](https://github.com/ruby-grape/grape/pull/293): Added options to `cookies.delete`, enables passing a path - [@inst](https://github.com/inst). * [#174](https://github.com/ruby-grape/grape/issues/174): The value of `env['PATH_INFO']` is no longer altered with `path` versioning - [@dblock](https://github.com/dblock). * [#296](https://github.com/ruby-grape/grape/issues/296): Fix: ArgumentError with default error formatter - [@dblock](https://github.com/dblock). * [#298](https://github.com/ruby-grape/grape/pull/298): Fix: subsequent calls to `body_params` would fail due to IO read - [@justinmcp](https://github.com/justinmcp). * [#301](https://github.com/ruby-grape/grape/issues/301): Fix: symbol memory leak in cookie and formatter middleware - [@dblock](https://github.com/dblock). * [#300](https://github.com/ruby-grape/grape/issues/300): Fix `Grape::API.routes` to include mounted api routes - [@aiwilliams](https://github.com/aiwilliams). * [#302](https://github.com/ruby-grape/grape/pull/302): Fix: removed redundant `autoload` entries - [@ugisozols](https://github.com/ugisozols). * [#172](https://github.com/ruby-grape/grape/issues/172): Fix: MultiJson deprecated methods warnings - [@dblock](https://github.com/dblock). * [#133](https://github.com/ruby-grape/grape/issues/133): Fix: header-based versioning with use of `prefix` - [@seanmoon](https://github.com/seanmoon), [@dblock](https://github.com/dblock). * [#280](https://github.com/ruby-grape/grape/issues/280): Fix: grouped parameters mangled in `route_params` hash - [@marcusg](https://github.com/marcusg), [@dblock](https://github.com/dblock). * [#304](https://github.com/ruby-grape/grape/issues/304): Fix: `present x, :with => Entity` returns class references with `format :json` - [@dblock](https://github.com/dblock). * [#196](https://github.com/ruby-grape/grape/issues/196): Fix: root requests don't work with `prefix` - [@dblock](https://github.com/dblock). 0.2.3 (24/12/2012) ================== * [#179](https://github.com/ruby-grape/grape/issues/178): Using `content_type` will remove all default content-types - [@dblock](https://github.com/dblock). * [#265](https://github.com/ruby-grape/grape/issues/264): Fix: Moved `ValidationError` into `Grape::Exceptions` - [@thepumpkin1979](https://github.com/thepumpkin1979). * [#269](https://github.com/ruby-grape/grape/pull/269): Fix: `LocalJumpError` will not be raised when using explict return in API methods - [@simulacre](https://github.com/simulacre). * [#86](https://github.com/ruby-grape/grape/issues/275): Fix Path-based versioning not recognizing `/` route - [@walski](https://github.com/walski). * [#273](https://github.com/ruby-grape/grape/pull/273): Disabled formatting via `serializable_hash` and added support for `format :serializable_hash` - [@dblock](https://github.com/dblock). * [#277](https://github.com/ruby-grape/grape/pull/277): Added a DSL to declare `formatter` in API settings - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#284](https://github.com/ruby-grape/grape/pull/284): Added a DSL to declare `error_formatter` in API settings - [@dblock](https://github.com/dblock). * [#285](https://github.com/ruby-grape/grape/pull/285): Removed `error_format` from API settings, now matches request format - [@dblock](https://github.com/dblock). * [#290](https://github.com/ruby-grape/grape/pull/290): The default error format for XML is now `error/message` instead of `hash/error` - [@dpsk](https://github.com/dpsk). * [#44](https://github.com/ruby-grape/grape/issues/44): Pass `env` into formatters to enable templating - [@dblock](https://github.com/dblock). 0.2.2 ===== #### Features * [#201](https://github.com/ruby-grape/grape/pull/201), [#236](https://github.com/ruby-grape/grape/pull/236), [#221](https://github.com/ruby-grape/grape/pull/221): Added coercion and validations support to `params` DSL - [@schmurfy](https://github.com/schmurfy), [@tim-vandecasteele](https://github.com/tim-vandecasteele), [@adamgotterer](https://github.com/adamgotterer). * [#204](https://github.com/ruby-grape/grape/pull/204): Added ability to declare shared `params` at `namespace` level - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#234](https://github.com/ruby-grape/grape/pull/234): Added a DSL for creating entities via mixin - [@mbleigh](https://github.com/mbleigh). * [#240](https://github.com/ruby-grape/grape/pull/240): Define API response format from a query string `format` parameter, if specified - [@neetiraj](https://github.com/neetiraj). * Adds Endpoint#declared to easily filter out unexpected params. - [@mbleigh](https://github.com/mbleigh) #### Fixes * [#248](https://github.com/ruby-grape/grape/pull/248): Fix: API `version` returns last version set - [@narkoz](https://github.com/narkoz). * [#242](https://github.com/ruby-grape/grape/issues/242): Fix: permanent redirect status should be `301`, was `304` - [@adamgotterer](https://github.com/adamgotterer). * [#211](https://github.com/ruby-grape/grape/pull/211): Fix: custom validations are no longer triggered when optional and parameter is not present - [@adamgotterer](https://github.com/adamgotterer). * [#210](https://github.com/ruby-grape/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer). * [#205](https://github.com/ruby-grape/grape/pull/205): Fix: Corrected parsing of empty JSON body on POST/PUT - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#181](https://github.com/ruby-grape/grape/pull/181): Fix: Corrected JSON serialization of nested hashes containing `Grape::Entity` instances - [@benrosenblum](https://github.com/benrosenblum). * [#203](https://github.com/ruby-grape/grape/pull/203): Added a check to `Entity#serializable_hash` that verifies an entity exists on an object - [@adamgotterer](https://github.com/adamgotterer). * [#208](https://github.com/ruby-grape/grape/pull/208): `Entity#serializable_hash` must also check if attribute is generated by a user supplied block - [@ppadron](https://github.com/ppadron). * [#252](https://github.com/ruby-grape/grape/pull/252): Resources that don't respond to a requested HTTP method return 405 (Method Not Allowed) instead of 404 (Not Found) - [@simulacre](https://github.com/simulacre) 0.2.1 (7/11/2012) ================= * [#186](https://github.com/ruby-grape/grape/issues/186): Fix: helpers allow multiple calls with modules and blocks - [@ppadron](https://github.com/ppadron). * [#188](https://github.com/ruby-grape/grape/pull/188): Fix: multi-method routes append '(.:format)' only once - [@kainosnoema](https://github.com/kainosnoema). * [#64](https://github.com/ruby-grape/grape/issues/64), [#180](https://github.com/ruby-grape/grape/pull/180): Added support to `GET` request bodies as parameters - [@bobbytables](https://github.com/bobbytables). * [#175](https://github.com/ruby-grape/grape/pull/175): Added support for API versioning based on a request parameter - [@jackcasey](https://github.com/jackcasey). * [#168](https://github.com/ruby-grape/grape/pull/168): Fix: Formatter can parse symbol keys in the headers hash - [@netmask](https://github.com/netmask). * [#169](https://github.com/ruby-grape/grape/pull/169): Silence multi_json deprecation warnings - [@whiteley](https://github.com/whiteley). * [#166](https://github.com/ruby-grape/grape/pull/166): Added support for `redirect`, including permanent and temporary - [@allenwei](https://github.com/allenwei). * [#159](https://github.com/ruby-grape/grape/pull/159): Added `:requirements` to routes, allowing to use reserved characters in paths - [@gaiottino](https://github.com/gaiottino). * [#156](https://github.com/ruby-grape/grape/pull/156): Added support for adding formatters to entities - [@bobbytables](https://github.com/bobbytables). * [#183](https://github.com/ruby-grape/grape/pull/183): Added ability to include documentation in entities - [@flah00](https://github.com/flah00). * [#189](https://github.com/ruby-grape/grape/pull/189): `HEAD` requests no longer return a body - [@stephencelis](https://github.com/stephencelis). * [#97](https://github.com/ruby-grape/grape/issues/97): Allow overriding `Content-Type` - [@dblock](https://github.com/dblock). 0.2.0 (3/28/2012) ================= * Added support for inheriting exposures from entities - [@bobbytables](https://github.com/bobbytables). * Extended formatting with `default_format` - [@dblock](https://github.com/dblock). * Added support for cookies - [@lukaszsliwa](https://github.com/lukaszsliwa). * Added support for declaring additional content-types - [@joeyAghion](https://github.com/joeyAghion). * Added support for HTTP PATCH - [@LTe](https://github.com/LTe). * Added support for describing, documenting and reflecting APIs - [@dblock](https://github.com/dblock). * Added support for anchoring and vendoring - [@jwkoelewijn](https://github.com/jwkoelewijn). * Added support for HTTP OPTIONS - [@grimen](https://github.com/grimen). * Added support for silencing logger - [@evansj](https://github.com/evansj). * Added support for helper modules - [@freelancing-god](https://github.com/freelancing-god). * Added support for Accept header-based versioning - [@jch](https://github.com/jch), [@rodzyn](https://github.com/rodzyn). * Added support for mounting APIs and other Rack applications within APIs - [@mbleigh](https://github.com/mbleigh). * Added entities, multiple object representations - [@mbleigh](https://github.com/mbleigh). * Added ability to handle XML in the incoming request body - [@jwillis](https://github.com/jwillis). * Added support for a configurable logger - [@mbleigh](https://github.com/mbleigh). * Added support for before and after filters - [@mbleigh](https://github.com/mbleigh). * Extended `rescue_from`, which can now take a block - [@dblock](https://github.com/dblock). 0.1.5 (6/14/2011) ================== * Extended exception handling to all exceptions - [@dblock](https://github.com/dblock). * Added support for returning JSON objects from within error blocks - [@dblock](https://github.com/dblock). * Added support for handling incoming JSON in body - [@tedkulp](https://github.com/tedkulp). * Added support for HTTP digest authentication - [@daddz](https://github.com/daddz). 0.1.4 (4/8/2011) ================== * Allow multiple definitions of the same endpoint under multiple versions - [@chrisrhoden](https://github.com/chrisrhoden). * Added support for multipart URL parameters - [@mcastilho](https://github.com/mcastilho). * Added support for custom formatters - [@spraints](https://github.com/spraints). 0.1.3 (1/10/2011) ================== * Added support for JSON format in route matching - [@aiwilliams](https://github.com/aiwilliams). * Added suport for custom middleware - [@mbleigh](https://github.com/mbleigh). 0.1.1 (11/14/2010) ================== * Endpoints properly reset between each request - [@mbleigh](https://github.com/mbleigh). 0.1.0 (11/13/2010) ================== * Initial public release - [@mbleigh](https://github.com/mbleigh). grape-0.13.0/README.md0000644000004100000410000021113712563420522014206 0ustar www-datawww-data![grape logo](grape.png) [![Gem Version](http://img.shields.io/gem/v/grape.svg)](http://badge.fury.io/rb/grape) [![Build Status](http://img.shields.io/travis/ruby-grape/grape.svg)](https://travis-ci.org/ruby-grape/grape) [![Dependency Status](https://gemnasium.com/ruby-grape/grape.svg)](https://gemnasium.com/ruby-grape/grape) [![Code Climate](https://codeclimate.com/github/ruby-grape/grape.svg)](https://codeclimate.com/github/ruby-grape/grape) [![Inline docs](http://inch-ci.org/github/ruby-grape/grape.svg)](http://inch-ci.org/github/ruby-grape/grape) ## Table of Contents - [What is Grape?](#what-is-grape) - [Stable Release](#stable-release) - [Project Resources](#project-resources) - [Installation](#installation) - [Basic Usage](#basic-usage) - [Mounting](#mounting) - [Rack](#rack) - [ActiveRecord without Rails](#activerecord-without-rails) - [Alongside Sinatra (or other frameworks)](#alongside-sinatra-or-other-frameworks) - [Rails](#rails) - [Modules](#modules) - [Versioning](#versioning) - [Path](#path) - [Header](#header) - [Accept-Version Header](#accept-version-header) - [Param](#param) - [Describing Methods](#describing-methods) - [Parameters](#parameters) - [Declared](#declared) - [Include Missing](#include-missing) - [Parameter Validation and Coercion](#parameter-validation-and-coercion) - [Supported Parameter Types](#supported-parameter-types) - [Custom Types](#custom-types) - [Dependent Parameters](#dependent-parameters) - [Built-in Validators](#built-in-validators) - [Namespace Validation and Coercion](#namespace-validation-and-coercion) - [Custom Validators](#custom-validators) - [Validation Errors](#validation-errors) - [I18n](#i18n) - [Headers](#headers) - [Routes](#routes) - [Helpers](#helpers) - [Path Helpers](#path-helpers) - [Parameter Documentation](#parameter-documentation) - [Cookies](#cookies) - [HTTP Status Code](#http-status-code) - [Redirecting](#redirecting) - [Allowed Methods](#allowed-methods) - [Raising Exceptions](#raising-exceptions) - [Default Error HTTP Status Code](#default-error-http-status-code) - [Handling 404](#handling-404) - [Exception Handling](#exception-handling) - [Rails 3.x](#rails-3x) - [Logging](#logging) - [API Formats](#api-formats) - [JSONP](#jsonp) - [CORS](#cors) - [Content-type](#content-type) - [API Data Formats](#api-data-formats) - [RESTful Model Representations](#restful-model-representations) - [Grape Entities](#grape-entities) - [Hypermedia](#hypermedia) - [Rabl](#rabl) - [Active Model Serializers](#active-model-serializers) - [Sending Raw or No Data](#sending-raw-or-no-data) - [Authentication](#authentication) - [Describing and Inspecting an API](#describing-and-inspecting-an-api) - [Current Route and Endpoint](#current-route-and-endpoint) - [Before and After](#before-and-after) - [Anchoring](#anchoring) - [Using Custom Middleware](#using-custom-middleware) - [Rails Middleware](#rails-middleware) - [Remote IP](#remote-ip) - [Writing Tests](#writing-tests) - [Writing Tests with Rack](#writing-tests-with-rack) - [Writing Tests with Rails](#writing-tests-with-rails) - [Stubbing Helpers](#stubbing-helpers) - [Reloading API Changes in Development](#reloading-api-changes-in-development) - [Reloading in Rack Applications](#reloading-in-rack-applications) - [Reloading in Rails Applications](#reloading-in-rails-applications) - [Performance Monitoring](#performance-monitoring) - [Active Support Instrumentation](#active-support-instrumentation) - [Monitoring Products](#monitoring-products) - [Contributing to Grape](#contributing-to-grape) - [Hacking on Grape](#hacking-on-grape) - [License](#license) - [Copyright](#copyright) ## What is Grape? Grape is a REST-like API micro-framework for Ruby. It's designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily develop RESTful APIs. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more. ## Stable Release You're reading the documentation for the stable release of Grape, 0.13.0. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. ## Project Resources * [Grape Website](http://www.ruby-grape.org) * Need help? [Grape Google Group](http://groups.google.com/group/ruby-grape) ## Installation Grape is available as a gem, to install it just install the gem: gem install grape If you're using Bundler, add the gem to Gemfile. gem 'grape' Run `bundle install`. ## Basic Usage Grape APIs are Rack applications that are created by subclassing `Grape::API`. Below is a simple example showing some of the more common features of Grape in the context of recreating parts of the Twitter API. ```ruby module Twitter class API < Grape::API version 'v1', using: :header, vendor: 'twitter' format :json prefix :api helpers do def current_user @current_user ||= User.authorize!(env) end def authenticate! error!('401 Unauthorized', 401) unless current_user end end resource :statuses do desc "Return a public timeline." get :public_timeline do Status.limit(20) end desc "Return a personal timeline." get :home_timeline do authenticate! current_user.statuses.limit(20) end desc "Return a status." params do requires :id, type: Integer, desc: "Status id." end route_param :id do get do Status.find(params[:id]) end end desc "Create a status." params do requires :status, type: String, desc: "Your status." end post do authenticate! Status.create!({ user: current_user, text: params[:status] }) end desc "Update a status." params do requires :id, type: String, desc: "Status ID." requires :status, type: String, desc: "Your status." end put ':id' do authenticate! current_user.statuses.find(params[:id]).update({ user: current_user, text: params[:status] }) end desc "Delete a status." params do requires :id, type: String, desc: "Status ID." end delete ':id' do authenticate! current_user.statuses.find(params[:id]).destroy end end end end ``` ## Mounting ### Rack The above sample creates a Rack application that can be run from a rackup `config.ru` file with `rackup`: ```ruby run Twitter::API ``` And would respond to the following routes: GET /api/statuses/public_timeline GET /api/statuses/home_timeline GET /api/statuses/:id POST /api/statuses PUT /api/statuses/:id DELETE /api/statuses/:id Grape will also automatically respond to HEAD and OPTIONS for all GET, and just OPTIONS for all other routes. ### ActiveRecord without Rails If you want to use ActiveRecord within Grape, you will need to make sure that ActiveRecord's connection pool is handled correctly. The easiest way to achieve that is by using ActiveRecord's `ConnectionManagement` middleware in your `config.ru` before mounting Grape, e.g.: ```ruby use ActiveRecord::ConnectionAdapters::ConnectionManagement run Twitter::API ``` ### Alongside Sinatra (or other frameworks) If you wish to mount Grape alongside another Rack framework such as Sinatra, you can do so easily using `Rack::Cascade`: ```ruby # Example config.ru require 'sinatra' require 'grape' class API < Grape::API get :hello do { hello: "world" } end end class Web < Sinatra::Base get '/' do "Hello world." end end use Rack::Session::Cookie run Rack::Cascade.new [API, Web] ``` ### Rails Place API files into `app/api`. Rails expects a subdirectory that matches the name of the Ruby module and a file name that matches the name of the class. In our example, the file name location and directory for `Twitter::API` should be `app/api/twitter/api.rb`. Modify `application.rb`: ```ruby config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] ``` Modify `config/routes`: ```ruby mount Twitter::API => '/' ``` Additionally, if the version of your Rails is 4.0+ and the application uses the default model layer of ActiveRecord, you will want to use the [hashie-forbidden_attributes gem](https://github.com/Maxim-Filimonov/hashie-forbidden_attributes). This gem disables the security feature of `strong_params` at the model layer, allowing you the use of Grape's own params validation instead. ```ruby # Gemfile gem "hashie-forbidden_attributes" ``` See [below](#reloading-api-changes-in-development) for additional code that enables reloading of API changes in development. ### Modules You can mount multiple API implementations inside another one. These don't have to be different versions, but may be components of the same API. ```ruby class Twitter::API < Grape::API mount Twitter::APIv1 mount Twitter::APIv2 end ``` You can also mount on a path, which is similar to using `prefix` inside the mounted API itself. ```ruby class Twitter::API < Grape::API mount Twitter::APIv1 => '/v1' end ``` ## Versioning There are four strategies in which clients can reach your API's endpoints: `:path`, `:header`, `:accept_version_header` and `:param`. The default strategy is `:path`. ### Path ```ruby version 'v1', using: :path ``` Using this versioning strategy, clients should pass the desired version in the URL. curl http://localhost:9292/v1/statuses/public_timeline ### Header ```ruby version 'v1', using: :header, vendor: 'twitter' ``` Using this versioning strategy, clients should pass the desired version in the HTTP `Accept` head. curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline By default, the first matching version is used when no `Accept` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied. When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is returned if the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. ### HTTP Status Code By default Grape returns a 200 status code for `GET`-Requests and 201 for `POST`-Requests. You can use `status` to query and set the actual HTTP Status Code ```ruby post do status 202 if status == 200 # do some thing end end ``` You can also use one of status codes symbols that are provided by [Rack utils](http://www.rubydoc.info/github/rack/rack/Rack/Utils#HTTP_STATUS_CODES-constant) ```ruby post do status :no_content end ``` ### Accept-Version Header ```ruby version 'v1', using: :accept_version_header ``` Using this versioning strategy, clients should pass the desired version in the HTTP `Accept-Version` header. curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline By default, the first matching version is used when no `Accept-Version` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied. ### Param ```ruby version 'v1', using: :param ``` Using this versioning strategy, clients should pass the desired version as a request parameter, either in the URL query string or in the request body. curl http://localhost:9292/statuses/public_timeline?apiver=v1 The default name for the query parameter is 'apiver' but can be specified using the `:parameter` option. ```ruby version 'v1', using: :param, parameter: "v" ``` curl http://localhost:9292/statuses/public_timeline?v=v1 ## Describing Methods You can add a description to API methods and namespaces. ```ruby desc "Returns your public timeline." do detail 'more details' params API::Entities::Status.documentation success API::Entities::Entity 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 get :public_timeline do Status.limit(20) end ``` * `detail`: A more enhanced description * `params`: Define parameters directly from an `Entity` * `success`: (former entity) The `Entity` to be used to present by default this route * `failure`: (former http_codes) A definition of the used failure HTTP Codes and Entities * `named`: A helper to give a route a name and find it with this name in the documentation Hash * `headers`: A definition of the used Headers ## Parameters Request parameters are available through the `params` hash object. This includes `GET`, `POST` and `PUT` parameters, along with any named parameters you specify in your route strings. ```ruby get :public_timeline do Status.order(params[:sort_by]) end ``` Parameters are automatically populated from the request body on `POST` and `PUT` for form input, JSON and XML content-types. The request: ``` curl -d '{"text": "140 characters"}' 'http://localhost:9292/statuses' -H Content-Type:application/json -v ``` The Grape endpoint: ```ruby post '/statuses' do Status.create!(text: params[:text]) end ``` Multipart POSTs and PUTs are supported as well. The request: ``` curl --form image_file='@image.jpg;type=image/jpg' http://localhost:9292/upload ``` The Grape endpoint: ```ruby post "upload" do # file in params[:image_file] end ``` In the case of conflict between either of: * route string parameters * `GET`, `POST` and `PUT` parameters * the contents of the request body on `POST` and `PUT` route string parameters will have precedence. #### Declared Grape allows you to access only the parameters that have been declared by your `params` block. It filters out the params that have been passed, but are not allowed. Let's have the following api: ````ruby format :json post 'users/signup' do { "declared_params" => declared(params) } end ```` If we do not specify any params, declared will return an empty Hashie::Mash instance. **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "last_name": "last name"}}' ```` **Response** ````json { "declared_params": {} } ```` Once we add parameters requirements, grape will start returning only the declared params. ````ruby format :json params do requires :user, type: Hash do requires :first_name, type: String requires :last_name, type: String end end post 'users/signup' do { "declared_params" => declared(params) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "last_name": "last name", "random": "never shown"}}' ```` **Response** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": "last name" } } } ```` Returned hash is a Hashie::Mash instance so you can access parameters via dot notation: ```ruby declared(params).user == declared(params)["user"] ``` #### Include missing By default `declared(params)` returns parameters that has `nil` value. If you want to return only the parameters that have any value, you can use the `include_missing` option. By default it is `true`. Let's have the following api: ````ruby format :json params do requires :first_name, type: String optional :last_name, type: String end post 'users/signup' do { "declared_params" => declared(params, include_missing: false) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "random": "never shown"}}' ```` **Response with include_missing:false** ````json { "declared_params": { "user": { "first_name": "first name" } } } ```` **Response with include_missing:true** ````json { "declared_params": { "first_name": "first name", "last_name": null } } ```` It also works on nested hashes: ````ruby format :json params do requires :user, :type => Hash do requires :first_name, type: String optional :last_name, type: String requires :address, :type => Hash do requires :city, type: String optional :region, type: String end end end post 'users/signup' do { "declared_params" => declared(params, include_missing: false) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "random": "never shown", "address": { "city": "SF"}}}' ```` **Response with include_missing:false** ````json { "declared_params": { "user": { "first_name": "first name", "address": { "city": "SF" } } } } ```` **Response with include_missing:true** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": null, "address": { "city": "Zurich", "region": null } } } } ```` Note that an attribute with a `nil` value is not considered *missing* and will also be returned when `include_missing` is set to `false`: **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "last_name": null, "address": { "city": "SF"}}}' ```` **Response with include_missing:false** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": null, "address": { "city": "SF"} } } } ```` ## Parameter Validation and Coercion You can define validations and coercion options for your parameters using a `params` block. ```ruby params do requires :id, type: Integer optional :text, type: String, regexp: /^[a-z]+$/ group :media do requires :url end optional :audio do requires :format, type: Symbol, values: [:mp3, :wav, :aac, :ogg], default: :mp3 end mutually_exclusive :media, :audio end put ':id' do # params[:id] is an Integer end ``` When a type is specified an implicit validation is done after the coercion to ensure the output type is the one declared. Optional parameters can have a default value. ```ruby params do optional :color, type: String, default: 'blue' optional :random_number, type: Integer, default: -> { Random.rand(1..100) } optional :non_random_number, type: Integer, default: Random.rand(1..100) end ``` Note that default values will be passed through to any validation options specified. The following example will always fail if `:color` is not explicitly provided. ```ruby params do optional :color, type: String, default: 'blue', values: ['red', 'green'] end ``` The correct implementation is to ensure the default value passes all validations. ```ruby params do optional :color, type: String, default: 'blue', values: ['blue', 'red', 'green'] end ``` #### Supported Parameter Types The following are all valid types, supported out of the box by Grape: * Integer * Float * BigDecimal * Numeric * Date * DateTime * Time * Boolean * String * Symbol * Rack::Multipart::UploadedFile #### Custom Types Aside from the default set of supported types listed above, any class can be used as a type so long as it defines a class-level `parse` method. This method must take one string argument and return an instance of the correct type, or raise an exception to indicate the value was invalid. E.g., ```ruby class Color attr_reader :value def initialize(color) @value = color end def self.parse(value) fail 'Invalid color' unless %w(blue red green).include?(value) new(value) end end # ... params do requires :color, type: Color, default: Color.new('blue') end get '/stuff' do # params[:color] is already a Color. params[:color].value end ``` #### Validation of Nested Parameters Parameters can be nested using `group` or by calling `requires` or `optional` with a block. In the above example, this means `params[:media][:url]` is required along with `params[:id]`, and `params[:audio][:format]` is required only if `params[:audio]` is present. With a block, `group`, `requires` and `optional` accept an additional option `type` which can be either `Array` or `Hash`, and defaults to `Array`. Depending on the value, the nested parameters will be treated either as values of a hash or as values of hashes in an array. ```ruby params do optional :preferences, type: Array do requires :key requires :value end requires :name, type: Hash do requires :first_name requires :last_name end end ``` #### Dependent Parameters Suppose some of your parameters are only relevant if another parameter is given; Grape allows you to express this relationship through the `given` method in your parameters block, like so: ```ruby params do optional :shelf_id, type: Integer given :shelf_id do requires :bin_id, type: Integer end end ``` ### Built-in Validators #### `allow_blank` Parameters can be defined as `allow_blank`, ensuring that they contain a value. By default, `requires` only validates that a parameter was sent in the request, regardless its value. With `allow_blank: false`, empty values or whitespace only values are invalid. `allow_blank` can be combined with both `requires` and `optional`. If the parameter is required, it has to contain a value. If it's optional, it's possible to not send it in the request, but if it's being sent, it has to have some value, and not an empty string/only whitespaces. ```ruby params do requires :username, allow_blank: false optional :first_name, allow_blank: false end ``` #### `values` Parameters can be restricted to a specific set of values with the `:values` option. Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same number for each call to the endpoint of this `params` block. To have the default evaluate lazily with each request use a lambda, like `:random_number` above. ```ruby params do requires :status, type: Symbol, values: [:not_started, :processing, :done] optional :numbers, type: Array[Integer], default: 1, values: [1, 2, 3, 5, 8] end ``` Supplying a range to the `:values` option ensures that the parameter is (or parameters are) included in that range (using `Range#include?`). ```ruby params do requires :latitude, type: Float, values: -90.0..+90.0 requires :longitude, type: Float, values: -180.0..+180.0 optional :letters, type: Array[String], values: 'a'..'z' end ``` Note that *both* range endpoints have to be a `#kind_of?` your `:type` option (if you don't supplied the `:type` option, it will be guessed to be equal to the class of the range's first endpoint). So the following is invalid: ```ruby params do requires :invalid1, type: Float, values: 0..10 # 0.kind_of?(Float) => false optional :invalid2, values: 0..10.0 # 10.0.kind_of?(0.class) => false end ``` The `:values` option can also be supplied with a `Proc`, evaluated lazily with each request. For example, given a status model you may want to restrict by hashtags that you have previously defined in the `HashTag` model. ```ruby params do requires :hashtag, type: String, values: -> { Hashtag.all.map(&:tag) } end ``` #### `regexp` Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value does not match the regular expression an error will be returned. Note that this is true for both `requires` and `optional` parameters. ```ruby params do requires :email, regexp: /.+@.+/ end ``` The validator will pass if the parameter was sent without value. To ensure that the parameter contains a value, use `allow_blank: false`. ```ruby params do requires :email, allow_blank: false, regexp: /.+@.+/ end ``` #### `mutually_exclusive` Parameters can be defined as `mutually_exclusive`, ensuring that they aren't present at the same time in a request. ```ruby params do optional :beer optional :wine mutually_exclusive :beer, :wine end ``` Multiple sets can be defined: ```ruby params do optional :beer optional :wine mutually_exclusive :beer, :wine optional :scotch optional :aquavit mutually_exclusive :scotch, :aquavit end ``` **Warning**: Never define mutually exclusive sets with any required params. Two mutually exclusive required params will mean params are never valid, thus making the endpoint useless. One required param mutually exclusive with an optional param will mean the latter is never valid. #### `exactly_one_of` Parameters can be defined as 'exactly_one_of', ensuring that exactly one parameter gets selected. ```ruby params do optional :beer optional :wine exactly_one_of :beer, :wine end ``` #### `at_least_one_of` Parameters can be defined as 'at_least_one_of', ensuring that at least one parameter gets selected. ```ruby params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end ``` #### `all_or_none_of` Parameters can be defined as 'all_or_none_of', ensuring that all or none of parameters gets selected. ```ruby params do optional :beer optional :wine optional :juice all_or_none_of :beer, :wine, :juice end ``` #### Nested `mutually_exclusive`, `exactly_one_of`, `at_least_one_of`, `all_or_none_of` All of these methods can be used at any nested level. ```ruby params do requires :food do optional :meat optional :fish optional :rice at_least_one_of :meat, :fish, :rice end group :drink do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end optional :dessert do optional :cake optional :icecream mutually_exclusive :cake, :icecream end optional :recipe do optional :oil optional :meat all_or_none_of :oil, :meat end end ``` ### Namespace Validation and Coercion Namespaces allow parameter definitions and apply to every method within the namespace. ```ruby namespace :statuses do params do requires :user_id, type: Integer, desc: "A user ID." end namespace ":user_id" do desc "Retrieve a user's status." params do requires :status_id, type: Integer, desc: "A status ID." end get ":status_id" do User.find(params[:user_id]).statuses.find(params[:status_id]) end end end ``` The `namespace` method has a number of aliases, including: `group`, `resource`, `resources`, and `segment`. Use whichever reads the best for your API. You can conveniently define a route parameter as a namespace using `route_param`. ```ruby namespace :statuses do route_param :id do desc "Returns all replies for a status." get 'replies' do Status.find(params[:id]).replies end desc "Returns a status." get do Status.find(params[:id]) end end end ``` ### Custom Validators ```ruby class AlphaNumeric < Grape::Validations::Base def validate_param!(attr_name, params) unless params[attr_name] =~ /^[[:alnum:]]+$/ fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "must consist of alpha-numeric characters" end end end ``` ```ruby params do requires :text, alpha_numeric: true end ``` You can also create custom classes that take parameters. ```ruby class Length < Grape::Validations::Base def validate_param!(attr_name, params) unless params[attr_name].length <= @option fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long" end end end ``` ```ruby params do requires :text, length: 140 end ``` ### Validation Errors Validation and coercion errors are collected and an exception of type `Grape::Exceptions::ValidationErrors` is raised. If the exception goes uncaught it will respond with a status of 400 and an error message. The validation errors are grouped by parameter name and can be accessed via `Grape::Exceptions::ValidationErrors#errors`. The default response from a `Grape::Exceptions::ValidationErrors` is a humanly readable string, such as "beer, wine are mutually exclusive", in the following example. ```ruby params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end ``` You can rescue a `Grape::Exceptions::ValidationErrors` and respond with a custom response or turn the response into well-formatted JSON for a JSON API that separates individual parameters and the corresponding error messages. The following `rescue_from` example produces `[{"params":["beer","wine"],"messages":["are mutually exclusive"]}]`. ```ruby format :json subject.rescue_from Grape::Exceptions::ValidationErrors do |e| error! e, 400 end ``` `Grape::Exceptions::ValidationErrors#full_messages` returns the validation messages as an array. `Grape::Exceptions::ValidationErrors#message` joins the messages to one string. For responding with an array of validation messages, you can use `Grape::Exceptions::ValidationErrors#full_messages`. ```ruby format :json subject.rescue_from Grape::Exceptions::ValidationErrors do |e| error!({ messages: e.full_messages }, 400) end ``` ### I18n Grape supports I18n for parameter-related error messages, but will fallback to English if translations for the default locale have not been provided. See [en.yml](lib/grape/locale/en.yml) for message keys. ## Headers Request headers are available through the `headers` helper or from `env` in their original form. ```ruby get do error!('Unauthorized', 401) unless headers['Secret-Password'] == 'swordfish' end ``` ```ruby get do error!('Unauthorized', 401) unless env['HTTP_SECRET_PASSWORD'] == 'swordfish' end ``` You can set a response header with `header` inside an API. ```ruby header 'X-Robots-Tag', 'noindex' ``` When raising `error!`, pass additional headers as arguments. ```ruby error! 'Unauthorized', 401, 'X-Error-Detail' => 'Invalid token.' ``` ## Routes Optionally, you can define requirements for your named route parameters using regular expressions on namespace or endpoint. The route will match only if all requirements are met. ```ruby get ':id', requirements: { id: /[0-9]*/ } do Status.find(params[:id]) end namespace :outer, requirements: { id: /[0-9]*/ } do get :id do end get ":id/edit" do end end ``` ## Helpers You can define helper methods that your endpoints can use with the `helpers` macro by either giving a block or a module. ```ruby module StatusHelpers def user_info(user) "#{user} has statused #{user.statuses} status(s)" end end class API < Grape::API # define helpers with a block helpers do def current_user User.find(params[:user_id]) end end # or mix in a module helpers StatusHelpers get 'info' do # helpers available in your endpoint and filters user_info(current_user) end end ``` You can define reusable `params` using `helpers`. ```ruby class API < Grape::API helpers do params :pagination do optional :page, type: Integer optional :per_page, type: Integer end end desc "Get collection" params do use :pagination # aliases: includes, use_scope end get do Collection.page(params[:page]).per(params[:per_page]) end end ``` You can also define reusable `params` using shared helpers. ```ruby module SharedParams extend Grape::API::Helpers params :period do optional :start_date optional :end_date end params :pagination do optional :page, type: Integer optional :per_page, type: Integer end end class API < Grape::API helpers SharedParams desc "Get collection." params do use :period, :pagination end get do Collection .from(params[:start_date]) .to(params[:end_date]) .page(params[:page]) .per(params[:per_page]) end end ``` Helpers support blocks that can help set default values. The following API can return a collection sorted by `id` or `created_at` in `asc` or `desc` order. ```ruby module SharedParams extend Grape::API::Helpers params :order do |options| optional :order_by, type:Symbol, values:options[:order_by], default:options[:default_order_by] optional :order, type:Symbol, values:%i(asc desc), default:options[:default_order] end end class API < Grape::API helpers SharedParams desc "Get a sorted collection." params do use :order, order_by:%i(id created_at), default_order_by: :created_at, default_order: :asc end get do Collection.send(params[:order], params[:order_by]) end end ``` ## Path Helpers If you need methods for generating paths inside your endpoints, please see the [grape-route-helpers](https://github.com/reprah/grape-route-helpers) gem. ## Parameter Documentation You can attach additional documentation to `params` using a `documentation` hash. ```ruby params do optional :first_name, type: String, documentation: { example: 'Jim' } requires :last_name, type: String, documentation: { example: 'Smith' } end ``` ## Cookies You can set, get and delete your cookies very simply using `cookies` method. ```ruby class API < Grape::API get 'status_count' do cookies[:status_count] ||= 0 cookies[:status_count] += 1 { status_count: cookies[:status_count] } end delete 'status_count' do { status_count: cookies.delete(:status_count) } end end ``` Use a hash-based syntax to set more than one value. ```ruby cookies[:status_count] = { value: 0, expires: Time.tomorrow, domain: '.twitter.com', path: '/' } cookies[:status_count][:value] +=1 ``` Delete a cookie with `delete`. ```ruby cookies.delete :status_count ``` Specify an optional path. ```ruby cookies.delete :status_count, path: '/' ``` ## Redirecting You can redirect to a new url temporarily (302) or permanently (301). ```ruby redirect '/statuses' ``` ```ruby redirect '/statuses', permanent: true ``` ## Allowed Methods When you add a `GET` route for a resource, a route for the `HEAD` method will also be added automatically. You can disable this behavior with `do_not_route_head!`. ``` ruby class API < Grape::API do_not_route_head! get '/example' do # only responds to GET end end ``` When you add a route for a resource, a route for the `OPTIONS` method will also be added. The response to an OPTIONS request will include an "Allow" header listing the supported methods. ```ruby class API < Grape::API get '/rt_count' do { rt_count: current_user.rt_count } end params do requires :value, type: Integer, desc: 'Value to add to the rt count.' end put '/rt_count' do current_user.rt_count += params[:value].to_i { rt_count: current_user.rt_count } end end ``` ``` shell curl -v -X OPTIONS http://localhost:3000/rt_count > OPTIONS /rt_count HTTP/1.1 > < HTTP/1.1 204 No Content < Allow: OPTIONS, GET, PUT ``` You can disable this behavior with `do_not_route_options!`. If a request for a resource is made with an unsupported HTTP method, an HTTP 405 (Method Not Allowed) response will be returned. ``` shell curl -X DELETE -v http://localhost:3000/rt_count/ > DELETE /rt_count/ HTTP/1.1 > Host: localhost:3000 > < HTTP/1.1 405 Method Not Allowed < Allow: OPTIONS, GET, PUT ``` ## Raising Exceptions You can abort the execution of an API method by raising errors with `error!`. ```ruby error! 'Access Denied', 401 ``` You can also return JSON formatted objects by raising error! and passing a hash instead of a message. ```ruby error!({ error: "unexpected error", detail: "missing widget" }, 500) ``` You can present documented errors with a Grape entity using the the [grape-entity](https://github.com/ruby-grape/grape-entity) gem. ```ruby module API class Error < Grape::Entity expose :code expose :message end end ``` The following example specifies the entity to use in the `http_codes` definition. ``` desc 'My Route' do failure [[408, 'Unauthorized', API::Error]] end error!({ message: 'Unauthorized' }, 408) ``` The following example specifies the presented entity explicitly in the error message. ```ruby desc 'My Route' do failure [[408, 'Unauthorized']] end error!({ message: 'Unauthorized', with: API::Error }, 408) ``` ### Default Error HTTP Status Code By default Grape returns a 500 status code from `error!`. You can change this with `default_error_status`. ``` ruby class API < Grape::API default_error_status 400 get '/example' do error! "This should have http status code 400" end end ``` ### Handling 404 For Grape to handle all the 404s for your API, it can be useful to use a catch-all. In its simplest form, it can be like: ```ruby route :any, '*path' do error! # or something else end ``` It is very crucial to __define this endpoint at the very end of your API__, as it literally accepts every request. ## Exception Handling Grape can be told to rescue all exceptions and return them in the API format. ```ruby class Twitter::API < Grape::API rescue_from :all end ``` You can also rescue specific exceptions. ```ruby class Twitter::API < Grape::API rescue_from ArgumentError, UserDefinedError end ``` In this case ```UserDefinedError``` must be inherited from ```StandardError```. The error format will match the request format. See "Content-Types" below. Custom error formatters for existing and additional types can be defined with a proc. ```ruby class Twitter::API < Grape::API error_formatter :txt, lambda { |message, backtrace, options, env| "error: #{message} from #{backtrace}" } end ``` You can also use a module or class. ```ruby module CustomFormatter def self.call(message, backtrace, options, env) { message: message, backtrace: backtrace } end end class Twitter::API < Grape::API error_formatter :custom, CustomFormatter end ``` You can rescue all exceptions with a code block. The `error!` wrapper automatically sets the default error code and content-type. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| error!("rescued from #{e.class.name}") end end ``` Optionally, you can set the format, status code and headers. ```ruby class Twitter::API < Grape::API format :json rescue_from :all do |e| error!({ error: "Server error.", 500, { 'Content-Type' => 'text/error' } }) end end ``` You can also rescue specific exceptions with a code block and handle the Rack response at the lowest level. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| Rack::Response.new([ e.message ], 500, { "Content-type" => "text/error" }).finish end end ``` Or rescue specific exceptions. ```ruby class Twitter::API < Grape::API rescue_from ArgumentError do |e| error!("ArgumentError: #{e.message}") end rescue_from NotImplementedError do |e| error!("NotImplementedError: #{e.message}") end end ``` By default, `rescue_from` will rescue the exceptions listed and all their subclasses. Assume you have the following exception classes defined. ```ruby module APIErrors class ParentError < StandardError; end class ChildError < ParentError; end end ``` Then the following `rescue_from` clause will rescue exceptions of type `APIErrors::ParentError` and its subclasses (in this case `APIErrors::ChildError`). ```ruby rescue_from APIErrors::ParentError do |e| error!({ error: "#{e.class} error", message: e.message }, e.status) end ``` To only rescue the base exception class, set `rescue_subclasses: false`. The code below will rescue exceptions of type `RuntimeError` but _not_ its subclasses. ```ruby rescue_from RuntimeError, rescue_subclasses: false do |e| error!({ status: e.status, message: e.message, errors: e.errors }, e.status) end ``` #### Rails 3.x When mounted inside containers, such as Rails 3.x, errors like "404 Not Found" or "406 Not Acceptable" will likely be handled and rendered by Rails handlers. For instance, accessing a nonexistent route "/api/foo" raises a 404, which inside rails will ultimately be translated to an `ActionController::RoutingError`, which most likely will get rendered to a HTML error page. Most APIs will enjoy preventing downstream handlers from handling errors. You may set the `:cascade` option to `false` for the entire API or separately on specific `version` definitions, which will remove the `X-Cascade: true` header from API responses. ```ruby cascade false ``` ```ruby version 'v1', using: :header, vendor: 'twitter', cascade: false ``` ## Logging `Grape::API` provides a `logger` method which by default will return an instance of the `Logger` class from Ruby's standard library. To log messages from within an endpoint, you need to define a helper to make the logger available in the endpoint context. ```ruby class API < Grape::API helpers do def logger API.logger end end post '/statuses' do # ... logger.info "#{current_user} has statused" end end ``` You can also set your own logger. ```ruby class MyLogger def warning(message) puts "this is a warning: #{message}" end end class API < Grape::API logger MyLogger.new helpers do def logger API.logger end end get '/statuses' do logger.warning "#{current_user} has statused" end end ``` For similar to Rails request logging try the [grape_logging](https://github.com/aserafin/grape_logging) gem. ## API Formats Your API can declare which content-types to support by using `content_type`. If you do not specify any, Grape will support _XML_, _JSON_, _BINARY_, and _TXT_ content-types. The default format is `:txt`; you can change this with `default_format`. Essentially, the two APIs below are equivalent. ```ruby class Twitter::API < Grape::API # no content_type declarations, so Grape uses the defaults end class Twitter::API < Grape::API # the following declarations are equivalent to the defaults content_type :xml, 'application/xml' content_type :json, 'application/json' content_type :binary, 'application/octet-stream' content_type :txt, 'text/plain' default_format :txt end ``` If you declare any `content_type` whatsoever, the Grape defaults will be overridden. For example, the following API will only support the `:xml` and `:rss` content-types, but not `:txt`, `:json`, or `:binary`. Importantly, this means the `:txt` default format is not supported! So, make sure to set a new `default_format`. ```ruby class Twitter::API < Grape::API content_type :xml, 'application/xml' content_type :rss, 'application/xml+rss' default_format :xml end ``` Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API endpoint implementation. The response format (and thus the automatic serialization) is determined in the following order: * Use the file extension, if specified. If the file is .json, choose the JSON format. * Use the value of the `format` parameter in the query string, if specified. * Use the format set by the `format` option, if specified. * Attempt to find an acceptable format from the `Accept` header. * Use the default format, if specified by the `default_format` option. * Default to `:txt`. For example, consider the following API. ```ruby class MultipleFormatAPI < Grape::API content_type :xml, 'application/xml' content_type :json, 'application/json' default_format :json get :hello do { hello: 'world' } end end ``` * `GET /hello` (with an `Accept: */*` header) does not have an extension or a `format` parameter, so it will respond with JSON (the default format). * `GET /hello.xml` has a recognized extension, so it will respond with XML. * `GET /hello?format=xml` has a recognized `format` parameter, so it will respond with XML. * `GET /hello.xml?format=json` has a recognized extension (which takes precedence over the `format` parameter), so it will respond with XML. * `GET /hello.xls` (with an `Accept: */*` header) has an extension, but that extension is not recognized, so it will respond with JSON (the default format). * `GET /hello.xls` with an `Accept: application/xml` header has an unrecognized extension, but the `Accept` header corresponds to a recognized format, so it will respond with XML. * `GET /hello.xls` with an `Accept: text/plain` header has an unrecognized extension *and* an unrecognized `Accept` header, so it will respond with JSON (the default format). You can override this process explicitly by specifying `env['api.format']` in the API itself. For example, the following API will let you upload arbitrary files and return their contents as an attachment with the correct MIME type. ```ruby class Twitter::API < Grape::API post "attachment" do filename = params[:file][:filename] content_type MIME::Types.type_for(filename)[0].to_s env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is" header "Content-Disposition", "attachment; filename*=UTF-8''#{URI.escape(filename)}" params[:file][:tempfile].read end end ``` You can have your API only respond to a single format with `format`. If you use this, the API will **not** respond to file extensions other than specified in `format`. For example, consider the following API. ```ruby class SingleFormatAPI < Grape::API format :json get :hello do { hello: 'world' } end end ``` * `GET /hello` will respond with JSON. * `GET /hello.json` will respond with JSON. * `GET /hello.xml`, `GET /hello.foobar`, or *any* other extension will respond with an HTTP 404 error code. * `GET /hello?format=xml` will respond with an HTTP 406 error code, because the XML format specified by the request parameter is not supported. * `GET /hello` with an `Accept: application/xml` header will still respond with JSON, since it could not negotiate a recognized content-type from the headers and JSON is the effective default. The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests will fail with an HTTP 406 error code. ```ruby class Twitter::API < Grape::API format :json end ``` When the content-type is omitted, Grape will return a 406 error code unless `default_format` is specified. The following API will try to parse any data without a content-type using a JSON parser. ```ruby class Twitter::API < Grape::API format :json default_format :json end ``` If you combine `format` with `rescue_from :all`, errors will be rendered using the same format. If you do not want this behavior, set the default error formatter with `default_error_formatter`. ```ruby class Twitter::API < Grape::API format :json content_type :txt, "text/plain" default_error_formatter :txt end ``` Custom formatters for existing and additional types can be defined with a proc. ```ruby class Twitter::API < Grape::API content_type :xls, "application/vnd.ms-excel" formatter :xls, lambda { |object, env| object.to_xls } end ``` You can also use a module or class. ```ruby module XlsFormatter def self.call(object, env) object.to_xls end end class Twitter::API < Grape::API content_type :xls, "application/vnd.ms-excel" formatter :xls, XlsFormatter end ``` Built-in formatters are the following. * `:json`: use object's `to_json` when available, otherwise call `MultiJson.dump` * `:xml`: use object's `to_xml` when available, usually via `MultiXml`, otherwise call `to_s` * `:txt`: use object's `to_txt` when available, otherwise `to_s` * `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json` * `:binary`: data will be returned "as is" ### JSONP Grape supports JSONP via [Rack::JSONP](https://github.com/rack/rack-contrib), part of the [rack-contrib](https://github.com/rack/rack-contrib) gem. Add `rack-contrib` to your `Gemfile`. ```ruby require 'rack/contrib' class API < Grape::API use Rack::JSONP format :json get '/' do 'Hello World' end end ``` ### CORS Grape supports CORS via [Rack::CORS](https://github.com/cyu/rack-cors), part of the [rack-cors](https://github.com/cyu/rack-cors) gem. Add `rack-cors` to your `Gemfile`, then use the middleware in your config.ru file. ```ruby require 'rack/cors' use Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: :get end end run Twitter::API ``` ## Content-type Content-type is set by the formatter. You can override the content-type of the response at runtime by setting the `Content-Type` header. ```ruby class API < Grape::API get '/home_timeline_js' do content_type "application/javascript" "var statuses = ...;" end end ``` ## API Data Formats Grape accepts and parses input data sent with the POST and PUT methods as described in the Parameters section above. It also supports custom data formats. You must declare additional content-types via `content_type` and optionally supply a parser via `parser` unless a parser is already available within Grape to enable a custom format. Such a parser can be a function or a class. With a parser, parsed data is available "as-is" in `env['api.request.body']`. Without a parser, data is available "as-is" and in `env['api.request.input']`. The following example is a trivial parser that will assign any input with the "text/custom" content-type to `:value`. The parameter will be available via `params[:value]` inside the API call. ```ruby module CustomParser def self.call(object, env) { value: object.to_s } end end ``` ```ruby content_type :txt, "text/plain" content_type :custom, "text/custom" parser :custom, CustomParser put "value" do params[:value] end ``` You can invoke the above API as follows. ``` curl -X PUT -d 'data' 'http://localhost:9292/value' -H Content-Type:text/custom -v ``` You can disable parsing for a content-type with `nil`. For example, `parser :json, nil` will disable JSON parsing altogether. The request data is then available as-is in `env['api.request.body']`. ## RESTful Model Representations Grape supports a range of ways to present your data with some help from a generic `present` method, which accepts two arguments: the object to be presented and the options associated with it. The options hash may include `:with`, which defines the entity to expose. ### Grape Entities Add the [grape-entity](https://github.com/ruby-grape/grape-entity) gem to your Gemfile. Please refer to the [grape-entity documentation](https://github.com/ruby-grape/grape-entity/blob/master/README.md) for more details. The following example exposes statuses. ```ruby module API module Entities class Status < Grape::Entity expose :user_name expose :text, documentation: { type: "string", desc: "Status update text." } expose :ip, if: { type: :full } expose :user_type, :user_id, if: lambda { |status, options| status.user.public? } expose :digest { |status, options| Digest::MD5.hexdigest(status.txt) } expose :replies, using: API::Status, as: :replies end end class Statuses < Grape::API version 'v1' desc 'Statuses index' do params: API::Entities::Status.documentation end get '/statuses' do statuses = Status.all type = current_user.admin? ? :full : :default present statuses, with: API::Entities::Status, type: type end end end ``` You can use entity documentation directly in the params block with `using: Entity.documentation`. ```ruby module API class Statuses < Grape::API version 'v1' desc 'Create a status' params do requires :all, except: [:ip], using: API::Entities::Status.documentation.except(:id) end post '/status' do Status.create! params end end end ``` You can present with multiple entities using an optional Symbol argument. ```ruby get '/statuses' do statuses = Status.all.page(1).per(20) present :total_page, 10 present :per_page, 20 present :statuses, statuses, with: API::Entities::Status end ``` The response will be ``` { total_page: 10, per_page: 20, statuses: [] } ``` In addition to separately organizing entities, it may be useful to put them as namespaced classes underneath the model they represent. ```ruby class Status def entity Entity.new(self) end class Entity < Grape::Entity expose :text, :user_id end end ``` If you organize your entities this way, Grape will automatically detect the `Entity` class and use it to present your models. In this example, if you added `present Status.new` to your endpoint, Grape will automatically detect that there is a `Status::Entity` class and use that as the representative entity. This can still be overridden by using the `:with` option or an explicit `represents` call. You can present `hash` with `Grape::Presenters::Presenter` to keep things consistent. ```ruby get '/users' do present { id: 10, name: :dgz }, with: Grape::Presenters::Presenter end ```` The response will be ```ruby { id: 10, name: 'dgz' } ``` It has the same result with ```ruby get '/users' do present :id, 10 present :name, :dgz end ``` ### Hypermedia and Roar You can use [Roar](https://github.com/apotonick/roar) to render HAL or Collection+JSON with the help of [grape-roar](https://github.com/dblock/grape-roar), which defines a custom JSON formatter and enables presenting entities with Grape's `present` keyword. ### Rabl You can use [Rabl](https://github.com/nesquena/rabl) templates with the help of the [grape-rabl](https://github.com/LTe/grape-rabl) gem, which defines a custom Grape Rabl formatter. ### Active Model Serializers You can use [Active Model Serializers](https://github.com/rails-api/active_model_serializers) serializers with the help of the [grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers) gem, which defines a custom Grape AMS formatter. ## Sending Raw or No Data In general, use the binary format to send raw data. ```ruby class API < Grape::API get '/file' do content_type 'application/octet-stream' File.binread 'file.bin' end end ``` You can set the response body explicitly with `body`. ```ruby class API < Grape::API get '/' do content_type 'text/plain' body 'Hello World' # return value ignored end end ``` Use `body false` to return `204 No Content` without any data or content-type. You can also set the response to a file-like object with `file`. Note: Rack will read your entire Enumerable before returning a response. If you would like to stream the response, see `stream`. ```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('file.bin') end end ``` If you want a file-like object to be streamed using Rack::Chunked, use `stream`. ```ruby class API < Grape::API get '/' do stream FileStreamer.new('file.bin') end end ``` ## Authentication ### Basic and Digest Auth Grape has built-in Basic and Digest authentication (the given `block` is executed in the context of the current `Endpoint`). Authentication applies to the current namespace and any children, but not parents. ```ruby http_basic do |username, password| # verify user's password here { 'test' => 'password1' }[username] == password end ``` ```ruby http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username| # lookup the user's password here { 'user1' => 'password1' }[username] end ``` ### Register custom middleware for authentication Grape can use custom Middleware for authentication. How to implement these Middleware have a look at `Rack::Auth::Basic` or similar implementations. For registering a Middleware you need the following options: * `label` - the name for your authenticator to use it later * `MiddlewareClass` - the MiddlewareClass to use for authentication * `option_lookup_proc` - A Proc with one Argument to lookup the options at runtime (return value is an `Array` as Paramter for the Middleware). Example: ```ruby Grape::Middleware::Auth::Strategies.add(:my_auth, AuthMiddleware, ->(options) { [options[:realm]] } ) auth :my_auth, { realm: 'Test Api'} do |credentials| # lookup the user's password here { 'user1' => 'password1' }[username] end ``` Use [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) for OAuth2 support. ## Describing and Inspecting an API Grape routes can be reflected at runtime. This can notably be useful for generating documentation. Grape exposes arrays of API versions and compiled routes. Each route contains a `route_prefix`, `route_version`, `route_namespace`, `route_method`, `route_path` and `route_params`. You can add custom route settings to the route metadata with `route_setting`. ```ruby class TwitterAPI < Grape::API version 'v1' desc "Includes custom settings." route_setting :custom, key: 'value' get do end end ``` Examine the routes at runtime. ```ruby TwitterAPI::versions # yields [ 'v1', 'v2' ] TwitterAPI::routes # yields an array of Grape::Route objects TwitterAPI::routes[0].route_version # => 'v1' TwitterAPI::routes[0].route_description # => 'Includes custom settings.' TwitterAPI::routes[0].route_settings[:custom] # => { key: 'value' } ``` ## Current Route and Endpoint It's possible to retrieve the information about the current route from within an API call with `route`. ```ruby class MyAPI < Grape::API desc "Returns a description of a parameter." params do requires :id, type: Integer, desc: "Identity." end get "params/:id" do route.route_params[params[:id]] # yields the parameter description end end ``` The current endpoint responding to the request is `self` within the API block or `env['api.endpoint']` elsewhere. The endpoint has some interesting properties, such as `source` which gives you access to the original code block of the API implementation. This can be particularly useful for building a logger middleware. ```ruby class ApiLogger < Grape::Middleware::Base def before file = env['api.endpoint'].source.source_location[0] line = env['api.endpoint'].source.source_location[1] logger.debug "[api] #{file}:#{line}" end end ``` ## Before and After Blocks can be executed before or after every API call, using `before`, `after`, `before_validation` and `after_validation`. Before and after callbacks execute in the following order: 1. `before` 2. `before_validation` 3. _validations_ 4. `after_validation` 5. _the API call_ 6. `after` Steps 4, 5 and 6 only happen if validation succeeds. E.g. using `before`: ```ruby before do header "X-Robots-Tag", "noindex" end ``` The block applies to every API call within and below the current namespace: ```ruby class MyAPI < Grape::API get '/' do "root - #{@blah}" end namespace :foo do before do @blah = 'blah' end get '/' do "root - foo - #{@blah}" end namespace :bar do get '/' do "root - foo - bar - #{@blah}" end end end end ``` The behaviour is then: ```bash GET / # 'root - ' GET /foo # 'root - foo - blah' GET /foo/bar # 'root - foo - bar - blah' ``` Params on a `namespace` (or whatever alias you are using) also work when using `before_validation` or `after_validation`: ```ruby class MyAPI < Grape::API params do requires :blah, type: Integer end resource ':blah' do after_validation do # if we reach this point validations will have passed @blah = declared(params, include_missing: false)[:blah] end get '/' do @blah.class end end end ``` The behaviour is then: ```bash GET /123 # 'Fixnum' GET /foo # 400 error - 'blah is invalid' ``` When a callback is defined within a version block, it's only called for the routes defined in that block. ```ruby class Test < Grape::API resource :foo do version 'v1', :using => :path do before do @output ||= 'v1-' end get '/' do @output += 'hello' end end version 'v2', :using => :path do before do @output ||= 'v2-' end get '/' do @output += 'hello' end end end end ``` The behaviour is then: ```bash GET /foo/v1 # 'v1-hello' GET /foo/v2 # 'v2-hello' ``` ## Anchoring Grape by default anchors all request paths, which means that the request URL should match from start to end to match, otherwise a `404 Not Found` is returned. However, this is sometimes not what you want, because it is not always known upfront what can be expected from the call. This is because Rack-mount by default anchors requests to match from the start to the end, or not at all. Rails solves this problem by using a `anchor: false` option in your routes. In Grape this option can be used as well when a method is defined. For instance when your API needs to get part of an URL, for instance: ```ruby class TwitterAPI < Grape::API namespace :statuses do get '/(*:status)', anchor: false do end end end ``` This will match all paths starting with '/statuses/'. There is one caveat though: the `params[:status]` parameter only holds the first part of the request url. Luckily this can be circumvented by using the described above syntax for path specification and using the `PATH_INFO` Rack environment variable, using `env["PATH_INFO"]`. This will hold everything that comes after the '/statuses/' part. # Using Custom Middleware ## Rails Middleware Note that when you're using Grape mounted on Rails you don't have to use Rails middleware because it's already included into your middleware stack. You only have to implement the helpers to access the specific `env` variable. ### Remote IP By default you can access remote IP with `request.ip`. This is the remote IP address implemented by Rack. Sometimes it is desirable to get the remote IP [Rails-style](http://stackoverflow.com/questions/10997005/whats-the-difference-between-request-remote-ip-and-request-ip-in-rails) with `ActionDispatch::RemoteIp`. Add `gem 'actionpack'` to your Gemfile and `require 'action_dispatch/middleware/remote_ip.rb'`. Use the middleware in your API and expose a `client_ip` helper. See [this documentation](http://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html) for additional options. ```ruby class API < Grape::API use ActionDispatch::RemoteIp helpers do def client_ip env["action_dispatch.remote_ip"].to_s end end get :remote_ip do { ip: client_ip } end end ``` ## Writing Tests ### Writing Tests with Rack Use `rack-test` and define your API as `app`. #### RSpec You can test a Grape API with RSpec by making HTTP requests and examining the response. ```ruby require 'spec_helper' describe Twitter::API do include Rack::Test::Methods def app Twitter::API end describe Twitter::API do describe "GET /api/statuses/public_timeline" do it "returns an empty array of statuses" do get "/api/statuses/public_timeline" expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq [] end end describe "GET /api/statuses/:id" do it "returns a status by id" do status = Status.create! get "/api/statuses/#{status.id}" expect(last_response.body).to eq status.to_json end end end end ``` #### Airborne You can test with other RSpec-based frameworks, including [Airborne](https://github.com/brooklynDev/airborne), which uses `rack-test` to make requests. ```ruby require 'airborne' Airborne.configure do |config| config.rack_app = Twitter::API end describe Twitter::API do describe "GET /api/statuses/:id" do it "returns a status by id" do status = Status.create! get "/api/statuses/#{status.id}" expect_json(status.as_json) end end end ``` #### MiniTest ```ruby require "test_helper" class Twitter::APITest < MiniTest::Test include Rack::Test::Methods def app Twitter::API end def test_get_api_statuses_public_timeline_returns_an_empty_array_of_statuses get "/api/statuses/public_timeline" assert last_response.ok? assert_equal [], JSON.parse(last_response.body) end def test_get_api_statuses_id_returns_a_status_by_id status = Status.create! get "/api/statuses/#{status.id}" assert_equal status.to_json, last_response.body end end ``` ### Writing Tests with Rails #### RSpec ```ruby describe Twitter::API do describe "GET /api/statuses/public_timeline" do it "returns an empty array of statuses" do get "/api/statuses/public_timeline" expect(response.status).to eq(200) expect(JSON.parse(response.body)).to eq [] end end describe "GET /api/statuses/:id" do it "returns a status by id" do status = Status.create! get "/api/statuses/#{status.id}" expect(response.body).to eq status.to_json end end end ``` In Rails, HTTP request tests would go into the `spec/requests` group. You may want your API code to go into `app/api` - you can match that layout under `spec` by adding the following in `spec/spec_helper.rb`. ```ruby RSpec.configure do |config| config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/ end ``` #### MiniTest ```ruby class Twitter::APITest < ActiveSupport::TestCase include Rack::Test::Methods def app Rails.application end test "GET /api/statuses/public_timeline returns an empty array of statuses" do get "/api/statuses/public_timeline" assert last_response.ok? assert_equal [], JSON.parse(last_response.body) end test "GET /api/statuses/:id returns a status by id" do status = Status.create! get "/api/statuses/#{status.id}" assert_equal status.to_json, last_response.body end end ``` ### Stubbing Helpers Because helpers are mixed in based on the context when an endpoint is defined, it can be difficult to stub or mock them for testing. The `Grape::Endpoint.before_each` method can help by allowing you to define behavior on the endpoint that will run before every request. ```ruby describe 'an endpoint that needs helpers stubbed' do before do Grape::Endpoint.before_each do |endpoint| allow(endpoint).to receive(:helper_name).and_return('desired_value') end end after do Grape::Endpoint.before_each nil end it 'should properly stub the helper' do # ... end end ``` ## Reloading API Changes in Development ### Reloading in Rack Applications Use [grape-reload](https://github.com/AlexYankee/grape-reload). ### Reloading in Rails Applications Add API paths to `config/application.rb`. ```ruby # Auto-load API and its subdirectories config.paths.add File.join("app", "api"), glob: File.join("**", "*.rb") config.autoload_paths += Dir[Rails.root.join("app", "api", "*")] ``` Create `config/initializers/reload_api.rb`. ```ruby if Rails.env.development? ActiveSupport::Dependencies.explicitly_unloadable_constants << "Twitter::API" api_files = Dir[Rails.root.join('app', 'api', '**', '*.rb')] api_reloader = ActiveSupport::FileUpdateChecker.new(api_files) do Rails.application.reload_routes! end ActionDispatch::Callbacks.to_prepare do api_reloader.execute_if_updated end end ``` See [StackOverflow #3282655](http://stackoverflow.com/questions/3282655/ruby-on-rails-3-reload-lib-directory-for-each-request/4368838#4368838) for more information. ## Performance Monitoring ### Active Support Instrumentation Grape has built-in support for [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) which provides simple hook points to instrument key parts of your application. The following are currently supported: #### endpoint_run.grape The main execution of an endpoint, includes filters and rendering. * *endpoint* - The endpoint instance #### endpoint_render.grape The execution of the main content block of the endpoint. * *endpoint* - The endpoint instance #### endpoint_run_filters.grape * *endpoint* - The endpoint instance * *filters* - The filters being executed * *type* - The type of filters (before, before_validation, after_validation, after) See the [ActiveSupport::Notifications documentation](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html] for information on how to subscribe to these events. ### Monitoring Products Grape integrates with NewRelic via the [newrelic-grape](https://github.com/flyerhzm/newrelic-grape) gem, and with Librato Metrics with the [grape-librato](https://github.com/seanmoon/grape-librato) gem. ## Contributing to Grape Grape is work of hundreds of contributors. You're encouraged to submit pull requests, propose features and discuss issues. See [CONTRIBUTING](CONTRIBUTING.md). ## Hacking on Grape You can start hacking on Grape on [Nitrous.IO](https://www.nitrous.io/?utm_source=github.com&utm_campaign=grape&utm_medium=hackonnitrous) in a matter of seconds: [![Hack ruby-grape/grape on Nitrous.IO](https://d3o0mnbgv6k92a.cloudfront.net/assets/hack-l-v1-3cc067e71372f6045e1949af9d96095b.png)](https://www.nitrous.io/hack_button?source=embed&runtime=rails&repo=intridea%2Fgrape&file_to_open=README.md) ## License MIT License. See LICENSE for details. ## Copyright Copyright (c) 2010-2015 Michael Bleigh, and Intridea, Inc. grape-0.13.0/Guardfile0000644000004100000410000000046612563420522014555 0ustar www-datawww-dataguard :rspec, all_on_start: true, cmd: 'bundle exec rspec' do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { 'spec' } end guard :rubocop do watch(/.+\.rb$/) watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } end