grape-1.5.2/0000755000004100000410000000000014016744305012651 5ustar www-datawww-datagrape-1.5.2/grape.png0000644000004100000410000001026014016744305014454 0ustar www-datawww-dataPNG  IHDRX{1#ltEXtSoftwareAdobe ImageReadyqe<RIDATx]r'ywD Xf(+3c @aSnӒԷP֦W[͸ ߿"T 8!S'3&W@h j=9/~Q,l|lPeq-PD*־;+ Q=?5 E6S`I\C@伳i u߽ , ^~`s?g{@ Ȉ ֵ͔0Ю,~S?:NY`I\sp~|B*<=I6W@hL".ŔFSLY?XXZ,0X_(w^|&ϟ'&ه*noL(bJ@d[_ߧ>2f|A %HO}ߚk}{:Q_n9 .˻ΩS*+h}ۨ;|Pg.Vr[slk"Yنq?sь~//]!$x{xslw-ovAeϩ>n= ͕/WZ.s:|wθGY)[KjW~FA\*W~=D9Sj =gmua…9DYq{~ JnMQRfAŧP}\V_yduoB#z 7ʷӁuZnZ"K\}$ [3U2S4 ikc„Y$.h\AeTe-um|Lap OΟ5ؿ`16EL ؽ*Hd6]\GeH^d!; 968iYϚx)S=6H&3>X}KnJ;ceF Ղ́{n'ߔ`ajXԑV,4B0WXܦ/Nc 'o%4J|&mlc-qݲxTN_s(jJ„>-Cq@$bẅvVMmKPW\vҁz`淜bAثKW.5a 0iL]}]THl!BەXA QTEqO ֮#1?iSo;}Vb+B$JXZ{{cցJLWM3?ek C8fxOGRV6~N*Vic<69y_(F\v+Ų}#ZءD:/>XWFd;C"&VY@er] ' .>_; ձUU TߦQ6s8?~̀؇S9T+j7¶;vq$)&Ρ6); 6$cS'&JE06mjɖl˝q[Vf1NPN1C6CAҥS]ǫokQiߡ I3~Né3W_*6[yJ3^w7;ouMR4|D)Bƹ . YY?t5ќ4cڎWݑۊ]/KԴ: \Zُy'M D+űp&0n >aݏWF~rYݓM< S>!﶑cZiᫍ܇sfZK7aqPy,E 5 Y,u}vS3,fۀg`UyL0 oj1TŜGRj)vWi1= h 7ÁG !AzQz^ŧ\>dQ8U5ȥ; ,`Ax`DC`[^+m*ntN4 [V>-}w) hR-W@7Fs`)5' @l&AvF87 67~}vad۰V۬  pX?.P]Xtg&) ln k)43,L^qm"8Rl_͵4B^zk^c7[&nuai~tGi"R6j`8>D6[eMpen0 l1sɻk3|kƤUt >]:s~)zJڒ(I|@3:tfxpE Q yeҽPy}6>6ܦUom兪<|VwZqr0/6Q?J+x\ z̈́D17s3I\, lzK?v, P*QYr_ÒOC9(%V/S`KԣJ[=Z-M-F+s؅P( z˳砜X4({_,nv+ i'M9UIENDB`grape-1.5.2/README.md0000644000004100000410000032536014016744305014141 0ustar www-datawww-data![grape logo](grape.png) [![Gem Version](https://badge.fury.io/rb/grape.svg)](http://badge.fury.io/rb/grape) [![Build Status](https://github.com/ruby-grape/grape/workflows/test/badge.svg?branch=master)](https://github.com/ruby-grape/grape/actions) [![Code Climate](https://codeclimate.com/github/ruby-grape/grape.svg)](https://codeclimate.com/github/ruby-grape/grape) [![Coverage Status](https://coveralls.io/repos/github/ruby-grape/grape/badge.svg?branch=master)](https://coveralls.io/github/ruby-grape/grape?branch=master) [![Inline docs](https://inch-ci.org/github/ruby-grape/grape.svg)](https://inch-ci.org/github/ruby-grape/grape) [![Join the chat at https://gitter.im/ruby-grape/grape](https://badges.gitter.im/ruby-grape/grape.svg)](https://gitter.im/ruby-grape/grape?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Table of Contents - [What is Grape?](#what-is-grape) - [Stable Release](#stable-release) - [Project Resources](#project-resources) - [Grape for Enterprise](#grape-for-enterprise) - [Installation](#installation) - [Basic Usage](#basic-usage) - [Mounting](#mounting) - [All](#all) - [Rack](#rack) - [ActiveRecord without Rails](#activerecord-without-rails) - [Rails 4](#rails-4) - [Rails 5+](#rails-5) - [Alongside Sinatra (or other frameworks)](#alongside-sinatra-or-other-frameworks) - [Rails](#rails) - [Rails < 5.2](#rails--52) - [Rails 6.0](#rails-60) - [Modules](#modules) - [Remounting](#remounting) - [Mount Configuration](#mount-configuration) - [Versioning](#versioning) - [Path](#path) - [Header](#header) - [Accept-Version Header](#accept-version-header) - [Param](#param) - [Describing Methods](#describing-methods) - [Configuration](#configuration) - [Parameters](#parameters) - [Params Class](#params-class) - [Declared](#declared) - [Include Parent Namespaces](#include-parent-namespaces) - [Include Missing](#include-missing) - [Parameter Validation and Coercion](#parameter-validation-and-coercion) - [Supported Parameter Types](#supported-parameter-types) - [Integer/Fixnum and Coercions](#integerfixnum-and-coercions) - [Custom Types and Coercions](#custom-types-and-coercions) - [Multipart File Parameters](#multipart-file-parameters) - [First-Class JSON Types](#first-class-json-types) - [Multiple Allowed Types](#multiple-allowed-types) - [Validation of Nested Parameters](#validation-of-nested-parameters) - [Dependent Parameters](#dependent-parameters) - [Group Options](#group-options) - [Renaming](#renaming) - [Built-in Validators](#built-in-validators) - [allow_blank](#allow_blank) - [values](#values) - [except_values](#except_values) - [same_as](#same_as) - [regexp](#regexp) - [mutually_exclusive](#mutually_exclusive) - [exactly_one_of](#exactly_one_of) - [at_least_one_of](#at_least_one_of) - [all_or_none_of](#all_or_none_of) - [Nested mutually_exclusive, exactly_one_of, at_least_one_of, all_or_none_of](#nested-mutually_exclusive-exactly_one_of-at_least_one_of-all_or_none_of) - [Namespace Validation and Coercion](#namespace-validation-and-coercion) - [Custom Validators](#custom-validators) - [Validation Errors](#validation-errors) - [I18n](#i18n) - [Custom Validation messages](#custom-validation-messages) - [presence, allow_blank, values, regexp](#presence-allow_blank-values-regexp) - [same_as](#same_as-1) - [all_or_none_of](#all_or_none_of-1) - [mutually_exclusive](#mutually_exclusive-1) - [exactly_one_of](#exactly_one_of-1) - [at_least_one_of](#at_least_one_of-1) - [Coerce](#coerce) - [With Lambdas](#with-lambdas) - [Pass symbols for i18n translations](#pass-symbols-for-i18n-translations) - [Overriding Attribute Names](#overriding-attribute-names) - [With Default](#with-default) - [Headers](#headers) - [Request](#request) - [Header Case Handling](#header-case-handling) - [Response](#response) - [Routes](#routes) - [Helpers](#helpers) - [Path Helpers](#path-helpers) - [Parameter Documentation](#parameter-documentation) - [Cookies](#cookies) - [HTTP Status Code](#http-status-code) - [Redirecting](#redirecting) - [Recognizing Path](#recognizing-path) - [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) - [Rescuing exceptions inside namespaces](#rescuing-exceptions-inside-namespaces) - [Unrescuable Exceptions](#unrescuable-exceptions) - [Exceptions that should be rescued explicitly](#exceptions-that-should-be-rescued-explicitly) - [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) - [JSON and XML Processors](#json-and-xml-processors) - [RESTful Model Representations](#restful-model-representations) - [Grape Entities](#grape-entities) - [Hypermedia and Roar](#hypermedia-and-roar) - [Rabl](#rabl) - [Active Model Serializers](#active-model-serializers) - [Sending Raw or No Data](#sending-raw-or-no-data) - [Authentication](#authentication) - [Basic and Digest Auth](#basic-and-digest-auth) - [Register custom middleware for authentication](#register-custom-middleware-for-authentication) - [Describing and Inspecting an API](#describing-and-inspecting-an-api) - [Current Route and Endpoint](#current-route-and-endpoint) - [Before, After and Finally](#before-after-and-finally) - [Anchoring](#anchoring) - [Using Custom Middleware](#using-custom-middleware) - [Grape Middleware](#grape-middleware) - [Rails Middleware](#rails-middleware) - [Remote IP](#remote-ip) - [Writing Tests](#writing-tests) - [Writing Tests with Rack](#writing-tests-with-rack) - [RSpec](#rspec) - [Airborne](#airborne) - [MiniTest](#minitest) - [Writing Tests with Rails](#writing-tests-with-rails) - [RSpec](#rspec-1) - [MiniTest](#minitest-1) - [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) - [endpoint_run.grape](#endpoint_rungrape) - [endpoint_render.grape](#endpoint_rendergrape) - [endpoint_run_filters.grape](#endpoint_run_filtersgrape) - [endpoint_run_validators.grape](#endpoint_run_validatorsgrape) - [format_response.grape](#format_responsegrape) - [Monitoring Products](#monitoring-products) - [Contributing to Grape](#contributing-to-grape) - [Security](#security) - [License](#license) - [Copyright](#copyright) ## What is Grape? Grape is a REST-like API 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, 1.5.2. ## Project Resources * [Grape Website](http://www.ruby-grape.org) * [Documentation](http://www.rubydoc.info/gems/grape) * Need help? Try [Grape Google Group](http://groups.google.com/group/ruby-grape) or [Gitter](https://gitter.im/ruby-grape/grape) * [Follow us on Twitter](https://twitter.com/grapeframework) ## Grape for Enterprise Available as part of the Tidelift Subscription. The maintainers of Grape are working with Tidelift to deliver commercial support and maintenance. Save time, reduce risk, and improve code health, while paying the maintainers of Grape. Click [here](https://tidelift.com/subscription/request-a-demo?utm_source=rubygems-grape&utm_medium=referral&utm_campaign=enterprise) for more details. In 2020, we plan to use the money towards gathering Grape contributors for dinner in New York City. ## Installation Ruby 2.4 or newer is required. 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 ### All By default Grape will compile the routes on the first route, it is possible to pre-load routes using the `compile!` method. ```ruby Twitter::API.compile! ``` This can be added to your `config.ru` (if using rackup), `application.rb` (if using rails), or any file that loads your server. ### Rack The above sample creates a Rack application that can be run from a rackup `config.ru` file with `rackup`: ```ruby run Twitter::API ``` (With pre-loading you can use) ```ruby Twitter::API.compile! 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. #### Rails 4 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 ``` #### Rails 5+ Use [otr-activerecord](https://github.com/jhollinger/otr-activerecord) as follows: ```ruby use OTR::ActiveRecord::ConnectionManagement ``` ### 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 [Web, API] ``` Note that order of loading apps using `Rack::Cascade` matters. The grape application must be last if you want to raise custom 404 errors from grape (such as `error!('Not Found',404)`). If the grape application is not last and returns 404 or 405 response, [cascade utilizes that as a signal to try the next app](https://www.rubydoc.info/gems/rack/Rack/Cascade). This may lead to undesirable behavior showing the [wrong 404 page from the wrong app](https://github.com/ruby-grape/grape/issues/1515). ### 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 `config/routes`: ```ruby mount Twitter::API => '/' ``` #### Rails < 5.2 Modify `application.rb`: ```ruby config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] ``` See [below](#reloading-api-changes-in-development) for additional code that enables reloading of API changes in development. #### Rails 6.0 For Rails versions greater than 6.0.0.beta2, `Zeitwerk` autoloader is the default for CRuby. By default `Zeitwerk` inflects `api` as `Api` instead of `API`. To make our example work, you need to uncomment the lines at the bottom of `config/initializers/inflections.rb`, and add `API` as an acronym: ```ruby ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'API' end ``` ### 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 ``` Keep in mind such declarations as `before/after/rescue_from` must be placed before `mount` in a case where they should be inherited. ```ruby class Twitter::API < Grape::API before do header 'X-Base-Header', 'will be defined for all APIs that are mounted below' end mount Twitter::Users mount Twitter::Search end ``` ## Remounting You can mount the same endpoints in two different locations. ```ruby class Voting::API < Grape::API namespace 'votes' do get do # Your logic end post do # Your logic end end end class Post::API < Grape::API mount Voting::API end class Comment::API < Grape::API mount Voting::API end ``` Assuming that the post and comment endpoints are mounted in `/posts` and `/comments`, you should now be able to do `get /posts/votes`, `post /posts/votes`, `get /comments/votes` and `post /comments/votes`. ### Mount Configuration You can configure remountable endpoints to change how they behave according to where they are mounted. ```ruby class Voting::API < Grape::API namespace 'votes' do desc "Vote for your #{configuration[:votable]}" get do # Your logic end end end class Post::API < Grape::API mount Voting::API, with: { votable: 'posts' } end class Comment::API < Grape::API mount Voting::API, with: { votable: 'comments' } end ``` Note that if you're passing a hash as the first parameter to `mount`, you will need to explicitly put `()` around parameters: ```ruby # good mount({ ::Some::Api => '/some/api' }, with: { condition: true }) # bad mount ::Some::Api => '/some/api', with: { condition: true } ``` You can access `configuration` on the class (to use as dynamic attributes), inside blocks (like namespace) If you want logic happening given on an `configuration`, you can use the helper `given`. ```ruby class ConditionalEndpoint::API < Grape::API given configuration[:some_setting] do get 'mount_this_endpoint_conditionally' do configuration[:configurable_response] end end end ``` If you want a block of logic running every time an endpoint is mounted (within which you can access the `configuration` Hash) ```ruby class ConditionalEndpoint::API < Grape::API mounted do YourLogger.info "This API was mounted at: #{Time.now}" get configuration[:endpoint_name] do configuration[:configurable_response] end end end ``` More complex results can be achieved by using `mounted` as an expression within which the `configuration` is already evaluated as a Hash. ```ruby class ExpressionEndpointAPI < Grape::API get(mounted { configuration[:route_name] || 'default_name' }) do # some logic end end ``` ```ruby class BasicAPI < Grape::API desc 'Statuses index' do params: mounted { configuration[:entity] || API::Entities::Status }.documentation end params do requires :all, using: mounted { configuration[:entity] || API::Entities::Status }.documentation end get '/statuses' do statuses = Status.all type = current_user.admin? ? :full : :default present statuses, with: mounted { configuration[:entity] || API::Entities::Status }, type: type end end class V1 < Grape::API version 'v1' mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::Status } } end class V2 < Grape::API version 'v2' mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::V2::Status } } 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' ``` Currently, Grape only supports versioned media types in the following format: ``` vnd.vendor-and-or-resource-v1234+format ``` Basically all tokens between the final `-` and the `+` will be interpreted as the version. 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. ### 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 and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. ### 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. The description would be used by [grape-swagger][grape-swagger] to generate swagger compliant documentation. Note: Description block is only for documentation and won't affects API behavior. ```ruby desc 'Returns your public timeline.' do summary 'summary' detail 'more details' params API::Entities::Status.documentation success API::Entities::Entity failure [[401, 'Unauthorized', 'Entities::Error']] named 'My named route' headers XAuthToken: { description: 'Validates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } hidden false deprecated false is_array true nickname 'nickname' produces ['application/json'] consumes ['application/json'] tags ['tag1', 'tag2'] 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 * Other options can be found in [grape-swagger][grape-swagger] [grape-swagger]: https://github.com/ruby-grape/grape-swagger ## Configuration Use `Grape.configure` to set up global settings at load time. Currently the configurable settings are: * `param_builder`: Sets the [Parameter Builder](#parameters), defaults to `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`. To change a setting value make sure that at some point during load time the following code runs ```ruby Grape.configure do |config| config.setting = value end ``` For example, for the `param_builder`, the following code could run in an initializer: ```ruby Grape.configure do |config| config.param_builder = Grape::Extensions::Hashie::Mash::ParamBuilder end ``` You can also configure a single API: ```ruby API.configure do |config| config[key] = value end ``` This will be available inside the API with `configuration`, as if it were [mount configuration](#mount-configuration). ## 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. ### Params Class By default parameters are available as `ActiveSupport::HashWithIndifferentAccess`. This can be changed to, for example, Ruby `Hash` or `Hashie::Mash` for the entire API. ```ruby class API < Grape::API include Grape::Extensions::Hashie::Mash::ParamBuilder params do optional :color, type: String end get do params.color # instead of params[:color] end ``` The class can also be overridden on individual parameter blocks using `build_with` as follows. ```ruby params do build_with Grape::Extensions::Hash::ParamBuilder optional :color, type: String end ``` Or globally with the [Configuration](#configuration) `Grape.configure.param_builder`. In the example above, `params["color"]` will return `nil` since `params` is a plain `Hash`. Available parameter builders are `Grape::Extensions::Hash::ParamBuilder`, `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder` and `Grape::Extensions::Hashie::Mash::ParamBuilder`. ### Declared Grape allows you to access only the parameters that have been declared by your `params` block. It will: * Filter out the params that have been passed, but are not allowed. * Include any optional params that are declared but not passed. Consider the following API endpoint: ````ruby format :json post 'users/signup' do { 'declared_params' => declared(params) } end ```` If you do not specify any parameters, `declared` will return an empty hash. **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 parameters. ````ruby format :json params do optional :user, type: Hash do optional :first_name, type: String optional :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" } } } ```` Missing params that are declared as type `Hash` or `Array` will be included. ````ruby format :json params do optional :user, type: Hash do optional :first_name, type: String optional :last_name, type: String end optional :widgets, type: Array 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 '{}' ```` **Response** ````json { "declared_params": { "user": { "first_name": null, "last_name": null }, "widgets": [] } } ```` The returned hash is an `ActiveSupport::HashWithIndifferentAccess`. The `#declared` method is not available to `before` filters, as those are evaluated prior to parameter coercion. ### Include Parent Namespaces By default `declared(params)` includes parameters that were defined in all parent namespaces. If you want to return only parameters from your current namespace, you can set `include_parent_namespaces` option to `false`. ````ruby format :json namespace :parent do params do requires :parent_name, type: String end namespace ':parent_name' do params do requires :child_name, type: String end get ':child_name' do { 'without_parent_namespaces' => declared(params, include_parent_namespaces: false), 'with_parent_namespaces' => declared(params, include_parent_namespaces: true), } end end end ```` **Request** ````bash curl -X GET -H "Content-Type: application/json" localhost:9292/parent/foo/bar ```` **Response** ````json { "without_parent_namespaces": { "child_name": "bar" }, "with_parent_namespaces": { "parent_name": "foo", "child_name": "bar" }, } ```` ### Include Missing By default `declared(params)` includes parameters that have `nil` values. If you want to return only the parameters that are not `nil`, you can use the `include_missing` option. By default, `include_missing` is set to `true`. Consider the following API: ````ruby format :json params do requires :user, type: Hash do requires :first_name, type: String optional :last_name, type: String 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"}}' ```` **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[a-z]+\z/ group :media, type: Hash do requires :url end optional :audio, type: Hash 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 ``` 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. 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 (alias `File`) * JSON ### Integer/Fixnum and Coercions Please be aware that the behavior differs between Ruby 2.4 and earlier versions. In Ruby 2.4, values consisting of numbers are converted to Integer, but in earlier versions it will be treated as Fixnum. ```ruby params do requires :integers, type: Hash do requires :int, coerce: Integer end end get '/int' do params[:integers][:int].class end ... get '/int' integers: { int: '45' } #=> Integer in ruby 2.4 #=> Fixnum in earlier ruby versions ``` ### Custom Types and Coercions Aside from the default set of supported types listed above, any class can be used as a type as long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct type, or return an instance of `Grape::Types::InvalidValue` which optionally accepts a message to be returned in the response. ```ruby class Color attr_reader :value def initialize(color) @value = color end def self.parse(value) return new(value) if %w[blue red green]).include?(value) Grape::Types::InvalidValue.new('Unsupported color') end end params do requires :color, type: Color, default: Color.new('blue') requires :more_colors, type: Array[Color] # Collections work optional :unique_colors, type: Set[Color] # Duplicates discarded end get '/stuff' do # params[:color] is already a Color. params[:color].value end ``` Alternatively, a custom coercion method may be supplied for any type of parameter using `coerce_with`. Any class or object may be given that implements a `parse` or `call` method, in that order of precedence. The method must accept a single string parameter, and the return value must match the given `type`. ```ruby params do requires :passwd, type: String, coerce_with: Base64.method(:decode64) requires :loud_color, type: Color, coerce_with: ->(c) { Color.parse(c.downcase) } requires :obj, type: Hash, coerce_with: JSON do requires :words, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) } optional :time, type: Time, coerce_with: Chronic end end ``` Example of use of `coerce_with` with a lambda (a class with a `parse` method could also have been used) It will parse a string and return an Array of Integers, matching the `Array[Integer]` `type`. ```ruby params do requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } end ``` Grape will assert that coerced values match the given `type`, and will reject the request if they do not. To override this behaviour, custom types may implement a `parsed?` method that should accept a single argument and return `true` if the value passes type validation. ```ruby class SecureUri def self.parse(value) URI.parse value end def self.parsed?(value) value.is_a? URI::HTTPS end end params do requires :secure_uri, type: SecureUri end ``` ### Multipart File Parameters Grape makes use of `Rack::Request`'s built-in support for multipart file parameters. Such parameters can be declared with `type: File`: ```ruby params do requires :avatar, type: File end post '/' do params[:avatar][:filename] # => 'avatar.png' params[:avatar][:type] # => 'image/png' params[:avatar][:tempfile] # => # end ``` ### First-Class `JSON` Types Grape supports complex parameters given as JSON-formatted strings using the special `type: JSON` declaration. JSON objects and arrays of objects are accepted equally, with nested validation rules applied to all objects in either case: ```ruby params do requires :json, type: JSON do requires :int, type: Integer, values: [1, 2, 3] end end get '/' do params[:json].inspect end client.get('/', json: '{"int":1}') # => "{:int=>1}" client.get('/', json: '[{"int":"1"}]') # => "[{:int=>1}]" client.get('/', json: '{"int":4}') # => HTTP 400 client.get('/', json: '[{"int":4}]') # => HTTP 400 ``` Additionally `type: Array[JSON]` may be used, which explicitly marks the parameter as an array of objects. If a single object is supplied it will be wrapped. ```ruby params do requires :json, type: Array[JSON] do requires :int, type: Integer end end get '/' do params[:json].each { |obj| ... } # always works end ``` For stricter control over the type of JSON structure which may be supplied, use `type: Array, coerce_with: JSON` or `type: Hash, coerce_with: JSON`. ### Multiple Allowed Types Variant-type parameters can be declared using the `types` option rather than `type`: ```ruby params do requires :status_code, types: [Integer, String, Array[Integer, String]] end get '/' do params[:status_code].inspect end client.get('/', status_code: 'OK_GOOD') # => "OK_GOOD" client.get('/', status_code: 300) # => 300 client.get('/', status_code: %w(404 NOT FOUND)) # => [404, "NOT", "FOUND"] ``` As a special case, variant-member-type collections may also be declared, by passing a `Set` or `Array` with more than one member to `type`: ```ruby params do requires :status_codes, type: Array[Integer,String] end get '/' do params[:status_codes].inspect end client.get('/', status_codes: %w(1 two)) # => [1, "two"] ``` ### Validation of Nested Parameters Parameters can be nested using `group` or by calling `requires` or `optional` with a block. In the [above example](#parameter-validation-and-coercion), 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 ``` In the example above Grape will use `blank?` to check whether the `shelf_id` param is present. `given` also takes a `Proc` with custom code. Below, the param `description` is required only if the value of `category` is equal `foo`: ```ruby params do optional :category given category: ->(val) { val == 'foo' } do requires :description end end ``` You can rename parameters: ```ruby params do optional :category, as: :type given type: ->(val) { val == 'foo' } do requires :description end end ``` Note: param in `given` should be the renamed one. In the example, it should be `type`, not `category`. ### Group Options Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters. The example below presents a typical case when parameters share common options. ```ruby params do requires :first_name, type: String, regexp: /w+/, desc: 'First name' requires :middle_name, type: String, regexp: /w+/, desc: 'Middle name' requires :last_name, type: String, regexp: /w+/, desc: 'Last name' end ``` Grape allows you to present the same logic through the `with` method in your parameters block, like so: ```ruby params do with(type: String, regexp: /w+/) do requires :first_name, desc: 'First name' requires :middle_name, desc: 'Middle name' requires :last_name, desc: 'Last name' end end ``` ### Renaming You can rename parameters using `as`, which can be useful when refactoring existing APIs: ```ruby resource :users do params do requires :email_address, as: :email requires :password end post do User.create!(declared(params)) # User takes email and password end end ``` The value passed to `as` will be the key when calling `params` or `declared(params)`. ### 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. ```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 supply 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. If the Proc has arity zero (i.e. it takes no arguments) it is expected to return either a list or a range which will then be used to validate the parameter. 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 ``` Alternatively, a Proc with arity one (i.e. taking one argument) can be used to explicitly validate each parameter value. In that case, the Proc is expected to return a truthy value if the parameter value is valid. The parameter will be considered invalid if the Proc returns a falsy value or if it raises a StandardError. ```ruby params do requires :number, type: Integer, values: ->(v) { v.even? && v < 25 } end ``` While Procs are convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once. #### `except_values` Parameters can be restricted from having a specific set of values with the `:except_values` option. The `except_values` validator behaves similarly to the `values` validator in that it accepts either an Array, a Range, or a Proc. Unlike the `values` validator, however, `except_values` only accepts Procs with arity zero. ```ruby params do requires :browser, except_values: [ 'ie6', 'ie7', 'ie8' ] requires :port, except_values: { value: 0..1024, message: 'is not allowed' } requires :hashtag, except_values: -> { Hashtag.FORBIDDEN_LIST } end ``` #### `same_as` A `same_as` option can be given to ensure that values of parameters match. ```ruby params do requires :password requires :password_confirmation, same_as: :password 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 ``` Note that using `:default` with `mutually_exclusive` will cause multiple parameters to always have a default value and raise a `Grape::Exceptions::Validation` mutually exclusive exception. #### `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, type: Hash do optional :meat optional :fish optional :rice at_least_one_of :meat, :fish, :rice end group :drink, type: Hash do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end optional :dessert, type: Hash do optional :cake optional :icecream mutually_exclusive :cake, :icecream end optional :recipe, type: Hash 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 ``` You can also define a route parameter type by passing to `route_param`'s options. ```ruby namespace :arithmetic do route_param :n, type: Integer do desc 'Returns in power' get 'power' do params[:n] ** params[:n] end end end ``` ### Custom Validators ```ruby class AlphaNumeric < Grape::Validations::Base def validate_param!(attr_name, params) unless params[attr_name] =~ /\A[[:alnum:]]+\z/ 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 ``` You can also create custom validation that use request to validate the attribute. For example if you want to have parameters that are available to only admins, you can do the following. ```ruby class Admin < Grape::Validations::Base def validate(request) # return if the param we are checking was not in request # @attrs is a list containing the attribute we are currently validating # in our sample case this method once will get called with # @attrs being [:admin_field] and once with @attrs being [:admin_false_field] return unless request.params.key?(@attrs.first) # check if admin flag is set to true return unless @option # check if user is admin or not # as an example get a token from request and check if it's admin or not fail Grape::Exceptions::Validation, params: @attrs, message: 'Can not set admin-only field.' unless request.headers['X-Access-Token'] == 'admin' end end ``` And use it in your endpoint definition as: ```ruby params do optional :admin_field, type: String, admin: true optional :non_admin_field, type: String optional :admin_false_field, type: String, admin: false end ``` Every validation will have its own instance of the validator, which means that the validator can have a state. ### 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 ``` Grape returns all validation and coercion errors found by default. To skip all subsequent validation checks when a specific param is found invalid, use `fail_fast: true`. The following example will not check if `:wine` is present unless it finds `:beer`. ```ruby params do required :beer, fail_fast: true required :wine end ``` The result of empty params would be a single `Grape::Exceptions::ValidationErrors` error. Similarly, no regular expression test will be performed if `:blah` is blank in the following example. ```ruby params do required :blah, allow_blank: false, regexp: /blah/, fail_fast: true 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. In case your app enforces available locales only and :en is not included in your available locales, Grape cannot fall back to English and will return the translation key for the error message. To avoid this behaviour, either provide a translation for your default locale or add :en to your available locales. ### Custom Validation messages Grape supports custom validation messages for parameter-related and coerce-related error messages. #### `presence`, `allow_blank`, `values`, `regexp` ```ruby params do requires :name, values: { value: 1..10, message: 'not in range from 1 to 10' }, allow_blank: { value: false, message: 'cannot be blank' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' }, message: 'is required' end ``` #### `same_as` ```ruby params do requires :password requires :password_confirmation, same_as: { value: :password, message: 'not match' } end ``` #### `all_or_none_of` ```ruby params do optional :beer optional :wine optional :juice all_or_none_of :beer, :wine, :juice, message: "all params are required or none is required" end ``` #### `mutually_exclusive` ```ruby params do optional :beer optional :wine optional :juice mutually_exclusive :beer, :wine, :juice, message: "are mutually exclusive cannot pass both params" end ``` #### `exactly_one_of` ```ruby params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice, message: { exactly_one: "are missing, exactly one parameter is required", mutual_exclusion: "are mutually exclusive, exactly one parameter is required" } end ``` #### `at_least_one_of` ```ruby params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice, message: "are missing, please specify at least one param" end ``` #### `Coerce` ```ruby params do requires :int, type: { value: Integer, message: "type cast is invalid" } end ``` #### `With Lambdas` ```ruby params do requires :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' } end ``` #### `Pass symbols for i18n translations` You can pass a symbol if you want i18n translations for your custom validation messages. ```ruby params do requires :name, message: :name_required end ``` ```ruby # en.yml en: grape: errors: format: ! '%{attributes} %{message}' messages: name_required: 'must be present' ``` #### Overriding Attribute Names You can also override attribute names. ```ruby # en.yml en: grape: errors: format: ! '%{attributes} %{message}' messages: name_required: 'must be present' attributes: name: 'Oops! Name' ``` Will produce 'Oops! Name must be present' #### With Default You cannot set a custom message option for Default as it requires interpolation `%{option1}: %{value1} is incompatible with %{option2}: %{value2}`. You can change the default error message for Default by changing the `incompatible_option_values` message key inside [en.yml](lib/grape/locale/en.yml) ```ruby params do requires :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' }, default: 5 end ``` ## Headers ### Request 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 ``` #### Header Case Handling The above example may have been requested as follows: ``` shell curl -H "secret_PassWord: swordfish" ... ``` The header name will have been normalized for you. - In the `header` helper names will be coerced into a capitalized kebab case. - In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_'. The header name will have been normalized per HTTP standards defined in [RFC2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) regardless of what is being sent by a client. ### Response 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. Additional headers will be merged with headers set before `error!` call. ```ruby error! 'Unauthorized', 401, 'X-Error-Detail' => 'Invalid token.' ``` ## Routes To define routes you can use the `route` method or the shorthands for the HTTP verbs. To define a route that accepts any route set to `:any`. Parts of the path that are denoted with a colon will be interpreted as route parameters. ```ruby route :get, 'status' do end # is the same as get 'status' do end # is the same as get :status do end # is NOT the same as get ':status' do # this makes params[:status] available end # This will make both params[:status_id] and params[:id] available get 'statuses/:status_id/reviews/:id' do end ``` To declare a namespace that prefixes all routes within, use the `namespace` method. `group`, `resource`, `resources` and `segment` are aliases to this method. Any endpoints within will share their parent context as well as any configuration done in the namespace context. The `route_param` method is a convenient method for defining a parameter route segment. If you define a type, it will add a validation for this parameter. ```ruby route_param :id, type: Integer do get 'status' do end end # is the same as namespace ':id' do params do requires :id, type: Integer end get 'status' do end end ``` 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 an array of modules. ```ruby module StatusHelpers def user_info(user) "#{user} has statused #{user.statuses} status(s)" end end module HttpCodesHelpers def unauthorized 401 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 an array of modules helpers StatusHelpers, HttpCodesHelpers before do error!('Access Denied', unauthorized) unless current_user end 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: '/' ``` ## HTTP Status Code By default Grape returns a 201 for `POST`-Requests, 204 for `DELETE`-Requests that don't return any content, and 200 status code for all other 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 ``` ## Redirecting You can redirect to a new url temporarily (302) or permanently (301). ```ruby redirect '/statuses' ``` ```ruby redirect '/statuses', permanent: true ``` ## Recognizing Path You can recognize the endpoint matched with given path. This API returns an instance of `Grape::Endpoint`. ```ruby class API < Grape::API get '/statuses' do end end API.recognize_path '/statuses' ``` ## 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. If the resource has `before` and `after` callbacks they will be executed, but no other callbacks will run. ```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. If the resource has `before` callbacks they will be executed, but no other callbacks will run. ``` 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 ``` Anything that responds to `#to_s` can be given as a first argument to `error!`. ```ruby error! :not_found, 404 ``` 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 set additional headers for the response. They will be merged with headers set before `error!` call. ```ruby error!('Something went wrong', 500, 'X-Error-Detail' => 'Invalid token.') ``` 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. ```ruby 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 `StandardError` exceptions and return them in the API format. ```ruby class Twitter::API < Grape::API rescue_from :all end ``` This mimics [default `rescue` behaviour](https://ruby-doc.org/core/StandardError.html) when an exception type is not provided. Any other exception should be rescued explicitly, see [below](#exceptions-that-should-be-rescued-explicitly). Grape can also rescue from all exceptions and still use the built-in exception handing. This will give the same behavior as `rescue_from :all` with the addition that Grape will use the exception handling defined by all Exception classes that inherit `Grape::Exceptions::Base`. The intent of this setting is to provide a simple way to cover the most common exceptions and return any unexpected exceptions in the API format. ```ruby class Twitter::API < Grape::API rescue_from :grape_exceptions 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```. Notice that you could combine these two approaches (rescuing custom errors takes precedence). For example, it's useful for handling all exceptions except Grape validation errors. ```ruby class Twitter::API < Grape::API rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e, 400) end rescue_from :all end ``` 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, ->(message, backtrace, options, env, original_exception) { "error: #{message} from #{backtrace}" } end ``` You can also use a module or class. ```ruby module CustomFormatter def self.call(message, backtrace, options, env, original_exception) { 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 all 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' }) end end ``` Or rescue specific exceptions. ```ruby class Twitter::API < Grape::API rescue_from ArgumentError do |e| error!("ArgumentError: #{e.message}") end rescue_from NoMethodError do |e| error!("NoMethodError: #{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 ``` Helpers are also available inside `rescue_from`. ```ruby class Twitter::API < Grape::API format :json helpers do def server_error! error!({ error: 'Server error.' }, 500, { 'Content-Type' => 'text/error' }) end end rescue_from :all do |e| server_error! end end ``` The `rescue_from` handler must return a `Rack::Response` object, call `error!`, or raise an exception (either the original exception or another custom one). The exception raised in `rescue_from` will be handled outside Grape. For example, if you mount Grape in Rails, the exception will be handle by [Rails Action Controller](https://guides.rubyonrails.org/action_controller_overview.html#rescue). Alternately, use the `with` option in `rescue_from` to specify a method or a `proc`. ```ruby class Twitter::API < Grape::API format :json helpers do def server_error! error!({ error: 'Server error.' }, 500, { 'Content-Type' => 'text/error' }) end end rescue_from :all, with: :server_error! rescue_from ArgumentError, with: -> { Rack::Response.new('rescued with a method', 400) } end ``` Inside the `rescue_from` block, the environment of the original controller method(`.self` receiver) is accessible through the `#context` method. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| user_id = context.params[:user_id] error!("error for #{user_id}") end end ``` #### Rescuing exceptions inside namespaces You could put `rescue_from` clauses inside a namespace and they will take precedence over ones defined in the root scope: ```ruby class Twitter::API < Grape::API rescue_from ArgumentError do |e| error!("outer") end namespace :statuses do rescue_from ArgumentError do |e| error!("inner") end get do raise ArgumentError.new end end end ``` Here `'inner'` will be result of handling occurred `ArgumentError`. #### Unrescuable Exceptions `Grape::Exceptions::InvalidVersionHeader`, which is raised when the version in the request header doesn't match the currently evaluated version for the endpoint, will _never_ be rescued from a `rescue_from` block (even a `rescue_from :all`) This is because Grape relies on Rack to catch that error and try the next versioned-route for cases where there exist identical Grape endpoints with different versions. #### Exceptions that should be rescued explicitly Any exception that is not subclass of `StandardError` should be rescued explicitly. Usually it is not a case for an application logic as such errors point to problems in Ruby runtime. This is following [standard recommendations for exceptions handling](https://ruby-doc.org/core/Exception.html). ### Rails 3.x When mounted inside containers, such as Rails 3.x, errors such as "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 ``` To change the logger level. ```ruby class API < Grape::API self.logger.level = Logger::INFO 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) or [grape-middleware-logger](https://github.com/ridiculous/grape-middleware-logger) gems. ## 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''#{CGI.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, ->(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" If a body is present in a request to an API, with a Content-Type header value that is of an unsupported type a "415 Unsupported Media Type" error code will be returned by Grape. Response statuses that indicate no content as defined by [Rack](https://github.com/rack) [here](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) will bypass serialization and the body entity - though there should be none - will not be modified. ### 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']`. ## JSON and XML Processors Grape uses `JSON` and `ActiveSupport::XmlMini` for JSON and XML parsing by default. It also detects and supports [multi_json](https://github.com/intridea/multi_json) and [multi_xml](https://github.com/sferik/multi_xml). Adding those gems to your Gemfile and requiring them will enable them and allow you to swap the JSON and XML back-ends. ## 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: ->(status, options) { status.user.public? } expose :digest do |status, options| Digest::MD5.hexdigest(status.txt) end 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/ruby-grape/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/ruby-grape/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 with `sendfile`. This works with the [Rack::Sendfile](https://www.rubydoc.info/gems/rack/Rack/Sendfile) middleware to optimally send the file through your web server software. ```ruby class API < Grape::API get '/' do sendfile '/path/to/file' end end ``` To stream a file in chunks use `stream` ```ruby class API < Grape::API get '/' do stream '/path/to/file' end end ``` If you want to stream non-file data use the `stream` method and a `Stream` object. This is an object that responds to `each` and yields for each chunk to send to the client. Each chunk will be sent as it is yielded instead of waiting for all of the content to be available. ```ruby class MyStream def each yield 'part 1' yield 'part 2' yield 'part 3' end end class API < Grape::API get '/' do stream MyStream.new 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 # IMPORTANT: make sure you use a comparison method which isn't prone to a timing attack end ``` ```ruby http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username| # lookup the user's password here 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 Parameter 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 [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) for OAuth2 support. You can access the controller params, headers, and helpers through the context with the `#context` method inside any auth middleware inherited from `Grape::Middleware::Auth::Base`. ## 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].version # => 'v1' TwitterAPI::routes[0].description # => 'Includes custom settings.' TwitterAPI::routes[0].settings[:custom] # => { key: 'value' } ``` Note that `Route#route_xyz` methods have been deprecated since 0.15.0. Please use `Route#xyz` instead. Note that difference of `Route#options` and `Route#settings`. The `options` can be referred from your route, it should be set by specifing key and value on verb methods such as `get`, `post` and `put`. The `settings` can also be referred from your route, but it should be set by specifing key and value on `route_setting`. ## 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, After and Finally Blocks can be executed before or after every API call, using `before`, `after`, `before_validation` and `after_validation`. If the API fails the `after` call will not be triggered, if you need code to execute for sure use the `finally`. Before and after callbacks execute in the following order: 1. `before` 2. `before_validation` 3. _validations_ 4. `after_validation` (upon successful validation) 5. _the API call_ (upon successful validation) 6. `after` (upon successful validation and API call) 7. `finally` (always) Steps 4, 5 and 6 only happen if validation succeeds. If a request for a resource is made with an unsupported HTTP method (returning HTTP 405) only `before` callbacks will be executed. The remaining callbacks will be bypassed. If a request for a resource is made that triggers the built-in `OPTIONS` handler, only `before` and `after` callbacks will be executed. The remaining callbacks will be bypassed. For example, using a simple `before` block to set a header. ```ruby before do header 'X-Robots-Tag', 'noindex' end ``` You can ensure a block of code runs after every request (including failures) with `finally`: ```ruby finally do # this code will run after every request (successful or failed) end ``` **Namespaces** Callbacks apply to each 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 behavior is then: ```bash GET / # 'root - ' GET /foo # 'root - foo - blah' GET /foo/bar # 'root - foo - bar - blah' ``` Params on a `namespace` (or whichever alias you are using) will also be available 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 behavior is then: ```bash GET /123 # 'Integer' GET /foo # 400 error - 'blah is invalid' ``` **Versioning** 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 behavior is then: ```bash GET /foo/v1 # 'v1-hello' GET /foo/v2 # 'v2-hello' ``` **Altering Responses** Using `present` in any callback allows you to add data to a response: ```ruby class MyAPI < Grape::API format :json after_validation do present :name, params[:name] if params[:name] end get '/greeting' do present :greeting, 'Hello!' end end ``` The behavior is then: ```bash GET /greeting # {"greeting":"Hello!"} GET /greeting?name=Alan # {"name":"Alan","greeting":"Hello!"} ``` Instead of altering a response, you can also terminate and rewrite it from any callback using `error!`, including `after`. This will cause all subsequent steps in the process to not be called. **This includes the actual api call and any callbacks** ## 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 ### Grape Middleware You can make a custom middleware by using `Grape::Middleware::Base`. It's inherited from some grape official middlewares in fact. For example, you can write a middleware to log application exception. ```ruby class LoggingError < Grape::Middleware::Base def after return unless @app_response && @app_response[0] == 500 env['rack.logger'].error("Raised error on #{env['PATH_INFO']}") end end ``` Your middleware can overwrite application response as follows, except error case. ```ruby class Overwriter < Grape::Middleware::Base def after [200, { 'Content-Type' => 'text/plain' }, ['Overwritten.']] end end ``` You can add your custom middleware with `use`, that push the middleware onto the stack, and you can also control where the middleware is inserted using `insert`, `insert_before` and `insert_after`. ```ruby class CustomOverwriter < Grape::Middleware::Base def after [200, { 'Content-Type' => 'text/plain' }, [@options[:message]]] end end class API < Grape::API use Overwriter insert_before Overwriter, CustomOverwriter, message: 'Overwritten again.' insert 0, CustomOverwriter, message: 'Overwrites all other middleware.' get '/' do end end ``` You can access the controller params, headers, and helpers through the context with the `#context` method inside any middleware inherited from `Grape::Middleware::Base`. ### 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 context '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 context '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 ``` There's no standard way of sending arrays of objects via an HTTP GET, so POST JSON data and specify the correct content-type. ```ruby describe Twitter::API do context 'POST /api/statuses' do it 'creates many statuses' do statuses = [{ text: '...' }, { text: '...'}] post '/api/statuses', statuses.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq 201 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 context '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 context '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 context '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/rails_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 'stubs 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 ``` For Rails >= 5.1.4, change this: ```ruby ActionDispatch::Callbacks.to_prepare do api_reloader.execute_if_updated end ``` to this: ```ruby ActiveSupport::Reloader.to_prepare do api_reloader.execute_if_updated 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) #### endpoint_run_validators.grape The execution of validators. * *endpoint* - The endpoint instance * *validators* - The validators being executed * *request* - The request being validated #### format_response.grape Serialization or template rendering. * *env* - The request environment * *formatter* - The formatter object (e.g., `Grape::Formatter::Json`) 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 following third-party tools: * **New Relic** - [built-in support](https://docs.newrelic.com/docs/agents/ruby-agent/frameworks/grape-instrumentation) from v3.10.0 of the official [newrelic_rpm](https://github.com/newrelic/rpm) gem, also [newrelic-grape](https://github.com/xinminlabs/newrelic-grape) gem * **Librato Metrics** - [grape-librato](https://github.com/seanmoon/grape-librato) gem * **[Skylight](https://www.skylight.io/)** - [skylight](https://github.com/skylightio/skylight-ruby) gem, [documentation](https://docs.skylight.io/grape/) * **[AppSignal](https://www.appsignal.com)** - [appsignal-ruby](https://github.com/appsignal/appsignal-ruby) gem, [documentation](http://docs.appsignal.com/getting-started/supported-frameworks.html#grape) * **[ElasticAPM](https://www.elastic.co/products/apm)** - [elastic-apm](https://github.com/elastic/apm-agent-ruby) gem, [documentation](https://www.elastic.co/guide/en/apm/agent/ruby/3.x/getting-started-rack.html#getting-started-grape) ## 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). ## Security See [SECURITY](SECURITY.md) for details. ## License MIT License. See [LICENSE](LICENSE) for details. ## Copyright Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors. grape-1.5.2/spec/0000755000004100000410000000000014016744305013603 5ustar www-datawww-datagrape-1.5.2/spec/grape/0000755000004100000410000000000014016744305014701 5ustar www-datawww-datagrape-1.5.2/spec/grape/request_spec.rb0000644000004100000410000000667014016744305017741 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape describe Request do let(:default_method) { 'GET' } let(:default_params) { {} } let(:default_options) do { method: method, params: params } end let(:default_env) do Rack::MockRequest.env_for('/', options) end let(:method) { default_method } let(:params) { default_params } let(:options) { default_options } let(:env) { default_env } let(:request) do Grape::Request.new(env) end describe '#params' do let(:params) do { a: '123', b: 'xyz' } end it 'by default returns stringified parameter keys' do expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz')) end context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do let(:request) do Grape::Request.new(env, build_params_with: Grape::Extensions::Hash::ParamBuilder) end it 'returns symbolized params' do expect(request.params).to eq(a: '123', b: 'xyz') end end describe 'with grape.routing_args' do let(:options) do default_options.merge('grape.routing_args' => routing_args) end let(:routing_args) do { version: '123', route_info: '456', c: 'ccc' } end it 'cuts version and route_info' do expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) end end end describe 'when the param_builder is set to Hashie' do before do Grape.configure do |config| config.param_builder = Grape::Extensions::Hashie::Mash::ParamBuilder end end after do Grape.config.reset end subject(:request_params) { Grape::Request.new(env, **opts).params } context 'when the API does not include a specific param builder' do let(:opts) { {} } it { is_expected.to be_a(Hashie::Mash) } end context 'when the API includes a specific param builder' do let(:opts) { { build_params_with: Grape::Extensions::Hash::ParamBuilder } } it { is_expected.to be_a(Hash) } end end describe '#headers' do let(:options) do default_options.merge(request_headers) end describe 'with http headers in env' do let(:request_headers) do { 'HTTP_X_GRAPE_IS_COOL' => 'yeah' } end it 'cuts HTTP_ prefix and capitalizes header name words' do expect(request.headers).to eq('X-Grape-Is-Cool' => 'yeah') end end describe 'with non-HTTP_* stuff in env' do let(:request_headers) do { 'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape' } end it 'does not include them' do expect(request.headers).to eq({}) end end describe 'with symbolic header names' do let(:request_headers) do { HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true' } end let(:env) do default_env.merge(request_headers) end it 'converts them to string' do expect(request.headers).to eq('Grape-Likes-Symbolic' => 'it is true') end end end end end grape-1.5.2/spec/grape/api_spec.rb0000644000004100000410000034644314016744305017027 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'shared/versioning_examples' describe Grape::API do subject { Class.new(Grape::API) } def app subject end describe '.prefix' do it 'routes root through with the prefix' do subject.prefix 'awesome/sauce' subject.get do 'Hello there.' end get 'awesome/sauce/' expect(last_response.status).to eql 200 expect(last_response.body).to eql 'Hello there.' end it 'routes through with the prefix' do subject.prefix 'awesome/sauce' subject.get :hello do 'Hello there.' end get 'awesome/sauce/hello' expect(last_response.body).to eql 'Hello there.' get '/hello' expect(last_response.status).to eql 404 end it 'supports OPTIONS' do subject.prefix 'awesome/sauce' subject.get do 'Hello there.' end options 'awesome/sauce' expect(last_response.status).to eql 204 expect(last_response.body).to be_blank end it 'disallows POST' do subject.prefix 'awesome/sauce' subject.get post 'awesome/sauce' expect(last_response.status).to eql 405 end end describe '.version' do context 'when defined' do it 'returns version value' do subject.version 'v1' expect(subject.version).to eq('v1') end end context 'when not defined' do it 'returns nil' do expect(subject.version).to be_nil end end end describe '.version using path' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :path } end end end describe '.version using param' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :param, parameter: 'apiver' } end end end describe '.version using header' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :header, vendor: 'mycompany', format: 'json' } end end # Behavior as defined by rfc2616 when no header is defined # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html describe 'no specified accept header' do # subject.version 'v1', using: :header # subject.get '/hello' do # 'hello' # end # it 'routes' do # get '/hello' # last_response.status.should eql 200 # end end # pending 'routes if any media type is allowed' end describe '.version using accept_version_header' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :accept_version_header } end end end describe '.represent' do it 'requires a :with option' do expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent) end it 'adds the association to the :representations setting' do klass = Class.new subject.represent Object, with: klass expect(subject.namespace_stackable_with_hash(:representations)[Object]).to eq(klass) end end describe '.namespace' do it 'is retrievable and converted to a path' do internal_namespace = nil subject.namespace :awesome do internal_namespace = namespace end expect(internal_namespace).to eql('/awesome') end it 'comes after the prefix and version' do subject.prefix :rad subject.version 'v1', using: :path subject.namespace :awesome do get('/hello') { 'worked' } end get '/rad/v1/awesome/hello' expect(last_response.body).to eq('worked') end it 'cancels itself after the block is over' do internal_namespace = nil subject.namespace :awesome do internal_namespace = namespace end expect(subject.namespace).to eql('/') end it 'is stackable' do internal_namespace = nil internal_second_namespace = nil subject.namespace :awesome do internal_namespace = namespace namespace :rad do internal_second_namespace = namespace end end expect(internal_namespace).to eq('/awesome') expect(internal_second_namespace).to eq('/awesome/rad') end it 'accepts path segments correctly' do inner_namespace = nil subject.namespace :members do namespace '/:member_id' do inner_namespace = namespace get '/' do params[:member_id] end end end get '/members/23' expect(last_response.body).to eq('23') expect(inner_namespace).to eq('/members/:member_id') end it 'is callable with nil just to push onto the stack' do subject.namespace do version 'v2', using: :path get('/hello') { 'inner' } end subject.get('/hello') { 'outer' } get '/v2/hello' expect(last_response.body).to eq('inner') get '/hello' expect(last_response.body).to eq('outer') end %w[group resource resources segment].each do |als| it "`.#{als}` is an alias" do inner_namespace = nil subject.send(als, :awesome) do inner_namespace = namespace end expect(inner_namespace).to eq '/awesome' end end end describe '.call' do context 'it does not add to the app setup' do it 'calls the app' do expect(subject).not_to receive(:add_setup) subject.call({}) end end end describe '.route_param' do it 'adds a parameterized route segment namespace' do subject.namespace :users do route_param :id do get do params[:id] end end end get '/users/23' expect(last_response.body).to eq('23') end it 'defines requirements with a single hash' do subject.namespace :users do route_param :id, requirements: /[0-9]+/ do get do params[:id] end end end get '/users/michael' expect(last_response.status).to eq(404) get '/users/23' expect(last_response.status).to eq(200) end context 'with param type definitions' do it 'is used by passing to options' do subject.namespace :route_param do route_param :foo, type: Integer do get { params.to_json } end end get '/route_param/1234' expect(last_response.body).to eq('{"foo":1234}') end end end describe '.route' do it 'allows for no path' do subject.namespace :votes do get do 'Votes' end post do 'Created a Vote' end end get '/votes' expect(last_response.body).to eql 'Votes' post '/votes' expect(last_response.body).to eql 'Created a Vote' end it 'handles empty calls' do subject.get '/' get '/' expect(last_response.body).to eql '' end describe 'root routes should work with' do before do subject.format :txt subject.content_type :json, 'application/json' subject.formatter :json, ->(object, _env) { object } def subject.enable_root_route! get('/') { 'root' } end end after do expect(last_response.body).to eql 'root' end describe 'path versioned APIs' do before do subject.version version, using: :path subject.enable_root_route! end context 'when a single version provided' do let(:version) { 'v1' } it 'without a format' do versioned_get '/', 'v1', using: :path end it 'with a format' do get '/v1/.json' end end context 'when array of versions provided' do let(:version) { %w[v1 v2] } it { versioned_get '/', 'v1', using: :path } it { versioned_get '/', 'v2', using: :path } end end it 'header versioned APIs' do subject.version 'v1', using: :header, vendor: 'test' subject.enable_root_route! versioned_get '/', 'v1', using: :header, vendor: 'test' end it 'header versioned APIs with multiple headers' do subject.version %w[v1 v2], using: :header, vendor: 'test' subject.enable_root_route! versioned_get '/', 'v1', using: :header, vendor: 'test' versioned_get '/', 'v2', using: :header, vendor: 'test' end it 'param versioned APIs' do subject.version 'v1', using: :param subject.enable_root_route! versioned_get '/', 'v1', using: :param end it 'Accept-Version header versioned APIs' do subject.version 'v1', using: :accept_version_header subject.enable_root_route! versioned_get '/', 'v1', using: :accept_version_header end it 'unversioned APIs' do subject.enable_root_route! get '/' end end it 'allows for multiple paths' do subject.get(['/abc', '/def']) do 'foo' end get '/abc' expect(last_response.body).to eql 'foo' get '/def' expect(last_response.body).to eql 'foo' end context 'format' do module ApiSpec class DummyFormatClass end end before(:each) do allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_json).and_return('abc') allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_txt).and_return('def') subject.get('/abc') do ApiSpec::DummyFormatClass.new end end it 'allows .json' do get '/abc.json' expect(last_response.status).to eq(200) expect(last_response.body).to eql 'abc' # json-encoded symbol end it 'allows .txt' do get '/abc.txt' expect(last_response.status).to eq(200) expect(last_response.body).to eql 'def' # raw text end end it 'allows for format without corrupting a param' do subject.get('/:id') do { 'id' => params[:id] } end get '/awesome.json' expect(last_response.body).to eql '{"id":"awesome"}' end it 'allows for format in namespace with no path' do subject.namespace :abc do get do ['json'] end end get '/abc.json' expect(last_response.body).to eql '["json"]' end it 'allows for multiple verbs' do subject.route(%i[get post], '/abc') do 'hiya' end subject.endpoints.first.routes.each do |route| expect(route.path).to eql '/abc(.:format)' end get '/abc' expect(last_response.body).to eql 'hiya' post '/abc' expect(last_response.body).to eql 'hiya' end %i[put post].each do |verb| context verb do ['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object| it "allows a(n) #{object.class} json object in params" do subject.format :json subject.send(verb) do env['api.request.body'] end send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql ::Grape::Json.dump(object) expect(last_request.params).to eql({}) end it 'stores input in api.request.input' do subject.format :json subject.send(verb) do env['api.request.input'] end send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql ::Grape::Json.dump(object).to_json end context 'chunked transfer encoding' do it 'stores input in api.request.input' do subject.format :json subject.send(verb) do env['api.request.input'] end send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql ::Grape::Json.dump(object).to_json end end end end end it 'allows for multipart paths' do subject.route(%i[get post], '/:id/first') do 'first' end subject.route(%i[get post], '/:id') do 'ola' end subject.route(%i[get post], '/:id/first/second') do 'second' end get '/1' expect(last_response.body).to eql 'ola' post '/1' expect(last_response.body).to eql 'ola' get '/1/first' expect(last_response.body).to eql 'first' post '/1/first' expect(last_response.body).to eql 'first' get '/1/first/second' expect(last_response.body).to eql 'second' end it 'allows for :any as a verb' do subject.route(:any, '/abc') do 'lol' end %w[get post put delete options patch].each do |m| send(m, '/abc') expect(last_response.body).to eql 'lol' end end it 'allows for catch-all in a namespace' do subject.namespace :nested do get do 'root' end get 'something' do 'something' end route :any, '*path' do 'catch-all' end end get 'nested' expect(last_response.body).to eql 'root' get 'nested/something' expect(last_response.body).to eql 'something' get 'nested/missing' expect(last_response.body).to eql 'catch-all' post 'nested' expect(last_response.body).to eql 'catch-all' post 'nested/something' expect(last_response.body).to eql 'catch-all' end verbs = %w[post get head delete put options patch] verbs.each do |verb| it "allows and properly constrain a #{verb.upcase} method" do subject.send(verb, '/example') do verb end send(verb, '/example') expect(last_response.body).to eql verb == 'head' ? '' : verb # Call it with all methods other than the properly constrained one. (verbs - [verb]).each do |other_verb| send(other_verb, '/example') expected_rc = if other_verb == 'options' then 204 elsif other_verb == 'head' && verb == 'get' then 200 else 405 end expect(last_response.status).to eql expected_rc end end end it 'returns a 201 response code for POST by default' do subject.post('example') do 'Created' end post '/example' expect(last_response.status).to eql 201 expect(last_response.body).to eql 'Created' end it 'returns a 405 for an unsupported method with an X-Custom-Header' do subject.before { header 'X-Custom-Header', 'foo' } subject.get 'example' do 'example' end put '/example' expect(last_response.status).to eql 405 expect(last_response.body).to eql '405 Not Allowed' expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs only the before filter on 405 bad method' do subject.namespace :example do before { header 'X-Custom-Header', 'foo' } before_validation { raise 'before_validation filter should not run' } after_validation { raise 'after_validation filter should not run' } after { raise 'after filter should not run' } params { requires :only_for_get } get end post '/example' expect(last_response.status).to eql 405 expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs before filter exactly once on 405 bad method' do already_run = false subject.namespace :example do before do raise 'before filter ran twice' if already_run already_run = true header 'X-Custom-Header', 'foo' end get end post '/example' expect(last_response.status).to eql 405 expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs all filters and body with a custom OPTIONS method' do subject.namespace :example do before { header 'X-Custom-Header-1', 'foo' } before_validation { header 'X-Custom-Header-2', 'foo' } after_validation { header 'X-Custom-Header-3', 'foo' } after { header 'X-Custom-Header-4', 'foo' } options { 'yup' } get end options '/example' expect(last_response.status).to eql 200 expect(last_response.body).to eql 'yup' expect(last_response.headers['Allow']).to be_nil expect(last_response.headers['X-Custom-Header-1']).to eql 'foo' expect(last_response.headers['X-Custom-Header-2']).to eql 'foo' expect(last_response.headers['X-Custom-Header-3']).to eql 'foo' expect(last_response.headers['X-Custom-Header-4']).to eql 'foo' end context 'when format is xml' do it 'returns a 405 for an unsupported method' do subject.format :xml subject.get 'example' do 'example' end put '/example' expect(last_response.status).to eql 405 expect(last_response.body).to eq <<-XML 405 Not Allowed XML end end context 'when accessing env' do it 'returns a 405 for an unsupported method' do subject.before do _customheader1 = headers['X-Custom-Header'] _customheader2 = env['HTTP_X_CUSTOM_HEADER'] end subject.get 'example' do 'example' end put '/example' expect(last_response.status).to eql 405 expect(last_response.body).to eql '405 Not Allowed' end end specify '405 responses includes an Allow header specifying supported methods' do subject.get 'example' do 'example' end subject.post 'example' do 'example' end put '/example' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, POST, HEAD' end specify '405 responses includes an Content-Type header' do subject.get 'example' do 'example' end subject.post 'example' do 'example' end put '/example' expect(last_response.headers['Content-Type']).to eql 'text/plain' end describe 'adds an OPTIONS route that' do before do subject.before { header 'X-Custom-Header', 'foo' } subject.before_validation { header 'X-Custom-Header-2', 'bar' } subject.after_validation { header 'X-Custom-Header-3', 'baz' } subject.after { header 'X-Custom-Header-4', 'bing' } subject.params { requires :only_for_get } subject.get 'example' do 'example' end subject.route :any, '*path' do error! :not_found, 404 end options '/example' end it 'returns a 204' do expect(last_response.status).to eql 204 end it 'has an empty body' do expect(last_response.body).to be_blank end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD' end it 'calls before hook' do expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'does not call before_validation hook' do expect(last_response.headers.key?('X-Custom-Header-2')).to be false end it 'does not call after_validation hook' do expect(last_response.headers.key?('X-Custom-Header-3')).to be false end it 'calls after hook' do expect(last_response.headers['X-Custom-Header-4']).to eq 'bing' end it 'has no Content-Type' do expect(last_response.content_type).to be_nil end it 'has no Content-Length' do expect(last_response.content_length).to be_nil end end describe 'adds an OPTIONS route for namespaced endpoints that' do before do subject.before { header 'X-Custom-Header', 'foo' } subject.namespace :example do before { header 'X-Custom-Header-2', 'foo' } get :inner do 'example/inner' end end options '/example/inner' end it 'returns a 204' do expect(last_response.status).to eql 204 end it 'has an empty body' do expect(last_response.body).to be_blank end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD' end it 'calls the outer before filter' do expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'calls the inner before filter' do expect(last_response.headers['X-Custom-Header-2']).to eql 'foo' end it 'has no Content-Type' do expect(last_response.content_type).to be_nil end it 'has no Content-Length' do expect(last_response.content_length).to be_nil end end describe 'adds a 405 Not Allowed route that' do before do subject.before { header 'X-Custom-Header', 'foo' } subject.post :example do 'example' end get '/example' end it 'returns a 405' do expect(last_response.status).to eql 405 end it 'contains error message in body' do expect(last_response.body).to eq '405 Not Allowed' end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, POST' end it 'has a X-Custom-Header' do expect(last_response.headers['X-Custom-Header']).to eql 'foo' end end describe 'when hook behaviour is controlled by attributes on the route ' do before do subject.before do error!('Access Denied', 401) unless route.options[:secret] == params[:secret] end subject.namespace 'example' do before do error!('Access Denied', 401) unless route.options[:namespace_secret] == params[:namespace_secret] end desc 'it gets with secret', secret: 'password' get { status(params[:id] == '504' ? 200 : 404) } desc 'it post with secret', secret: 'password', namespace_secret: 'namespace_password' post {} end end context 'when HTTP method is not defined' do let(:response) { delete('/example') } it 'responds with a 405 status' do expect(response.status).to eql 405 end end context 'when HTTP method is defined with attribute' do let(:response) { post('/example?secret=incorrect_password') } it 'responds with the defined error in the before hook' do expect(response.status).to eql 401 end end context 'when HTTP method is defined and the underlying before hook expectation is not met' do let(:response) { post('/example?secret=password&namespace_secret=wrong_namespace_password') } it 'ends up in the endpoint' do expect(response.status).to eql 401 end end context 'when HTTP method is defined and everything is like the before hooks expect' do let(:response) { post('/example?secret=password&namespace_secret=namespace_password') } it 'ends up in the endpoint' do expect(response.status).to eql 201 end end context 'when HEAD is called for the defined GET' do let(:response) { head('/example?id=504') } it 'responds with 401 because before expectations in before hooks are not met' do expect(response.status).to eql 401 end end context 'when HEAD is called for the defined GET' do let(:response) { head('/example?id=504&secret=password') } it 'responds with 200 because before hooks are not called' do expect(response.status).to eql 200 end end end context 'allows HEAD on a GET request that' do before do subject.get 'example' do 'example' end subject.route :any, '*path' do error! :not_found, 404 end head '/example' end it 'returns a 200' do expect(last_response.status).to eql 200 end it 'has an empty body' do expect(last_response.body).to eql '' end end it 'overwrites the default HEAD request' do subject.head 'example' do error! 'nothing to see here', 400 end subject.get 'example' do 'example' end head '/example' expect(last_response.status).to eql 400 end end context 'do_not_route_head!' do before :each do subject.do_not_route_head! subject.get 'example' do 'example' end end it 'options does not contain HEAD' do options '/example' expect(last_response.status).to eql 204 expect(last_response.body).to eql '' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET' end it 'does not allow HEAD on a GET request' do head '/example' expect(last_response.status).to eql 405 end end context 'do_not_route_options!' do before :each do subject.do_not_route_options! subject.get 'example' do 'example' end end it 'does not create an OPTIONS route' do options '/example' expect(last_response.status).to eql 405 end it 'does not include OPTIONS in Allow header' do options '/example' expect(last_response.status).to eql 405 expect(last_response.headers['Allow']).to eql 'GET, HEAD' end end describe '.compile!' do it 'requires the grape/eager_load file' do expect(app).to receive(:require).with('grape/eager_load') { nil } app.compile! end it 'compiles the instance for rack!' do stubbed_object = double(:instance_for_rack) allow(app).to receive(:instance_for_rack) { stubbed_object } end end # NOTE: this method is required to preserve the ability of pre-mounting # the root API into a namespace, it may be deprecated in the future. describe 'instance_for_rack' do context 'when the app was not mounted' do it 'returns the base_instance' do expect(app.send(:instance_for_rack)).to eq app.base_instance end end context 'when the app was mounted' do it 'returns the first mounted instance' do mounted_app = app Class.new(Grape::API) do namespace 'new_namespace' do mount mounted_app end end expect(app.send(:instance_for_rack)).to eq app.send(:mounted_instances).first end 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 - #{instance_variable_defined?(:@foo) ? @foo : nil}" end subject.namespace :blah do before { @foo = 'foo' } get '/' do "blah - #{@foo}" end namespace :bar do get '/' do "blah - bar - #{@foo}" end end end get '/' expect(last_response.body).to eql 'root - ' get '/blah' expect(last_response.body).to eql 'blah - foo' get '/blah/bar' expect(last_response.body).to eql 'blah - bar - foo' end it 'adds a after_validation filter' do subject.after_validation { @foo = "first #{params[:id]}:#{params[:id].class}" } subject.after_validation { @bar = 'second' } subject.params do requires :id, type: Integer end subject.get '/' do "#{@foo} #{@bar}" end get '/', id: '32' expect(last_response.body).to eql "first 32:#{integer_class_name} second" end it 'adds a after filter' do m = double('after mock') subject.after { m.do_something! } subject.after { m.do_something! } subject.get '/' do @var ||= 'default' end expect(m).to receive(:do_something!).exactly(2).times get '/' expect(last_response.body).to eql 'default' end it 'calls all filters when validation passes' do a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.do_something! } before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } get do 'got it' end end expect(a).to receive(:do_something!).exactly(1).times expect(b).to receive(:do_something!).exactly(1).times expect(c).to receive(:do_something!).exactly(1).times expect(d).to receive(:do_something!).exactly(1).times get '/123' expect(last_response.status).to eql 200 expect(last_response.body).to eql 'got it' end it 'calls only before filters when validation fails' do a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.do_something! } before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } get do 'got it' end end expect(a).to receive(:do_something!).exactly(1).times expect(b).to receive(:do_something!).exactly(1).times expect(c).to receive(:do_something!).exactly(0).times expect(d).to receive(:do_something!).exactly(0).times get '/abc' expect(last_response.status).to eql 400 expect(last_response.body).to eql 'id is invalid' end it 'calls filters in the correct order' do i = 0 a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.here(i += 1) } before_validation { b.here(i += 1) } after_validation { c.here(i += 1) } after { d.here(i += 1) } get do 'got it' end end expect(a).to receive(:here).with(1).exactly(1).times expect(b).to receive(:here).with(2).exactly(1).times expect(c).to receive(:here).with(3).exactly(1).times expect(d).to receive(:here).with(4).exactly(1).times get '/123' expect(last_response.status).to eql 200 expect(last_response.body).to eql 'got it' end end context 'format' do before do subject.get('/foo') { 'bar' } end it 'sets content type for txt format' do get '/foo' expect(last_response.headers['Content-Type']).to eq('text/plain') end it 'does not set Cache-Control' do get '/foo' expect(last_response.headers['Cache-Control']).to eq(nil) end it 'sets content type for xml' do get '/foo.xml' expect(last_response.headers['Content-Type']).to eq('application/xml') end it 'sets content type for json' do get '/foo.json' expect(last_response.headers['Content-Type']).to eq('application/json') end it 'sets content type for serializable hash format' do get '/foo.serializable_hash' expect(last_response.headers['Content-Type']).to eq('application/json') end it 'sets content type for binary format' do get '/foo.binary' expect(last_response.headers['Content-Type']).to eq('application/octet-stream') end it 'returns raw data when content type binary' do image_filename = 'grape.png' file = File.open(image_filename, 'rb', &:read) subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' expect(last_response.headers['Content-Type']).to eq('application/octet-stream') expect(last_response.body).to eq(file) end it 'returns the content of the file with file' do file_content = 'This is some file content' test_file = Tempfile.new('test') test_file.write file_content test_file.rewind subject.get('/file') { file test_file } get '/file' expect(last_response.headers['Content-Length']).to eq('25') expect(last_response.headers['Content-Type']).to eq('text/plain') expect(last_response.body).to eq(file_content) end it 'streams the content of the file with stream' do test_stream = Enumerator.new do |blk| blk.yield 'This is some' blk.yield ' file content' end subject.use Rack::Chunked subject.get('/stream') { stream test_stream } get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' expect(last_response.headers['Content-Type']).to eq('text/plain') expect(last_response.headers['Content-Length']).to eq(nil) expect(last_response.headers['Cache-Control']).to eq('no-cache') expect(last_response.headers['Transfer-Encoding']).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") end it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' expect(last_response.headers['Content-Type']).to eql 'text/plain' end it 'sets content type for json error' do subject.format :json subject.get('/error') { error!('error in json', 500) } get '/error.json' expect(last_response.status).to eql 500 expect(last_response.headers['Content-Type']).to eql 'application/json' end it 'sets content type for xml error' do subject.format :xml subject.get('/error') { error!('error in xml', 500) } get '/error' expect(last_response.status).to eql 500 expect(last_response.headers['Content-Type']).to eql 'application/xml' end it 'includes extension in format' do subject.get(':id') { params[:format] } get '/baz.bar' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bar' end it 'does not include extension in id' do subject.format :json subject.get(':id') { params } get '/baz.bar' expect(last_response.status).to eq 404 end context 'with a custom content_type' do before do subject.content_type :custom, 'application/custom' subject.formatter :custom, ->(_object, _env) { 'custom' } subject.get('/custom') { 'bar' } subject.get('/error') { error!('error in custom', 500) } end it 'sets content type' do get '/custom.custom' expect(last_response.headers['Content-Type']).to eql 'application/custom' end it 'sets content type for error' do get '/error.custom' expect(last_response.headers['Content-Type']).to eql 'application/custom' end end context 'env["api.format"]' do before do subject.post 'attachment' do filename = params[:file][:filename] content_type MIME::Types.type_for(filename)[0].to_s env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is" header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}" params[:file][:tempfile].read end end ['/attachment.png', 'attachment'].each do |url| it "uploads and downloads a PNG file via #{url}" do image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true) expect(last_response.status).to eq(201) expect(last_response.headers['Content-Type']).to eq('image/png') expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| expect(last_response.body).to eq io.read end end end it 'uploads and downloads a Ruby file' do filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true) expect(last_response.status).to eq(201) expect(last_response.headers['Content-Type']).to eq('application/x-ruby') expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| expect(last_response.body).to eq io.read end end end end context 'custom middleware' do module ApiSpec class PhonyMiddleware def initialize(app, *args) @args = args @app = app @block = block_given? ? true : nil end def call(env) env['phony.args'] ||= [] env['phony.args'] << @args env['phony.block'] = true if @block @app.call(env) end end end describe '.middleware' do it 'includes middleware arguments from settings' do subject.use ApiSpec::PhonyMiddleware, 'abc', 123 expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 'abc', 123]] end it 'includes all middleware from stacked settings' do subject.use ApiSpec::PhonyMiddleware, 123 subject.use ApiSpec::PhonyMiddleware, 'abc' subject.use ApiSpec::PhonyMiddleware, 'foo' expect(subject.middleware).to eql [ [:use, ApiSpec::PhonyMiddleware, 123], [:use, ApiSpec::PhonyMiddleware, 'abc'], [:use, ApiSpec::PhonyMiddleware, 'foo'] ] end end describe '.use' do it 'adds middleware' do subject.use ApiSpec::PhonyMiddleware, 123 expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]] end it 'does not show up outside the namespace' do inner_middleware = nil subject.use ApiSpec::PhonyMiddleware, 123 subject.namespace :awesome do use ApiSpec::PhonyMiddleware, 'abc' inner_middleware = middleware end expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]] expect(inner_middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123], [:use, ApiSpec::PhonyMiddleware, 'abc']] end it 'calls the middleware' do subject.use ApiSpec::PhonyMiddleware, 'hello' subject.get '/' do env['phony.args'].first.first end get '/' expect(last_response.body).to eql 'hello' end it 'adds a block if one is given' do block = -> {} subject.use ApiSpec::PhonyMiddleware, &block expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, block]] end it 'uses a block if one is given' do block = -> {} subject.use ApiSpec::PhonyMiddleware, &block subject.get '/' do env['phony.block'].inspect end get '/' expect(last_response.body).to eq('true') end it 'does not destroy the middleware settings on multiple runs' do block = -> {} subject.use ApiSpec::PhonyMiddleware, &block subject.get '/' do env['phony.block'].inspect end 2.times do get '/' expect(last_response.body).to eq('true') end end it 'mounts behind error middleware' do m = Class.new(Grape::Middleware::Base) do def before throw :error, message: 'Caught in the Net', status: 400 end end subject.use m subject.get '/' do end get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('Caught in the Net') end end describe '.insert_before' do it 'runs before a given middleware' do m = Class.new(Grape::Middleware::Base) do def call(env) env['phony.args'] ||= [] env['phony.args'] << @options[:message] @app.call(env) end end subject.use ApiSpec::PhonyMiddleware, 'hello' subject.insert_before ApiSpec::PhonyMiddleware, m, message: 'bye' subject.get '/' do env['phony.args'].join(' ') end get '/' expect(last_response.body).to eql 'bye hello' end end describe '.insert_after' do it 'runs after a given middleware' do m = Class.new(Grape::Middleware::Base) do def call(env) env['phony.args'] ||= [] env['phony.args'] << @options[:message] @app.call(env) end end subject.use ApiSpec::PhonyMiddleware, 'hello' subject.insert_after ApiSpec::PhonyMiddleware, m, message: 'bye' subject.get '/' do env['phony.args'].join(' ') end get '/' expect(last_response.body).to eql 'hello bye' end end end describe '.insert' do it 'inserts middleware in a specific location in the stack' do m = Class.new(Grape::Middleware::Base) do def call(env) env['phony.args'] ||= [] env['phony.args'] << @options[:message] @app.call(env) end end subject.use ApiSpec::PhonyMiddleware, 'bye' subject.insert 0, m, message: 'good' subject.insert 0, m, message: 'hello' subject.get '/' do env['phony.args'].join(' ') end get '/' expect(last_response.body).to eql 'hello good bye' end end describe '.http_basic' do it 'protects any resources on the same scope' do subject.http_basic do |u, _p| u == 'allow' end subject.get(:hello) { 'Hello, world.' } get '/hello' expect(last_response.status).to eql 401 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response.status).to eql 200 end it 'is scopable' do subject.get(:hello) { 'Hello, world.' } subject.namespace :admin do http_basic do |u, _p| u == 'allow' end get(:hello) { 'Hello, world.' } end get '/hello' expect(last_response.status).to eql 200 get '/admin/hello' expect(last_response.status).to eql 401 end it 'is callable via .auth as well' do subject.auth :http_basic do |u, _p| u == 'allow' end subject.get(:hello) { 'Hello, world.' } get '/hello' expect(last_response.status).to eql 401 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response.status).to eql 200 end it 'has access to the current endpoint' do basic_auth_context = nil subject.http_basic do |u, _p| basic_auth_context = self u == 'allow' end subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(basic_auth_context).to be_a_kind_of(Grape::Endpoint) end it 'has access to helper methods' do subject.helpers do def authorize(u, p) u == 'allow' && p == 'whatever' end end subject.http_basic do |u, p| authorize(u, p) end subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response.status).to eql 200 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever') expect(last_response.status).to eql 401 end it 'can set instance variables accessible to routes' do subject.http_basic do |u, _p| @hello = 'Hello, world.' u == 'allow' end subject.get(:hello) { @hello } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response.status).to eql 200 expect(last_response.body).to eql 'Hello, world.' end end describe '.logger' do subject do Class.new(Grape::API) do def self.io @io ||= StringIO.new end logger ::Logger.new(io) end end it 'returns an instance of Logger class by default' do expect(subject.logger.class).to eql Logger end it 'allows setting a custom logger' do mylogger = Class.new subject.logger mylogger expect(mylogger).to receive(:info).exactly(1).times subject.logger.info 'this will be logged' end it 'defaults to a standard logger log format' do t = Time.at(100) allow(Time).to receive(:now).and_return(t) message = "this will be logged\n" message = "I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : #{message}" if !defined?(Rails) || Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new('4.0') expect(subject.io).to receive(:write).with(message) subject.logger.info 'this will be logged' end it 'does not unnecessarily retain duplicate setup blocks' do subject.logger expect { subject.logger }.to_not change(subject.instance_variable_get(:@setup), :size) 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 'lifecycle' do let!(:lifecycle) { [] } let!(:standard_cycle) do %i[before before_validation after_validation api_call after finally] end let!(:validation_error) do %i[before before_validation finally] end let!(:errored_cycle) do %i[before before_validation after_validation api_call finally] end before do current_cycle = lifecycle subject.before do current_cycle << :before end subject.before_validation do current_cycle << :before_validation end subject.after_validation do current_cycle << :after_validation end subject.after do current_cycle << :after end subject.finally do current_cycle << :finally end end context 'when the api_call succeeds' do before do current_cycle = lifecycle subject.get 'api_call' do current_cycle << :api_call end end it 'follows the standard life_cycle' do get '/api_call' expect(lifecycle).to eq standard_cycle end end context 'when the api_call has a controlled error' do before do current_cycle = lifecycle subject.get 'api_call' do current_cycle << :api_call error!(:some_error) end end it 'follows the errored life_cycle (skips after)' do get '/api_call' expect(lifecycle).to eq errored_cycle end end context 'when the api_call has an exception' do before do current_cycle = lifecycle subject.get 'api_call' do current_cycle << :api_call raise StandardError end end it 'follows the errored life_cycle (skips after)' do expect { get '/api_call' }.to raise_error(StandardError) expect(lifecycle).to eq errored_cycle end end context 'when the api_call fails validation' do before do current_cycle = lifecycle subject.params do requires :some_param, type: String end subject.get 'api_call' do current_cycle << :api_call end end it 'follows the failed_validation cycle (skips after_validation, api_call & after)' do get '/api_call' expect(lifecycle).to eq validation_error end end end describe '.finally' do let!(:code) { { has_executed: false } } let(:block_to_run) do code_to_execute = code proc do code_to_execute[:has_executed] = true end end context 'when the ensure block has no exceptions' do before { subject.finally(&block_to_run) } context 'when no API call is made' do it 'has not executed the ensure code' do expect(code[:has_executed]).to be false end end context 'when no errors occurs' do before do subject.get '/no_exceptions' do 'success' end end it 'executes the ensure code' do get '/no_exceptions' expect(last_response.body).to eq 'success' expect(code[:has_executed]).to be true end context 'with a helper' do let(:block_to_run) do code_to_execute = code proc do code_to_execute[:value] = some_helper end end before do subject.helpers do def some_helper 'some_value' end end subject.get '/with_helpers' do 'success' end end it 'has access to the helper' do get '/with_helpers' expect(code[:value]).to eq 'some_value' end end end context 'when an unhandled occurs inside the API call' do before do subject.get '/unhandled_exception' do raise StandardError end end it 'executes the ensure code' do expect { get '/unhandled_exception' }.to raise_error StandardError expect(code[:has_executed]).to be true end end context 'when a handled error occurs inside the API call' do before do subject.rescue_from(StandardError) { error! 'handled' } subject.get '/handled_exception' do raise StandardError end end it 'executes the ensure code' do get '/handled_exception' expect(code[:has_executed]).to be true expect(last_response.body).to eq 'handled' end end end end describe '.rescue_from' do it 'does not rescue errors when rescue_from is not set' do subject.get '/exception' do raise 'rain!' end expect { get '/exception' }.to raise_error(RuntimeError, 'rain!') end it 'uses custom helpers defined by using #helpers method' do subject.helpers do def custom_error!(name) error! "hello #{name}" end end subject.rescue_from(ArgumentError) { custom_error! :bob } subject.get '/custom_error' do raise ArgumentError end get '/custom_error' expect(last_response.body).to eq 'hello bob' end context 'with multiple apis' do let(:a) { Class.new(Grape::API) } let(:b) { Class.new(Grape::API) } before do a.helpers do def foo error!('foo', 401) end end a.rescue_from(:all) { foo } a.get { raise 'boo' } b.helpers do def foo error!('bar', 401) end end b.rescue_from(:all) { foo } b.get { raise 'boo' } end it 'avoids polluting global namespace' do env = Rack::MockRequest.env_for('/') expect(read_chunks(a.call(env)[2])).to eq(['foo']) expect(read_chunks(b.call(env)[2])).to eq(['bar']) expect(read_chunks(a.call(env)[2])).to eq(['foo']) end end it 'rescues all errors if rescue_from :all is called' do subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.status).to eql 500 expect(last_response.body).to eq 'rain!' end it 'rescues all errors with a json formatter' do subject.format :json subject.default_format :json subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.status).to eql 500 expect(last_response.body).to eq({ error: 'rain!' }.to_json) end it 'rescues only certain errors if rescue_from is called with specific errors' do subject.rescue_from ArgumentError subject.get('/rescued') { raise ArgumentError } subject.get('/unrescued') { raise 'beefcake' } get '/rescued' expect(last_response.status).to eql 500 expect { get '/unrescued' }.to raise_error(RuntimeError, 'beefcake') end it 'mimics default ruby "rescue" handler' do # The exception is matched to the rescue starting at the top, and matches only once subject.rescue_from ArgumentError do |e| error!(e, 402) end subject.rescue_from StandardError do |e| error!(e, 401) end subject.get('/child_of_standard_error') { raise ArgumentError } subject.get('/standard_error') { raise StandardError } get '/child_of_standard_error' expect(last_response.status).to eql 402 get '/standard_error' expect(last_response.status).to eql 401 end context 'CustomError subclass of Grape::Exceptions::Base' do before do module ApiSpec class CustomError < Grape::Exceptions::Base; end end end it 'does not re-raise exceptions of type Grape::Exceptions::Base' do subject.get('/custom_exception') { raise ApiSpec::CustomError } expect { get '/custom_exception' }.not_to raise_error end it 'rescues custom grape exceptions' do subject.rescue_from ApiSpec::CustomError do |e| rack_response('New Error', e.status) end subject.get '/custom_error' do raise ApiSpec::CustomError.new(status: 400, message: 'Custom Error') end get '/custom_error' expect(last_response.status).to eq(400) expect(last_response.body).to eq('New Error') end end it 'can rescue exceptions raised in the formatter' do formatter = double(:formatter) allow(formatter).to receive(:call) { raise StandardError } allow(Grape::Formatter).to receive(:formatter_for) { formatter } subject.rescue_from :all do |_e| rack_response('Formatter Error', 500) end subject.get('/formatter_exception') { 'Hello world' } get '/formatter_exception' expect(last_response.status).to eql 500 expect(last_response.body).to eq('Formatter Error') end it 'uses default_rescue_handler to handle invalid response from rescue_from' do subject.rescue_from(:all) { 'error' } subject.get('/') { raise } expect_any_instance_of(Grape::Middleware::Error).to receive(:default_rescue_handler).and_call_original get '/' expect(last_response.status).to eql 500 expect(last_response.body).to eql 'Invalid response' end end describe '.rescue_from klass, block' do it 'rescues Exception' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.message}", 202) end subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.status).to eql 202 expect(last_response.body).to eq('rescued from rain!') end context 'custom errors' do before do class ConnectionError < RuntimeError; end class DatabaseError < RuntimeError; end class CommunicationError < StandardError; end end it 'rescues an error via rescue_from :all' do subject.rescue_from :all do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' expect(last_response.status).to eql 500 expect(last_response.body).to eq('rescued from ConnectionError') end it 'rescues a specific error' do subject.rescue_from ConnectionError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' expect(last_response.status).to eql 500 expect(last_response.body).to eq('rescued from ConnectionError') end it 'rescues a subclass of an error by default' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' expect(last_response.status).to eql 500 expect(last_response.body).to eq('rescued from ConnectionError') end it 'rescues multiple specific errors' do subject.rescue_from ConnectionError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.rescue_from DatabaseError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/connection' do raise ConnectionError end subject.get '/database' do raise DatabaseError end get '/connection' expect(last_response.status).to eql 500 expect(last_response.body).to eq('rescued from ConnectionError') get '/database' expect(last_response.status).to eql 500 expect(last_response.body).to eq('rescued from DatabaseError') end it 'does not rescue a different error' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise CommunicationError end expect { get '/uncaught' }.to raise_error(CommunicationError) end end end describe '.rescue_from klass, lambda' do it 'rescues an error with the lambda' do subject.rescue_from ArgumentError, lambda { rack_response('rescued with a lambda', 400) } subject.get('/rescue_lambda') { raise ArgumentError } get '/rescue_lambda' expect(last_response.status).to eq(400) expect(last_response.body).to eq('rescued with a lambda') end it 'can execute the lambda with an argument' do subject.rescue_from ArgumentError, lambda { |e| rack_response(e.message, 400) } subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' } get '/rescue_lambda' expect(last_response.status).to eq(400) expect(last_response.body).to eq('lambda takes an argument') end end describe '.rescue_from klass, with: :method_name' do it 'rescues an error with the specified method name' do subject.helpers do def rescue_arg_error error!('500 ArgumentError', 500) end def rescue_no_method_error error!('500 NoMethodError', 500) end end subject.rescue_from ArgumentError, with: :rescue_arg_error subject.rescue_from NoMethodError, with: :rescue_no_method_error subject.get('/rescue_arg_error') { raise ArgumentError } subject.get('/rescue_no_method_error') { raise NoMethodError } get '/rescue_arg_error' expect(last_response.status).to eq(500) expect(last_response.body).to eq('500 ArgumentError') get '/rescue_no_method_error' expect(last_response.status).to eq(500) expect(last_response.body).to eq('500 NoMethodError') end it 'aborts if the specified method name does not exist' do subject.rescue_from :all, with: :not_exist_method subject.get('/rescue_method') { raise StandardError } expect { get '/rescue_method' }.to raise_error(NoMethodError, 'undefined method `not_exist_method\'') end it 'correctly chooses exception handler if :all handler is specified' do subject.helpers do def rescue_arg_error error!('500 ArgumentError', 500) end def rescue_all_errors error!('500 AnotherError', 500) end end subject.rescue_from ArgumentError, with: :rescue_arg_error subject.rescue_from :all, with: :rescue_all_errors subject.get('/argument_error') { raise ArgumentError } subject.get('/another_error') { raise NoMethodError } get '/argument_error' expect(last_response.status).to eq(500) expect(last_response.body).to eq('500 ArgumentError') get '/another_error' expect(last_response.status).to eq(500) expect(last_response.body).to eq('500 AnotherError') end end describe '.rescue_from klass, rescue_subclasses: boolean' do before do module ApiSpec module APIErrors class ParentError < StandardError; end class ChildError < ParentError; end end end end it 'rescues error as well as subclass errors with rescue_subclasses option set' do subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: true do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise ApiSpec::APIErrors::ChildError end subject.get '/caught_parent' do raise ApiSpec::APIErrors::ParentError end subject.get '/uncaught_parent' do raise StandardError end get '/caught_child' expect(last_response.status).to eql 500 get '/caught_parent' expect(last_response.status).to eql 500 expect { get '/uncaught_parent' }.to raise_error(StandardError) end it 'sets rescue_subclasses to true by default' do subject.rescue_from ApiSpec::APIErrors::ParentError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise ApiSpec::APIErrors::ChildError end get '/caught_child' expect(last_response.status).to eql 500 end it 'does not rescue child errors if rescue_subclasses is false' do subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: false do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise ApiSpec::APIErrors::ChildError end expect { get '/uncaught' }.to raise_error(ApiSpec::APIErrors::ChildError) end end describe '.rescue_from :grape_exceptions' do before do subject.rescue_from :grape_exceptions end let(:grape_exception) do Grape::Exceptions::Base.new(status: 400, message: 'Grape Error') end it 'rescues grape exceptions' do exception = grape_exception subject.get('/grape_exception') { raise exception } get '/grape_exception' expect(last_response.status).to eq(exception.status) expect(last_response.body).to eq(exception.message) end it 'rescues grape exceptions with a user-defined handler' do subject.rescue_from grape_exception.class do |_error| rack_response('Redefined Error', 403) end exception = grape_exception subject.get('/grape_exception') { raise exception } get '/grape_exception' expect(last_response.status).to eq(403) expect(last_response.body).to eq('Redefined Error') end end describe '.error_format' do it 'rescues all errors and return :txt' do subject.rescue_from :all subject.format :txt subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body).to eql 'rain!' end it 'rescues all errors and return :txt with backtrace' do subject.rescue_from :all, backtrace: true subject.format :txt subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body.start_with?("rain!\r\n")).to be true end it 'rescues all errors with a default formatter' do subject.default_format :foo subject.content_type :foo, 'text/foo' subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception.foo' expect(last_response.body).to start_with 'rain!' end it 'defaults the error formatter to format' do subject.format :json subject.rescue_from :all subject.content_type :json, 'application/json' subject.content_type :foo, 'text/foo' subject.get '/exception' do raise 'rain!' end get '/exception.json' expect(last_response.body).to eq('{"error":"rain!"}') get '/exception.foo' expect(last_response.body).to eq('{"error":"rain!"}') end context 'class' do before :each do module ApiSpec class CustomErrorFormatter def self.call(message, _backtrace, _options, _env, _original_exception) "message: #{message} @backtrace" end end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, ApiSpec::CustomErrorFormatter subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end end describe 'with' do context 'class' do before :each do module ApiSpec class CustomErrorFormatter def self.call(message, _backtrace, _option, _env, _original_exception) "message: #{message} @backtrace" end end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, with: ApiSpec::CustomErrorFormatter subject.get('/exception') { raise 'rain!' } get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end end end it 'rescues all errors and return :json' do subject.rescue_from :all subject.format :json subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body).to eql '{"error":"rain!"}' end it 'rescues all errors and return :json with backtrace' do subject.rescue_from :all, backtrace: true subject.format :json subject.get '/exception' do raise 'rain!' end get '/exception' json = ::Grape::Json.load(last_response.body) expect(json['error']).to eql 'rain!' expect(json['backtrace'].length).to be > 0 end it 'rescues error! and return txt' do subject.format :txt subject.get '/error' do error!('Access Denied', 401) end get '/error' expect(last_response.body).to eql 'Access Denied' end context 'with json format' do before { subject.format :json } it 'rescues error! called with a string and returns json' do subject.get('/error') { error!(:failure, 401) } end it 'rescues error! called with a symbol and returns json' do subject.get('/error') { error!(:failure, 401) } end it 'rescues error! called with a hash and returns json' do subject.get('/error') { error!({ error: :failure }, 401) } end after do get '/error' expect(last_response.body).to eql('{"error":"failure"}') end end end describe '.content_type' do it 'sets additional content-type' do subject.content_type :xls, 'application/vnd.ms-excel' subject.get :excel do 'some binary content' end get '/excel.xls' expect(last_response.content_type).to eq('application/vnd.ms-excel') end it 'allows to override content-type' do subject.get :content do content_type 'text/javascript' 'var x = 1;' end get '/content' expect(last_response.content_type).to eq('text/javascript') end it 'removes existing content types' do subject.content_type :xls, 'application/vnd.ms-excel' subject.get :excel do 'some binary content' end get '/excel.json' expect(last_response.status).to eq(406) if ActiveSupport::VERSION::MAJOR == 3 expect(last_response.body).to eq('The requested format 'txt' is not supported.') else expect(last_response.body).to eq('The requested format 'txt' is not supported.') end end end describe '.formatter' do context 'multiple formatters' do before :each do subject.formatter :json, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" } subject.formatter :txt, ->(object, _env) { "custom_formatter: #{object[:some]}" } subject.get :simple do { some: 'hash' } end end it 'sets one formatter' do get '/simple.json' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end it 'sets another formatter' do get '/simple.txt' expect(last_response.body).to eql 'custom_formatter: hash' end end context 'custom formatter' do before :each do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" } subject.get :simple do { some: 'hash' } end end it 'uses json' do get '/simple.json' expect(last_response.body).to eql '{"some":"hash"}' end it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end context 'custom formatter class' do module ApiSpec module CustomFormatter def self.call(object, _env) "{\"custom_formatter\":\"#{object[:some]}\"}" end end end before :each do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, ApiSpec::CustomFormatter subject.get :simple do { some: 'hash' } end end it 'uses json' do get '/simple.json' expect(last_response.body).to eql '{"some":"hash"}' end it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end end describe '.parser' do it 'parses data in format requested by content-type' do subject.format :json subject.post '/data' do { x: params[:x] } end post '/data', '{"x":42}', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(last_response.body).to eq('{"x":42}') end context 'lambda parser' do before :each do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, ->(object, _env) { { object.to_sym => object.to_s.reverse } } subject.put :simple do params[:simple] end end ['text/custom', 'text/custom; charset=UTF-8'].each do |content_type| it "uses parser for #{content_type}" do put '/simple', 'simple', 'CONTENT_TYPE' => content_type expect(last_response.status).to eq(200) expect(last_response.body).to eql 'elpmis' end end end context 'custom parser class' do module ApiSpec module CustomParser def self.call(object, _env) { object.to_sym => object.to_s.reverse } end end end before :each do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, ApiSpec::CustomParser subject.put :simple do params[:simple] end end it 'uses custom parser' do put '/simple', 'simple', 'CONTENT_TYPE' => 'text/custom' expect(last_response.status).to eq(200) expect(last_response.body).to eql 'elpmis' end end if Object.const_defined? :MultiXml context 'multi_xml' do it "doesn't parse yaml" do subject.put :yaml do params[:tag] end put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq(400) expect(last_response.body).to eql 'Disallowed type attribute: "symbol"' end end else context 'default xml parser' do it 'parses symbols' do subject.put :yaml do params[:tag] end put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq(200) expect(last_response.body).to eql '{"type"=>"symbol", "__content__"=>"a123"}' end end end context 'none parser class' do before :each do subject.parser :json, nil subject.put 'data' do "body: #{env['api.request.body']}" end end it 'does not parse data' do put '/data', 'not valid json', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(200) expect(last_response.body).to eq('body: not valid json') end end end describe '.default_format' do before :each do subject.format :json subject.default_format :json end it 'returns data in default format' do subject.get '/data' do { x: 42 } end get '/data' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"x":42}') end it 'parses data in default format' do subject.post '/data' do { x: params[:x] } end post '/data', '{"x":42}', 'CONTENT_TYPE' => '' expect(last_response.status).to eq(201) expect(last_response.body).to eq('{"x":42}') end end describe '.default_error_status' do it 'allows setting default_error_status' do subject.rescue_from :all subject.default_error_status 200 subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.status).to eql 200 end it 'has a default error status' do subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.status).to eql 500 end it 'uses the default error status in error!' do subject.rescue_from :all subject.default_error_status 400 subject.get '/exception' do error! 'rain!' end get '/exception' expect(last_response.status).to eql 400 end end context 'http_codes' do let(:error_presenter) do Class.new(Grape::Entity) do expose :code expose :static def static 'some static text' end end end it 'is used as presenter' do subject.desc 'some desc', http_codes: [ [408, 'Unauthorized', error_presenter] ] subject.get '/exception' do error!({ code: 408 }, 408) end get '/exception' expect(last_response.status).to eql 408 expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json) end it 'presented with' do error = { code: 408, with: error_presenter }.freeze subject.get '/exception' do error! error, 408 end get '/exception' expect(last_response.status).to eql 408 expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json) end end context 'routes' do describe 'empty api structure' do it 'returns an empty array of routes' do expect(subject.routes).to eq([]) end end describe 'single method api structure' do before(:each) do subject.get :ping do 'pong' end end it 'returns one route' do expect(subject.routes.size).to eq(1) route = subject.routes[0] expect(route.version).to be_nil expect(route.path).to eq('/ping(.:format)') expect(route.request_method).to eq('GET') end end describe 'api structure with two versions and a namespace' do before :each do subject.version 'v1', using: :path subject.get 'version' do api.version end # version v2 subject.version 'v2', using: :path subject.prefix 'p' subject.namespace 'n1' do namespace 'n2' do get 'version' do api.version end end end end it 'returns the latest version set' do expect(subject.version).to eq('v2') end it 'returns versions' do expect(subject.versions).to eq(%w[v1 v2]) end it 'sets route paths' do expect(subject.routes.size).to be >= 2 expect(subject.routes[0].path).to eq('/:version/version(.:format)') expect(subject.routes[1].path).to eq('/p/:version/n1/n2/version(.:format)') end it 'sets route versions' do expect(subject.routes[0].version).to eq('v1') expect(subject.routes[1].version).to eq('v2') end it 'sets a nested namespace' do expect(subject.routes[1].namespace).to eq('/n1/n2') end it 'sets prefix' do expect(subject.routes[1].prefix).to eq('p') end end describe 'api structure with additional parameters' do before(:each) do subject.params do requires :token, desc: 'a token' optional :limit, desc: 'the limit' end subject.get 'split/:string' do params[:string].split(params[:token], (params[:limit] || 0).to_i) end end it 'splits a string' do get '/split/a,b,c.json', token: ',' expect(last_response.body).to eq('["a","b","c"]') end it 'splits a string with limit' do get '/split/a,b,c.json', token: ',', limit: '2' expect(last_response.body).to eq('["a","b,c"]') end it 'sets params' do expect(subject.routes.map do |route| { params: route.params } end).to eq [ { params: { 'string' => '', 'token' => { required: true, desc: 'a token' }, 'limit' => { required: false, desc: 'the limit' } } } ] end end describe 'api structure with multiple apis' do before(:each) do subject.params do requires :one, desc: 'a token' optional :two, desc: 'the limit' end subject.get 'one' do end subject.params do requires :three, desc: 'a token' optional :four, desc: 'the limit' end subject.get 'two' do end end it 'sets params' do expect(subject.routes.map do |route| { params: route.params } end).to eq [ { params: { 'one' => { required: true, desc: 'a token' }, 'two' => { required: false, desc: 'the limit' } } }, { params: { 'three' => { required: true, desc: 'a token' }, 'four' => { required: false, desc: 'the limit' } } } ] end end describe 'api structure with an api without params' do before(:each) do subject.params do requires :one, desc: 'a token' optional :two, desc: 'the limit' end subject.get 'one' do end subject.get 'two' do end end it 'sets params' do expect(subject.routes.map do |route| { params: route.params } end).to eq [ { params: { 'one' => { required: true, desc: 'a token' }, 'two' => { required: false, desc: 'the limit' } } }, { params: {} } ] end end describe 'api with a custom route setting' do before(:each) do subject.route_setting :custom, key: 'value' subject.get 'one' end it 'exposed' do expect(subject.routes.count).to eq 1 route = subject.routes.first expect(route.settings[:custom]).to eq(key: 'value') end end describe 'status' do it 'can be set to arbitrary Integer value' do subject.get '/foo' do status 210 end get '/foo' expect(last_response.status).to eq 210 end it 'can be set with a status code symbol' do subject.get '/foo' do status :see_other end get '/foo' expect(last_response.status).to eq 303 end end end context 'desc' do it 'empty array of routes' do expect(subject.routes).to eq([]) end it 'empty array of routes' do subject.desc 'grape api' expect(subject.routes).to eq([]) end it 'describes a method' do subject.desc 'first method' subject.get :first expect(subject.routes.length).to eq(1) route = subject.routes.first expect(route.description).to eq('first method') expect(route.route_foo).to be_nil expect(route.params).to eq({}) expect(route.options).to be_a_kind_of(Hash) end it 'has params which does not include format and version as named captures' do subject.version :v1, using: :path subject.get :first param_keys = subject.routes.first.params.keys expect(param_keys).not_to include('format') expect(param_keys).not_to include('version') end it 'describes methods separately' do subject.desc 'first method' subject.get :first subject.desc 'second method' subject.get :second expect(subject.routes.count).to eq(2) expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'first method', params: {} }, { description: 'second method', params: {} } ] end it 'resets desc' do subject.desc 'first method' subject.get :first subject.get :second expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'first method', params: {} }, { description: nil, params: {} } ] end it 'namespaces and describe arbitrary parameters' do subject.namespace 'ns' do desc 'ns second', foo: 'bar' get 'second' end expect(subject.routes.map do |route| { description: route.description, foo: route.route_foo, params: route.params } end).to eq [ { description: 'ns second', foo: 'bar', params: {} } ] end it 'includes details' do subject.desc 'method', details: 'method details' subject.get 'method' expect(subject.routes.map do |route| { description: route.description, details: route.details, params: route.params } end).to eq [ { description: 'method', details: 'method details', params: {} } ] end it 'describes a method with parameters' do subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } subject.get 'reverse' do params[:s].reverse end expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } } ] end it 'does not inherit param descriptions in consequent namespaces' do subject.desc 'global description' subject.params do requires :param1 optional :param2 end subject.namespace 'ns1' do get { ; } end subject.params do optional :param2 end subject.namespace 'ns2' do get { ; } end routes_doc = subject.routes.map do |route| { description: route.description, params: route.params } end expect(routes_doc).to eq [ { description: 'global description', params: { 'param1' => { required: true }, 'param2' => { required: false } } }, { description: 'global description', params: { 'param2' => { required: false } } } ] end it 'merges the parameters of the namespace with the parameters of the method' do subject.desc 'namespace' subject.params do requires :ns_param, desc: 'namespace parameter' end subject.namespace 'ns' do desc 'method' params do optional :method_param, desc: 'method parameter' end get 'method' end routes_doc = subject.routes.map do |route| { description: route.description, params: route.params } end expect(routes_doc).to eq [ { description: 'method', params: { 'ns_param' => { required: true, desc: 'namespace parameter' }, 'method_param' => { required: false, desc: 'method parameter' } } } ] end it 'merges the parameters of nested namespaces' do subject.desc 'ns1' subject.params do optional :ns_param, desc: 'ns param 1' requires :ns1_param, desc: 'ns1 param' end subject.namespace 'ns1' do desc 'ns2' params do requires :ns_param, desc: 'ns param 2' requires :ns2_param, desc: 'ns2 param' end namespace 'ns2' do desc 'method' params do optional :method_param, desc: 'method param' end get 'method' end end expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'method', params: { 'ns_param' => { required: true, desc: 'ns param 2' }, 'ns1_param' => { required: true, desc: 'ns1 param' }, 'ns2_param' => { required: true, desc: 'ns2 param' }, 'method_param' => { required: false, desc: 'method param' } } } ] end it 'groups nested params and prevents overwriting of params with same name in different groups' do subject.desc 'method' subject.params do group :group1, type: Array do optional :param1, desc: 'group1 param1 desc' requires :param2, desc: 'group1 param2 desc' end group :group2, type: Array do optional :param1, desc: 'group2 param1 desc' requires :param2, desc: 'group2 param2 desc' end end subject.get 'method' expect(subject.routes.map(&:params)).to eq [{ 'group1' => { required: true, type: 'Array' }, 'group1[param1]' => { required: false, desc: 'group1 param1 desc' }, 'group1[param2]' => { required: true, desc: 'group1 param2 desc' }, 'group2' => { required: true, type: 'Array' }, 'group2[param1]' => { required: false, desc: 'group2 param1 desc' }, 'group2[param2]' => { required: true, desc: 'group2 param2 desc' } }] end it 'uses full name of parameters in nested groups' do subject.desc 'nesting' subject.params do requires :root_param, desc: 'root param' group :nested, type: Array do requires :nested_param, desc: 'nested param' end end subject.get 'method' expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'nesting', params: { 'root_param' => { required: true, desc: 'root param' }, 'nested' => { required: true, type: 'Array' }, 'nested[nested_param]' => { required: true, desc: 'nested param' } } } ] end it 'allows to set the type attribute on :group element' do subject.params do group :foo, type: Array do optional :bar end end end it 'parses parameters when no description is given' do subject.params do requires :one_param, desc: 'one param' end subject.get 'method' expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: nil, params: { 'one_param' => { required: true, desc: 'one param' } } } ] end it 'does not symbolize params' do subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } subject.get 'reverse/:s' do params[:s].reverse end expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } } ] end end describe '.mount' do let(:mounted_app) { ->(_env) { [200, {}, ['MOUNTED']] } } context 'with a bare rack app' do before do subject.mount mounted_app => '/mounty' end it 'makes a bare Rack app available at the endpoint' do get '/mounty' expect(last_response.body).to eq('MOUNTED') end it 'anchors the routes, passing all subroutes to it' do get '/mounty/awesome' expect(last_response.body).to eq('MOUNTED') end it 'is able to cascade' do subject.mount lambda { |env| headers = {} headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo') [200, headers, ['Farfegnugen']] } => '/' get '/boo' expect(last_response.body).to eq('Farfegnugen') get '/mounty' expect(last_response.body).to eq('MOUNTED') end end context 'without a hash' do it 'calls through setting the route to "/"' do subject.mount mounted_app get '/' expect(last_response.body).to eq('MOUNTED') end end context 'mounting an API' do it 'applies the settings of the mounting api' do subject.version 'v1', using: :path subject.namespace :cool do app = Class.new(Grape::API) app.get('/awesome') do 'yo' end mount app end get '/v1/cool/awesome' expect(last_response.body).to eq('yo') end it 'applies the settings to nested mounted apis' do subject.version 'v1', using: :path subject.namespace :cool do inner_app = Class.new(Grape::API) inner_app.get('/awesome') do 'yo' end app = Class.new(Grape::API) app.mount inner_app mount app end get '/v1/cool/awesome' expect(last_response.body).to eq('yo') end context 'when some rescues are defined by mounted' do it 'inherits parent rescues' do subject.rescue_from :all do |e| rack_response("rescued from #{e.message}", 202) end app = Class.new(Grape::API) subject.namespace :mounted do app.rescue_from ArgumentError app.get('/fail') { raise 'doh!' } mount app end get '/mounted/fail' expect(last_response.status).to eql 202 expect(last_response.body).to eq('rescued from doh!') end it 'prefers rescues defined by mounted if they rescue similar error class' do subject.rescue_from StandardError do rack_response('outer rescue') end app = Class.new(Grape::API) subject.namespace :mounted do rescue_from StandardError do rack_response('inner rescue') end app.get('/fail') { raise 'doh!' } mount app end get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end it 'prefers rescues defined by mounted even if outer is more specific' do subject.rescue_from ArgumentError do rack_response('outer rescue') end app = Class.new(Grape::API) subject.namespace :mounted do rescue_from StandardError do rack_response('inner rescue') end app.get('/fail') { raise ArgumentError.new } mount app end get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end it 'prefers more specific rescues defined by mounted' do subject.rescue_from StandardError do rack_response('outer rescue') end app = Class.new(Grape::API) subject.namespace :mounted do rescue_from ArgumentError do rack_response('inner rescue') end app.get('/fail') { raise ArgumentError.new } mount app end get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end end it 'collects the routes of the mounted api' do subject.namespace :cool do app = Class.new(Grape::API) app.get('/awesome') {} app.post('/sauce') {} mount app end expect(subject.routes.size).to eq(2) expect(subject.routes.first.path).to match(%r{\/cool\/awesome}) expect(subject.routes.last.path).to match(%r{\/cool\/sauce}) end it 'mounts on a path' do subject.namespace :cool do app = Class.new(Grape::API) app.get '/awesome' do 'sauce' end mount app => '/mounted' end get '/mounted/cool/awesome' expect(last_response.status).to eq(200) expect(last_response.body).to eq('sauce') end it 'mounts on a nested path' do APP1 = Class.new(Grape::API) APP2 = Class.new(Grape::API) APP2.get '/nice' do 'play' end # note that the reverse won't work, mount from outside-in APP3 = subject APP3.mount APP1 => '/app1' APP1.mount APP2 => '/app2' get '/app1/app2/nice' expect(last_response.status).to eq(200) expect(last_response.body).to eq('play') options '/app1/app2/nice' expect(last_response.status).to eq(204) end it 'responds to options' do app = Class.new(Grape::API) app.get '/colour' do 'red' end app.namespace :pears do get '/colour' do 'green' end end subject.namespace :apples do mount app end get '/apples/colour' expect(last_response.status).to eql 200 expect(last_response.body).to eq('red') options '/apples/colour' expect(last_response.status).to eql 204 get '/apples/pears/colour' expect(last_response.status).to eql 200 expect(last_response.body).to eq('green') options '/apples/pears/colour' expect(last_response.status).to eql 204 end it 'responds to options with path versioning' do subject.version 'v1', using: :path subject.namespace :apples do app = Class.new(Grape::API) app.get('/colour') do 'red' end mount app end get '/v1/apples/colour' expect(last_response.status).to eql 200 expect(last_response.body).to eq('red') options '/v1/apples/colour' expect(last_response.status).to eql 204 end it 'mounts a versioned API with nested resources' do api = Class.new(Grape::API) do version 'v1' resources :users do get :hello do 'hello users' end end end subject.mount api get '/v1/users/hello' expect(last_response.body).to eq('hello users') end it 'mounts a prefixed API with nested resources' do api = Class.new(Grape::API) do prefix 'api' resources :users do get :hello do 'hello users' end end end subject.mount api get '/api/users/hello' expect(last_response.body).to eq('hello users') end it 'applies format to a mounted API with nested resources' do api = Class.new(Grape::API) do format :json resources :users do get do { users: true } end end end subject.mount api get '/users' expect(last_response.body).to eq({ users: true }.to_json) end it 'applies auth to a mounted API with nested resources' do api = Class.new(Grape::API) do format :json http_basic do |username, password| username == 'username' && password == 'password' end resources :users do get do { users: true } end end end subject.mount api get '/users' expect(last_response.status).to eq(401) get '/users', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('username', 'password') expect(last_response.body).to eq({ users: true }.to_json) end it 'mounts multiple versioned APIs with nested resources' do api1 = Class.new(Grape::API) do version 'one', using: :header, vendor: 'test' resources :users do get :hello do 'one' end end end api2 = Class.new(Grape::API) do version 'two', using: :header, vendor: 'test' resources :users do get :hello do 'two' end end end subject.mount api1 subject.mount api2 versioned_get '/users/hello', 'one', using: :header, vendor: 'test' expect(last_response.body).to eq('one') versioned_get '/users/hello', 'two', using: :header, vendor: 'test' expect(last_response.body).to eq('two') end it 'recognizes potential versions with mounted path' do a = Class.new(Grape::API) do version :v1, using: :path get '/hello' do 'hello' end end b = Class.new(Grape::API) do version :v1, using: :path get '/world' do 'world' end end subject.mount a => '/one' subject.mount b => '/two' get '/one/v1/hello' expect(last_response.status).to eq 200 get '/two/v1/world' expect(last_response.status).to eq 200 end context 'when mounting class extends a subclass of Grape::API' do it 'mounts APIs with the same superclass' do base_api = Class.new(Grape::API) a = Class.new(base_api) b = Class.new(base_api) expect { a.mount b }.to_not raise_error end end context 'when including a module' do let(:included_module) do Module.new do def self.included(base) base.extend(ClassMethods) end module ClassMethods def my_method @test = true end end end end it 'should correctly include module in nested mount' do module_to_include = included_module v1 = Class.new(Grape::API) do version :v1, using: :path include module_to_include my_method end v2 = Class.new(Grape::API) do version :v2, using: :path end segment_base = Class.new(Grape::API) do mount v1 mount v2 end Class.new(Grape::API) do mount segment_base end expect(v1.my_method).to be_truthy end end end end describe '.endpoints' do it 'adds one for each route created' do subject.get '/' subject.post '/' expect(subject.endpoints.size).to eq(2) end end describe '.compile' do it 'sets the instance' do expect(subject.instance).to be_nil subject.compile expect(subject.instance).to be_kind_of(subject.base_instance) end end describe '.change!' do it 'invalidates any compiled instance' do subject.compile subject.change! expect(subject.instance).to be_nil end end describe '.endpoint' do before(:each) do subject.format :json subject.get '/endpoint/options' do { path: options[:path], source_location: source.source_location } end end it 'path' do get '/endpoint/options' options = ::Grape::Json.load(last_response.body) expect(options['path']).to eq(['/endpoint/options']) expect(options['source_location'][0]).to include 'api_spec.rb' expect(options['source_location'][1].to_i).to be > 0 end end describe '.route' do context 'plain' do before(:each) do subject.get '/' do route.path end subject.get '/path' do route.path end end it 'provides access to route info' do get '/' expect(last_response.body).to eq('/(.:format)') get '/path' expect(last_response.body).to eq('/path(.:format)') end end context 'with desc' do before(:each) do subject.desc 'returns description' subject.get '/description' do route.description end subject.desc 'returns parameters', params: { 'x' => 'y' } subject.get '/params/:id' do route.params[params[:id]] end end it 'returns route description' do get '/description' expect(last_response.body).to eq('returns description') end it 'returns route parameters' do get '/params/x' expect(last_response.body).to eq('y') end end end describe '.format' do context ':txt' do before(:each) do subject.format :txt subject.content_type :json, 'application/json' subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces txt without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'does not force txt with an extension' do get '/meaning_of_life.json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end context ':txt only' do before(:each) do subject.format :txt subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces txt without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'accepts specified extension' do get '/meaning_of_life.txt' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'does not accept extensions other than specified' do get '/meaning_of_life.json' expect(last_response.status).to eq(404) end it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end context ':json' do before(:each) do subject.format :json subject.content_type :txt, 'text/plain' subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces json without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end it 'does not force json with an extension' do get '/meaning_of_life.txt' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'forces json from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end it 'can be overwritten with an explicit content type' do subject.get '/meaning_of_life_with_content_type' do content_type 'text/plain' { meaning_of_life: 42 }.to_s end get '/meaning_of_life_with_content_type' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before throw :error, message: 'Unauthorized', status: 42 end end subject.use middleware subject.get do end get '/' expect(last_response.status).to eq(42) expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json) end end context ':serializable_hash' do class SerializableHashExample def serializable_hash { abc: 'def' } end end before(:each) do subject.format :serializable_hash end it 'instance' do subject.get '/example' do SerializableHashExample.new end get '/example' expect(last_response.body).to eq('{"abc":"def"}') end it 'root' do subject.get '/example' do { 'root' => SerializableHashExample.new } end get '/example' expect(last_response.body).to eq('{"root":{"abc":"def"}}') end it 'array' do subject.get '/examples' do [SerializableHashExample.new, SerializableHashExample.new] end get '/examples' expect(last_response.body).to eq('[{"abc":"def"},{"abc":"def"}]') end end context ':xml' do before(:each) do subject.format :xml end it 'string' do subject.get '/example' do 'example' end get '/example' expect(last_response.status).to eq(500) expect(last_response.body).to eq <<-XML cannot convert String to xml XML end it 'hash' do subject.get '/example' do { example1: 'example1', example2: 'example2' } end get '/example' expect(last_response.status).to eq(200) expect(last_response.body).to eq <<-XML 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 describe '.configure' do context 'when given a block' do it 'returns self' do expect(subject.configure {}).to be subject end it 'calls the block passing the config' do call = [false, nil] subject.configure do |config| call = [true, config] end expect(call[0]).to be true expect(call[1]).not_to be_nil end end context 'when not given a block' do it 'returns a configuration object' do expect(subject.configure).to respond_to(:[], :[]=) end end it 'allows configuring the api' do subject.configure do |config| config[:hello] = 'hello' config[:bread] = 'bread' end subject.get '/hello-bread' do "#{configuration[:hello]} #{configuration[:bread]}" end get '/hello-bread' expect(last_response.body).to eq 'hello bread' end end context 'catch-all' do before do api1 = Class.new(Grape::API) api1.version 'v1', using: :path api1.get 'hello' do 'v1' end api2 = Class.new(Grape::API) api2.version 'v2', using: :path api2.get 'hello' do 'v2' end subject.mount api1 subject.mount api2 end [true, false].each do |anchor| it "anchor=#{anchor}" do subject.route :any, '*path', anchor: anchor do error!("Unrecognized request path: #{params[:path]} - #{env['PATH_INFO']}#{env['SCRIPT_NAME']}", 404) end get '/v1/hello' expect(last_response.status).to eq(200) expect(last_response.body).to eq('v1') get '/v2/hello' expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') options '/v2/hello' expect(last_response.status).to eq(204) expect(last_response.body).to be_blank head '/v2/hello' expect(last_response.status).to eq(200) expect(last_response.body).to be_blank get '/foobar' expect(last_response.status).to eq(404) expect(last_response.body).to eq('Unrecognized request path: foobar - /foobar') end end end context 'cascading' do context 'via version' do it 'cascades' do subject.version 'v1', using: :path, cascade: true get '/v1/hello' expect(last_response.status).to eq(404) expect(last_response.headers['X-Cascade']).to eq('pass') end it 'does not cascade' do subject.version 'v2', using: :path, cascade: false get '/v2/hello' expect(last_response.status).to eq(404) expect(last_response.headers.keys).not_to include 'X-Cascade' end end context 'via endpoint' do it 'cascades' do subject.cascade true get '/hello' expect(last_response.status).to eq(404) expect(last_response.headers['X-Cascade']).to eq('pass') end it 'does not cascade' do subject.cascade false get '/hello' expect(last_response.status).to eq(404) expect(last_response.headers.keys).not_to include 'X-Cascade' end end end context 'with json default_error_formatter' do it 'returns json error' do subject.content_type :json, 'application/json' subject.default_error_formatter :json subject.get '/something' do 'foo' end get '/something' expect(last_response.status).to eq(406) if ActiveSupport::VERSION::MAJOR == 3 expect(last_response.body).to eq('{"error":"The requested format 'txt' is not supported."}') else expect(last_response.body).to eq('{"error":"The requested format 'txt' is not supported."}') end end end context 'with unsafe HTML format specified' do it 'escapes the HTML' do subject.content_type :json, 'application/json' subject.get '/something' do 'foo' end get '/something?format=' expect(last_response.status).to eq(406) if ActiveSupport::VERSION::MAJOR == 3 expect(last_response.body).to eq('The requested format '<script>blah</script>' is not supported.') else expect(last_response.body).to eq('The requested format '<script>blah</script>' is not supported.') end 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 describe 'normal class methods' do subject(:grape_api) { Class.new(Grape::API) } before do stub_const('MyAPI', grape_api) end it 'can find the appropiate name' do expect(grape_api.name).to eq 'MyAPI' end it 'is equal to itself' do expect(grape_api.itself).to eq grape_api expect(grape_api).to eq MyAPI expect(grape_api.eql?(MyAPI)) end end describe 'const_missing' do subject(:grape_api) { Class.new(Grape::API) } let(:mounted) do Class.new(Grape::API) do get '/missing' do SomeRandomConstant end end end before { subject.mount mounted => '/const' } it 'raises an error' do expect { get '/const/missing' }.to raise_error(NameError).with_message(/SomeRandomConstant/) end end end grape-1.5.2/spec/grape/named_api_spec.rb0000644000004100000410000000060414016744305020155 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe 'A named API' do subject(:api_name) { NamedAPI.endpoints.last.options[:for].to_s } let(:api) do Class.new(Grape::API) do get 'test' do 'response' end end end before { stub_const('NamedAPI', api) } it 'can access the name of the API' do expect(api_name).to eq 'NamedAPI' end end grape-1.5.2/spec/grape/loading_spec.rb0000644000004100000410000000151114016744305017653 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API do let(:jobs_api) do Class.new(Grape::API) do namespace :one do namespace :two do namespace :three do get :one do end get :two do end end end end end end let(:combined_api) do JobsApi = jobs_api Class.new(Grape::API) do version :v1, using: :accept_version_header, cascade: true mount JobsApi end end subject do CombinedApi = combined_api Class.new(Grape::API) do format :json mount CombinedApi => '/' end end def app subject end it 'execute first request in reasonable time' do started = Time.now get '/mount1/nested/test_method' expect(Time.now - started).to be < 5 end end grape-1.5.2/spec/grape/dsl/0000755000004100000410000000000014016744305015463 5ustar www-datawww-datagrape-1.5.2/spec/grape/dsl/headers_spec.rb0000644000004100000410000000131414016744305020434 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module HeadersSpec class Dummy include Grape::DSL::Headers end end describe Headers do subject { HeadersSpec::Dummy.new } describe '#header' do describe 'set' do before do subject.header 'Name', 'Value' end it 'returns value' do expect(subject.header['Name']).to eq 'Value' expect(subject.header('Name')).to eq 'Value' end end it 'returns nil' do expect(subject.header['Name']).to be nil expect(subject.header('Name')).to be nil end end end end end grape-1.5.2/spec/grape/dsl/helpers_spec.rb0000644000004100000410000000532414016744305020470 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module HelpersSpec class Dummy include Grape::DSL::Helpers def self.mods namespace_stackable(:helpers) end def self.first_mod mods.first end end end module BooleanParam extend Grape::API::Helpers params :requires_toggle_prm do requires :toggle_prm, type: Boolean end end class Base < Grape::API helpers BooleanParam end class Child < Base; end describe Helpers do subject { Class.new(HelpersSpec::Dummy) } let(:proc) do lambda do |*| def test :test end end end describe '.helpers' do it 'adds a module with the given block' do expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original subject.helpers(&proc) expect(subject.first_mod.instance_methods).to include(:test) end it 'uses provided modules' do mod = Module.new expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(2).times expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original subject.helpers(mod, &proc) expect(subject.first_mod).to eq mod end it 'uses many provided modules' do mod = Module.new mod2 = Module.new mod3 = Module.new expect(subject).to receive(:namespace_stackable).with(:helpers, kind_of(Grape::DSL::Helpers::BaseHelper)).and_call_original.exactly(4).times expect(subject).to receive(:namespace_stackable).with(:helpers).and_call_original.exactly(3).times subject.helpers(mod, mod2, mod3, &proc) expect(subject.mods).to include(mod) expect(subject.mods).to include(mod2) expect(subject.mods).to include(mod3) end context 'with an external file' do it 'sets Boolean as a Grape::API::Boolean' do subject.helpers BooleanParam expect(subject.first_mod::Boolean).to eq Grape::API::Boolean end end context 'in child classes' do it 'is available' do klass = Child expect do klass.instance_eval do params do use :requires_toggle_prm end end end.to_not raise_exception end end end end end end grape-1.5.2/spec/grape/dsl/desc_spec.rb0000644000004100000410000000647314016744305017752 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module DescSpec class Dummy extend Grape::DSL::Desc end end describe Desc do subject { Class.new(DescSpec::Dummy) } describe '.desc' do it 'sets a description' do desc_text = 'The description' options = { message: 'none' } subject.desc desc_text, options expect(subject.namespace_setting(:description)).to eq(options.merge(description: desc_text)) expect(subject.route_setting(:description)).to eq(options.merge(description: desc_text)) end it 'can be set with a block' do expected_options = { summary: 'summary', description: 'The description', detail: 'more details', params: { first: :param }, entity: Object, http_codes: [[401, 'Unauthorized', 'Entities::Error']], named: 'My named route', body_name: 'My body name', headers: [ XAuthToken: { description: 'Valdates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } ], hidden: false, deprecated: false, is_array: true, nickname: 'nickname', produces: %w[array of mime_types], consumes: %w[array of mime_types], tags: %w[tag1 tag2], security: %w[array of security schemes] } subject.desc 'The description' do summary 'summary' detail 'more details' params(first: :param) success Object failure [[401, 'Unauthorized', 'Entities::Error']] named 'My named route' body_name 'My body name' headers [ XAuthToken: { description: 'Valdates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } ] hidden false deprecated false is_array true nickname 'nickname' produces %w[array of mime_types] consumes %w[array of mime_types] tags %w[tag1 tag2] security %w[array of security schemes] end expect(subject.namespace_setting(:description)).to eq(expected_options) expect(subject.route_setting(:description)).to eq(expected_options) end it 'can be set with options and a block' do expect(subject).to receive(:warn).with('[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.') desc_text = 'The description' detail_text = 'more details' options = { message: 'none' } subject.desc desc_text, options do detail detail_text end expect(subject.namespace_setting(:description)).to eq(description: desc_text, detail: detail_text) expect(subject.route_setting(:description)).to eq(description: desc_text, detail: detail_text) end end end end end grape-1.5.2/spec/grape/dsl/middleware_spec.rb0000644000004100000410000000350714016744305021144 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module MiddlewareSpec class Dummy include Grape::DSL::Middleware end end describe Middleware do subject { Class.new(MiddlewareSpec::Dummy) } let(:proc) { ->() {} } let(:foo_middleware) { Class.new } let(:bar_middleware) { Class.new } describe '.use' do it 'adds a middleware with the right operation' do expect(subject).to receive(:namespace_stackable).with(:middleware, [:use, foo_middleware, :arg1, proc]) subject.use foo_middleware, :arg1, &proc end end describe '.insert' do it 'adds a middleware with the right operation' do expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert, 0, :arg1, proc]) subject.insert 0, :arg1, &proc end end describe '.insert_before' do it 'adds a middleware with the right operation' do expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_before, foo_middleware, :arg1, proc]) subject.insert_before foo_middleware, :arg1, &proc end end describe '.insert_after' do it 'adds a middleware with the right operation' do expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_after, foo_middleware, :arg1, proc]) subject.insert_after foo_middleware, :arg1, &proc end end describe '.middleware' do it 'returns the middleware stack' do subject.use foo_middleware, :arg1, &proc subject.insert_before bar_middleware, :arg1, :arg2 expect(subject.middleware).to eq [[:use, foo_middleware, :arg1, proc], [:insert_before, bar_middleware, :arg1, :arg2]] end end end end end grape-1.5.2/spec/grape/dsl/request_response_spec.rb0000644000004100000410000002060214016744305022430 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module RequestResponseSpec class Dummy include Grape::DSL::RequestResponse def self.set(key, value) settings[key.to_sym] = value end def self.imbue(key, value) settings.imbue(key, value) end end end describe RequestResponse do subject { Class.new(RequestResponseSpec::Dummy) } let(:c_type) { 'application/json' } let(:format) { 'txt' } describe '.default_format' do it 'sets the default format' do expect(subject).to receive(:namespace_inheritable).with(:default_format, :format) subject.default_format :format end it 'returns the format without paramter' do subject.default_format :format expect(subject.default_format).to eq :format end end describe '.format' do it 'sets a new format' do expect(subject).to receive(:namespace_inheritable).with(:format, format.to_sym) expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Txt) subject.format format end end describe '.formatter' do it 'sets the formatter for a content type' do expect(subject).to receive(:namespace_stackable).with(:formatters, c_type.to_sym => :formatter) subject.formatter c_type, :formatter end end describe '.parser' do it 'sets a parser for a content type' do expect(subject).to receive(:namespace_stackable).with(:parsers, c_type.to_sym => :parser) subject.parser c_type, :parser end end describe '.default_error_formatter' do it 'sets a new error formatter' do expect(subject).to receive(:namespace_inheritable).with(:default_error_formatter, Grape::ErrorFormatter::Json) subject.default_error_formatter :json end end describe '.error_formatter' do it 'sets a error_formatter' do format = 'txt' expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter) subject.error_formatter format, :error_formatter end it 'understands syntactic sugar' do expect(subject).to receive(:namespace_stackable).with(:error_formatters, format.to_sym => :error_formatter) subject.error_formatter format, with: :error_formatter end end describe '.content_type' do it 'sets a content type for a format' do expect(subject).to receive(:namespace_stackable).with(:content_types, format.to_sym => c_type) subject.content_type format, c_type end end describe '.content_types' do it 'returns all content types' do expect(subject.content_types).to eq(xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', txt: 'text/plain', binary: 'application/octet-stream') end end describe '.default_error_status' do it 'sets a default error status' do expect(subject).to receive(:namespace_inheritable).with(:default_error_status, 500) subject.default_error_status 500 end end describe '.rescue_from' do describe ':all' do it 'sets rescue all to true' do expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, nil) subject.rescue_from :all end it 'sets given proc as rescue handler' do rescue_handler_proc = proc {} expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc) subject.rescue_from :all, rescue_handler_proc end it 'sets given block as rescue handler' do rescue_handler_proc = proc {} expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, rescue_handler_proc) subject.rescue_from :all, &rescue_handler_proc end it 'sets a rescue handler declared through :with option' do with_block = -> { 'hello' } expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:all_rescue_handler, an_instance_of(Proc)) subject.rescue_from :all, with: with_block end it 'abort if :with option value is not Symbol, String or Proc' do expect { subject.rescue_from :all, with: 1234 }.to raise_error(ArgumentError, "with: #{integer_class_name}, expected Symbol, String or Proc") end it 'abort if both :with option and block are passed' do expect do subject.rescue_from :all, with: -> { 'hello' } do error!('bye') end end.to raise_error(ArgumentError, 'both :with option and block cannot be passed') end end describe ':grape_exceptions' do it 'sets rescue all to true' do expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) subject.rescue_from :grape_exceptions end it 'sets rescue_grape_exceptions to true' do expect(subject).to receive(:namespace_inheritable).with(:rescue_all, true) expect(subject).to receive(:namespace_inheritable).with(:rescue_grape_exceptions, true) subject.rescue_from :grape_exceptions end end describe 'list of exceptions is passed' do it 'sets hash of exceptions as rescue handlers' do expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => nil) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError end it 'rescues only base handlers if rescue_subclasses: false option is passed' do expect(subject).to receive(:namespace_reverse_stackable).with(:base_only_rescue_handlers, StandardError => nil) expect(subject).to receive(:namespace_stackable).with(:rescue_options, rescue_subclasses: false) subject.rescue_from StandardError, rescue_subclasses: false end it 'sets given proc as rescue handler for each key in hash' do rescue_handler_proc = proc {} expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => rescue_handler_proc) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError, rescue_handler_proc end it 'sets given block as rescue handler for each key in hash' do rescue_handler_proc = proc {} expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => rescue_handler_proc) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError, &rescue_handler_proc end it 'sets a rescue handler declared through :with option for each key in hash' do with_block = -> { 'hello' } expect(subject).to receive(:namespace_reverse_stackable).with(:rescue_handlers, StandardError => an_instance_of(Proc)) expect(subject).to receive(:namespace_stackable).with(:rescue_options, {}) subject.rescue_from StandardError, with: with_block end end end describe '.represent' do it 'sets a presenter for a class' do presenter = Class.new expect(subject).to receive(:namespace_stackable).with(:representations, ThisClass: presenter) subject.represent :ThisClass, with: presenter end end end end end grape-1.5.2/spec/grape/dsl/settings_spec.rb0000644000004100000410000002023614016744305020665 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module SettingsSpec class Dummy include Grape::DSL::Settings def reset_validations!; end end end describe Settings do subject { SettingsSpec::Dummy.new } describe '#unset' do it 'deletes a key from settings' do subject.namespace_setting :dummy, 1 expect(subject.inheritable_setting.namespace.keys).to include(:dummy) subject.unset :namespace, :dummy expect(subject.inheritable_setting.namespace.keys).not_to include(:dummy) end end describe '#get_or_set' do it 'sets a values' do subject.get_or_set :namespace, :dummy, 1 expect(subject.namespace_setting(:dummy)).to eq 1 end it 'returns a value when nil is new value is provided' do subject.get_or_set :namespace, :dummy, 1 expect(subject.get_or_set(:namespace, :dummy, nil)).to eq 1 end end describe '#global_setting' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:global, :dummy, 1) subject.global_setting(:dummy, 1) end end describe '#unset_global_setting' do it 'delegates to unset' do expect(subject).to receive(:unset).with(:global, :dummy) subject.unset_global_setting(:dummy) end end describe '#route_setting' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:route, :dummy, 1) subject.route_setting(:dummy, 1) end it 'sets a value until the next route' do subject.route_setting :some_thing, :foo_bar expect(subject.route_setting(:some_thing)).to eq :foo_bar subject.route_end expect(subject.route_setting(:some_thing)).to be_nil end end describe '#unset_route_setting' do it 'delegates to unset' do expect(subject).to receive(:unset).with(:route, :dummy) subject.unset_route_setting(:dummy) end end describe '#namespace_setting' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:namespace, :dummy, 1) subject.namespace_setting(:dummy, 1) end it 'sets a value until the end of a namespace' do subject.namespace_start subject.namespace_setting :some_thing, :foo_bar expect(subject.namespace_setting(:some_thing)).to eq :foo_bar subject.namespace_end expect(subject.namespace_setting(:some_thing)).to be_nil end it 'resets values after leaving nested namespaces' do subject.namespace_start subject.namespace_setting :some_thing, :foo_bar expect(subject.namespace_setting(:some_thing)).to eq :foo_bar subject.namespace_start expect(subject.namespace_setting(:some_thing)).to be_nil subject.namespace_end expect(subject.namespace_setting(:some_thing)).to eq :foo_bar subject.namespace_end expect(subject.namespace_setting(:some_thing)).to be_nil end end describe '#unset_namespace_setting' do it 'delegates to unset' do expect(subject).to receive(:unset).with(:namespace, :dummy) subject.unset_namespace_setting(:dummy) end end describe '#namespace_inheritable' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:namespace_inheritable, :dummy, 1) subject.namespace_inheritable(:dummy, 1) end it 'inherits values from surrounding namespace' do subject.namespace_start subject.namespace_inheritable(:some_thing, :foo_bar) expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar subject.namespace_start expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar subject.namespace_inheritable(:some_thing, :foo_bar_2) expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar_2 subject.namespace_end expect(subject.namespace_inheritable(:some_thing)).to eq :foo_bar subject.namespace_end end end describe '#unset_namespace_inheritable' do it 'delegates to unset' do expect(subject).to receive(:unset).with(:namespace_inheritable, :dummy) subject.unset_namespace_inheritable(:dummy) end end describe '#namespace_stackable' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:namespace_stackable, :dummy, 1) subject.namespace_stackable(:dummy, 1) end it 'stacks values from surrounding namespace' do subject.namespace_start subject.namespace_stackable(:some_thing, :foo_bar) expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] subject.namespace_start expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] subject.namespace_stackable(:some_thing, :foo_bar_2) expect(subject.namespace_stackable(:some_thing)).to eq %i[foo_bar foo_bar_2] subject.namespace_end expect(subject.namespace_stackable(:some_thing)).to eq [:foo_bar] subject.namespace_end end end describe '#unset_namespace_stackable' do it 'delegates to unset' do expect(subject).to receive(:unset).with(:namespace_stackable, :dummy) subject.unset_namespace_stackable(:dummy) end end describe '#api_class_setting' do it 'delegates to get_or_set' do expect(subject).to receive(:get_or_set).with(:api_class, :dummy, 1) subject.api_class_setting(:dummy, 1) end end describe '#unset_api_class_setting' do it 'delegates to unset' do expect(subject).to receive(:unset).with(:api_class, :dummy) subject.unset_api_class_setting(:dummy) end end describe '#within_namespace' do it 'calls start and end for a namespace' do expect(subject).to receive :namespace_start expect(subject).to receive :namespace_end subject.within_namespace do end end it 'returns the last result' do result = subject.within_namespace do 1 end expect(result).to eq 1 end end describe 'complex scenario' do it 'plays well' do obj1 = SettingsSpec::Dummy.new obj2 = SettingsSpec::Dummy.new obj3 = SettingsSpec::Dummy.new obj1_copy = nil obj2_copy = nil obj3_copy = nil obj1.within_namespace do obj1.namespace_stackable(:some_thing, :obj1) expect(obj1.namespace_stackable(:some_thing)).to eq [:obj1] obj1_copy = obj1.inheritable_setting.point_in_time_copy end expect(obj1.namespace_stackable(:some_thing)).to eq [] expect(obj1_copy.namespace_stackable[:some_thing]).to eq [:obj1] obj2.within_namespace do obj2.namespace_stackable(:some_thing, :obj2) expect(obj2.namespace_stackable(:some_thing)).to eq [:obj2] obj2_copy = obj2.inheritable_setting.point_in_time_copy end expect(obj2.namespace_stackable(:some_thing)).to eq [] expect(obj2_copy.namespace_stackable[:some_thing]).to eq [:obj2] obj3.within_namespace do obj3.namespace_stackable(:some_thing, :obj3) expect(obj3.namespace_stackable(:some_thing)).to eq [:obj3] obj3_copy = obj3.inheritable_setting.point_in_time_copy end expect(obj3.namespace_stackable(:some_thing)).to eq [] expect(obj3_copy.namespace_stackable[:some_thing]).to eq [:obj3] obj1.top_level_setting.inherit_from obj2_copy.point_in_time_copy obj2.top_level_setting.inherit_from obj3_copy.point_in_time_copy expect(obj1_copy.namespace_stackable[:some_thing]).to eq %i[obj3 obj2 obj1] end end end end end grape-1.5.2/spec/grape/dsl/configuration_spec.rb0000644000004100000410000000044114016744305021670 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module ConfigurationSpec class Dummy include Grape::DSL::Configuration end end describe Configuration do subject { Class.new(ConfigurationSpec::Dummy) } end end end grape-1.5.2/spec/grape/dsl/parameters_spec.rb0000644000004100000410000001317614016744305021175 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module ParametersSpec class Dummy include Grape::DSL::Parameters attr_accessor :api, :element, :parent def validate_attributes(*args) @validate_attributes = *args end def validate_attributes_reader @validate_attributes end def push_declared_params(args, **_opts) @push_declared_params = args end def push_declared_params_reader @push_declared_params end def validates(*args) @validates = *args end def validates_reader @validates end def new_group_scope(args) @group = args.clone.first yield end def extract_message_option(attrs) return nil unless attrs.is_a?(Array) opts = attrs.last.is_a?(Hash) ? attrs.pop : {} opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil end end end describe Parameters do subject { ParametersSpec::Dummy.new } describe '#use' do before do allow_message_expectations_on_nil allow(subject.api).to receive(:namespace_stackable).with(:named_params) end let(:options) { { option: 'value' } } let(:named_params) { { params_group: proc {} } } it 'calls processes associated with named params' do allow(subject.api).to receive(:namespace_stackable_with_hash).and_return(named_params) expect(subject).to receive(:instance_exec).with(options).and_yield subject.use :params_group, options end it 'raises error when non-existent named param is called' do allow(subject.api).to receive(:namespace_stackable_with_hash).and_return({}) expect { subject.use :params_group }.to raise_error('Params :params_group not found!') end end describe '#use_scope' do it 'is alias to #use' do expect(subject.method(:use_scope)).to eq subject.method(:use) end end describe '#includes' do it 'is alias to #use' do expect(subject.method(:includes)).to eq subject.method(:use) end end describe '#requires' do it 'adds a required parameter' do subject.requires :id, type: Integer, desc: 'Identity.' expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }]) expect(subject.push_declared_params_reader).to eq([:id]) end end describe '#optional' do it 'adds an optional parameter' do subject.optional :id, type: Integer, desc: 'Identity.' expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) expect(subject.push_declared_params_reader).to eq([:id]) end end describe '#with' do it 'creates a scope with group attributes' do subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' } expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) expect(subject.push_declared_params_reader).to eq([:id]) end end describe '#mutually_exclusive' do it 'adds an mutally exclusive parameter validation' do subject.mutually_exclusive :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { mutual_exclusion: { value: true, message: nil } }]) end end describe '#exactly_one_of' do it 'adds an exactly of one parameter validation' do subject.exactly_one_of :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { exactly_one_of: { value: true, message: nil } }]) end end describe '#at_least_one_of' do it 'adds an at least one of parameter validation' do subject.at_least_one_of :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { at_least_one_of: { value: true, message: nil } }]) end end describe '#all_or_none_of' do it 'adds an all or none of parameter validation' do subject.all_or_none_of :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { all_or_none_of: { value: true, message: nil } }]) end end describe '#group' do it 'is alias to #requires' do expect(subject.method(:group)).to eq subject.method(:requires) end end describe '#params' do it 'inherits params from parent' do parent_params = { foo: 'bar' } subject.parent = Object.new allow(subject.parent).to receive(:params).and_return(parent_params) expect(subject.params({})).to eq parent_params end describe 'when params argument is an array of hashes' do it 'returns values of each hash for @element key' do subject.element = :foo expect(subject.params([{ foo: 'bar' }, { foo: 'baz' }])).to eq(%w[bar baz]) end end describe 'when params argument is a hash' do it 'returns value for @element key' do subject.element = :foo expect(subject.params(foo: 'bar')).to eq('bar') end end describe 'when params argument is not a array or a hash' do it 'returns empty hash' do subject.element = Object.new expect(subject.params(Object.new)).to eq({}) end end end end end end grape-1.5.2/spec/grape/dsl/inside_route_spec.rb0000644000004100000410000003312314016744305021515 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module InsideRouteSpec class Dummy include Grape::DSL::InsideRoute attr_reader :env, :request, :new_settings def initialize @env = {} @header = {} @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } end end end end end describe Grape::Endpoint do subject { Grape::DSL::InsideRouteSpec::Dummy.new } describe '#version' do it 'defaults to nil' do expect(subject.version).to be nil end it 'returns env[api.version]' do subject.env['api.version'] = 'dummy' expect(subject.version).to eq 'dummy' end end describe '#error!' do it 'throws :error' do expect { subject.error! 'Not Found', 404 }.to throw_symbol(:error) end describe 'thrown' do before do catch(:error) { subject.error! 'Not Found', 404 } end it 'sets status' do expect(subject.status).to eq 404 end end describe 'default_error_status' do before do subject.namespace_inheritable(:default_error_status, 500) catch(:error) { subject.error! 'Unknown' } end it 'sets status to default_error_status' do expect(subject.status).to eq 500 end end # self.status(status || settings[:default_error_status]) # throw :error, message: message, status: self.status, headers: headers end describe '#redirect' do describe 'default' do before do subject.redirect '/' end it 'sets status to 302' do expect(subject.status).to eq 302 end it 'sets location header' do expect(subject.header['Location']).to eq '/' end end describe 'permanent' do before do subject.redirect '/', permanent: true end it 'sets status to 301' do expect(subject.status).to eq 301 end it 'sets location header' do expect(subject.header['Location']).to eq '/' end end end describe '#status' do %w[GET PUT OPTIONS].each do |method| it 'defaults to 200 on GET' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: method)) expect(subject).to receive(:request).and_return(request) expect(subject.status).to eq 200 end end it 'defaults to 201 on POST' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'POST')) expect(subject).to receive(:request).and_return(request) expect(subject.status).to eq 201 end it 'defaults to 204 on DELETE' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE')) expect(subject).to receive(:request).and_return(request) expect(subject.status).to eq 204 end it 'defaults to 200 on DELETE with a body present' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE')) subject.body 'content here' expect(subject).to receive(:request).and_return(request) expect(subject.status).to eq 200 end it 'returns status set' do subject.status 501 expect(subject.status).to eq 501 end it 'accepts symbol for status' do subject.status :see_other expect(subject.status).to eq 303 end it 'raises error if unknow symbol is passed' do expect { subject.status :foo_bar } .to raise_error(ArgumentError, 'Status code :foo_bar is invalid.') end it 'accepts unknown Integer status codes' do expect { subject.status 210 }.to_not raise_error end it 'raises error if status is not a integer or symbol' do expect { subject.status Object.new } .to raise_error(ArgumentError, 'Status code must be Integer or Symbol.') end end describe '#return_no_content' do it 'sets the status code and body' do subject.return_no_content expect(subject.status).to eq 204 expect(subject.body).to eq '' end end describe '#content_type' do describe 'set' do before do subject.content_type 'text/plain' end it 'returns value' do expect(subject.content_type).to eq 'text/plain' end end it 'returns default' do expect(subject.content_type).to be nil end end describe '#cookies' do it 'returns an instance of Cookies' do expect(subject.cookies).to be_a Grape::Cookies end end describe '#body' do describe 'set' do before do subject.body 'body' end it 'returns value' do expect(subject.body).to eq 'body' end end describe 'false' do before do subject.body false end it 'sets status to 204' do expect(subject.body).to eq '' expect(subject.status).to eq 204 end end it 'returns default' do expect(subject.body).to be nil end end describe '#file' do before do allow(subject).to receive(:warn) end describe 'set' do context 'as file path' do let(:file_path) { '/some/file/path' } it 'emits a warning that this method is deprecated' do expect(subject).to receive(:warn).with(/Use sendfile or stream/) subject.file file_path end it 'forwards the call to sendfile' do expect(subject).to receive(:sendfile).with(file_path) subject.file file_path end end context 'as object (backward compatibility)' do let(:file_object) { double('StreamerObject', each: nil) } it 'emits a warning that this method is deprecated' do expect(subject).to receive(:warn).with(/Use stream to use a Stream object/) subject.file file_object end it 'forwards the call to stream' do expect(subject).to receive(:stream).with(file_object) subject.file file_object end end end describe 'get' do it 'emits a warning that this method is deprecated' do expect(subject).to receive(:warn).with(/Use sendfile or stream/) subject.file end it 'fowards call to sendfile' do expect(subject).to receive(:sendfile) subject.file end end end describe '#sendfile' do describe 'set' do context 'as file path' do let(:file_path) { '/some/file/path' } let(:file_response) do file_body = Grape::ServeStream::FileBody.new(file_path) Grape::ServeStream::StreamResponse.new(file_body) end before do subject.header 'Cache-Control', 'cache' subject.header 'Content-Length', 123 subject.header 'Transfer-Encoding', 'base64' end it 'sends no deprecation warnings' do expect(subject).to_not receive(:warn) subject.sendfile file_path end it 'returns value wrapped in StreamResponse' do subject.sendfile file_path expect(subject.sendfile).to eq file_response end it 'does not change the Cache-Control header' do subject.sendfile file_path expect(subject.header['Cache-Control']).to eq 'cache' end it 'does not change the Content-Length header' do subject.sendfile file_path expect(subject.header['Content-Length']).to eq 123 end it 'does not change the Transfer-Encoding header' do subject.sendfile file_path expect(subject.header['Transfer-Encoding']).to eq 'base64' end end context 'as object' do let(:file_object) { double('StreamerObject', each: nil) } it 'raises an error that only a file path is supported' do expect { subject.sendfile file_object }.to raise_error(ArgumentError, /Argument must be a file path/) end end end it 'returns default' do expect(subject.sendfile).to be nil end end describe '#stream' do describe 'set' do context 'as a file path' do let(:file_path) { '/some/file/path' } let(:file_response) do file_body = Grape::ServeStream::FileBody.new(file_path) Grape::ServeStream::StreamResponse.new(file_body) end before do subject.header 'Cache-Control', 'cache' subject.header 'Content-Length', 123 subject.header 'Transfer-Encoding', 'base64' end it 'emits no deprecation warnings' do expect(subject).to_not receive(:warn) subject.stream file_path end it 'returns file body wrapped in StreamResponse' do subject.stream file_path expect(subject.stream).to eq file_response end it 'sets Cache-Control header to no-cache' do subject.stream file_path expect(subject.header['Cache-Control']).to eq 'no-cache' end it 'does not change Cache-Control header' do subject.stream expect(subject.header['Cache-Control']).to eq 'cache' end it 'sets Content-Length header to nil' do subject.stream file_path expect(subject.header['Content-Length']).to eq nil end it 'sets Transfer-Encoding header to nil' do subject.stream file_path expect(subject.header['Transfer-Encoding']).to eq nil end end context 'as a stream object' do let(:stream_object) { double('StreamerObject', each: nil) } let(:stream_response) do Grape::ServeStream::StreamResponse.new(stream_object) end before do subject.header 'Cache-Control', 'cache' subject.header 'Content-Length', 123 subject.header 'Transfer-Encoding', 'base64' end it 'emits no deprecation warnings' do expect(subject).to_not receive(:warn) subject.stream stream_object end it 'returns value wrapped in StreamResponse' do subject.stream stream_object expect(subject.stream).to eq stream_response end it 'sets Cache-Control header to no-cache' do subject.stream stream_object expect(subject.header['Cache-Control']).to eq 'no-cache' end it 'sets Content-Length header to nil' do subject.stream stream_object expect(subject.header['Content-Length']).to eq nil end it 'sets Transfer-Encoding header to nil' do subject.stream stream_object expect(subject.header['Transfer-Encoding']).to eq nil end end context 'as a non-stream object' do let(:non_stream_object) { double('NonStreamerObject') } it 'raises an error that the object must implement :each' do expect { subject.stream non_stream_object }.to raise_error(ArgumentError, /:each/) end end end it 'returns default' do expect(subject.stream).to be nil expect(subject.header['Cache-Control']).to eq nil end end describe '#route' do before do subject.env['grape.routing_args'] = {} subject.env['grape.routing_args'][:route_info] = 'dummy' end it 'returns route_info' do expect(subject.route).to eq 'dummy' end end describe '#present' do # see entity_spec.rb for entity representation spec coverage describe 'dummy' do before do subject.present 'dummy' end it 'presents dummy object' do expect(subject.body).to eq 'dummy' end end describe 'with' do describe 'entity' do let(:entity_mock) do entity_mock = Object.new allow(entity_mock).to receive(:represent).and_return('dummy') entity_mock end describe 'instance' do before do subject.present 'dummy', with: entity_mock end it 'presents dummy object' do expect(subject.body).to eq 'dummy' end end end end describe 'multiple entities' do let(:entity_mock_one) do entity_mock_one = Object.new allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1') entity_mock_one end let(:entity_mock_two) do entity_mock_two = Object.new allow(entity_mock_two).to receive(:represent).and_return(dummy2: 'dummy2') entity_mock_two end describe 'instance' do before do subject.present 'dummy1', with: entity_mock_one subject.present 'dummy2', with: entity_mock_two end it 'presents both dummy objects' do expect(subject.body[:dummy1]).to eq 'dummy1' expect(subject.body[:dummy2]).to eq 'dummy2' end end end describe 'non mergeable entity' do let(:entity_mock_one) do entity_mock_one = Object.new allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1') entity_mock_one end let(:entity_mock_two) do entity_mock_two = Object.new allow(entity_mock_two).to receive(:represent).and_return('not a hash') entity_mock_two end describe 'instance' do it 'fails' do subject.present 'dummy1', with: entity_mock_one expect do subject.present 'dummy2', with: entity_mock_two end.to raise_error ArgumentError, 'Representation of type String cannot be merged.' end end end end describe '#declared' do # see endpoint_spec.rb#declared for spec coverage it 'is not available by default' do expect { subject.declared({}) }.to raise_error( Grape::DSL::InsideRoute::MethodNotYetAvailable ) end end end grape-1.5.2/spec/grape/dsl/routing_spec.rb0000644000004100000410000002146214016744305020516 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module RoutingSpec class Dummy include Grape::DSL::Routing end end describe Routing do subject { Class.new(RoutingSpec::Dummy) } let(:proc) { ->() {} } let(:options) { { a: :b } } let(:path) { '/dummy' } describe '.version' do it 'sets a version for route' do version = 'v1' expect(subject).to receive(:namespace_inheritable).with(:version, [version]) expect(subject).to receive(:namespace_inheritable).with(:version_options, using: :path) expect(subject.version(version)).to eq(version) end end describe '.prefix' do it 'sets a prefix for route' do prefix = '/api' expect(subject).to receive(:namespace_inheritable).with(:root_prefix, prefix) subject.prefix prefix end end describe '.scope' do it 'create a scope without affecting the URL' do expect(subject).to receive(:within_namespace) subject.scope {} end end describe '.do_not_route_head!' do it 'sets do not route head option' do expect(subject).to receive(:namespace_inheritable).with(:do_not_route_head, true) subject.do_not_route_head! end end describe '.do_not_route_options!' do it 'sets do not route options option' do expect(subject).to receive(:namespace_inheritable).with(:do_not_route_options, true) subject.do_not_route_options! end end describe '.mount' do it 'mounts on a nested path' do subject = Class.new(Grape::API) app1 = Class.new(Grape::API) app2 = Class.new(Grape::API) app2.get '/nice' do 'play' end subject.mount app1 => '/app1' app1.mount app2 => '/app2' expect(subject.inheritable_setting.to_hash[:namespace]).to eq({}) expect(subject.inheritable_setting.to_hash[:namespace_inheritable]).to eq({}) expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1']) expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1', '/app2']) end it 'mounts multiple routes at once' do base_app = Class.new(Grape::API) app1 = Class.new(Grape::API) app2 = Class.new(Grape::API) base_app.mount(app1 => '/app1', app2 => '/app2') 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: ['/app2']) end end describe '.route' do before do allow(subject).to receive(:endpoints).and_return([]) allow(subject).to receive(:route_end) allow(subject).to receive(:reset_validations!) end it 'marks end of the route' do expect(subject).to receive(:route_end) subject.route(:any) end it 'resets validations' do expect(subject).to receive(:reset_validations!) subject.route(:any) end it 'defines a new endpoint' do expect { subject.route(:any) } .to change { subject.endpoints.count }.from(0).to(1) end it 'does not duplicate identical endpoints' do subject.route(:any) expect { subject.route(:any) } .to_not change(subject.endpoints, :count) end it 'generates correct endpoint options' do allow(subject).to receive(:route_setting).with(:description).and_return(fiz: 'baz') allow(subject).to receive(:namespace_stackable_with_hash).and_return(nuz: 'naz') expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options| expect(endpoint_options[:method]).to eq :get expect(endpoint_options[:path]).to eq '/foo' expect(endpoint_options[:for]).to eq subject expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz', params: { nuz: 'naz' }) end.and_yield subject.route(:get, '/foo', { foo: 'bar' }, &proc {}) end end describe '.get' do it 'delegates to .route' do expect(subject).to receive(:route).with('GET', path, options) subject.get path, options, &proc end end describe '.post' do it 'delegates to .route' do expect(subject).to receive(:route).with('POST', path, options) subject.post path, options, &proc end end describe '.put' do it 'delegates to .route' do expect(subject).to receive(:route).with('PUT', path, options) subject.put path, options, &proc end end describe '.head' do it 'delegates to .route' do expect(subject).to receive(:route).with('HEAD', path, options) subject.head path, options, &proc end end describe '.delete' do it 'delegates to .route' do expect(subject).to receive(:route).with('DELETE', path, options) subject.delete path, options, &proc end end describe '.options' do it 'delegates to .route' do expect(subject).to receive(:route).with('OPTIONS', path, options) subject.options path, options, &proc end end describe '.patch' do it 'delegates to .route' do expect(subject).to receive(:route).with('PATCH', path, options) subject.patch path, options, &proc end end describe '.namespace' do let(:new_namespace) { Object.new } it 'creates a new namespace with given name and options' do expect(subject).to receive(:within_namespace).and_yield expect(subject).to receive(:nest).and_yield expect(Namespace).to receive(:new).with(:foo, foo: 'bar').and_return(new_namespace) expect(subject).to receive(:namespace_stackable).with(:namespace, new_namespace) subject.namespace :foo, foo: 'bar', &proc {} end it 'calls #joined_space_path on Namespace' do result_of_namspace_stackable = Object.new allow(subject).to receive(:namespace_stackable).and_return(result_of_namspace_stackable) expect(Namespace).to receive(:joined_space_path).with(result_of_namspace_stackable) subject.namespace end end describe '.group' do it 'is alias to #namespace' do expect(subject.method(:group)).to eq subject.method(:namespace) end end describe '.resource' do it 'is alias to #namespace' do expect(subject.method(:resource)).to eq subject.method(:namespace) end end describe '.resources' do it 'is alias to #namespace' do expect(subject.method(:resources)).to eq subject.method(:namespace) end end describe '.segment' do it 'is alias to #namespace' do expect(subject.method(:segment)).to eq subject.method(:namespace) end end describe '.routes' do let(:routes) { Object.new } it 'returns value received from #prepare_routes' do expect(subject).to receive(:prepare_routes).and_return(routes) expect(subject.routes).to eq routes end context 'when #routes was already called once' do before do allow(subject).to receive(:prepare_routes).and_return(routes) subject.routes end it 'it does not call prepare_routes again' do expect(subject).to_not receive(:prepare_routes) expect(subject.routes).to eq routes end end end describe '.route_param' do it 'calls #namespace with given params' do expect(subject).to receive(:namespace).with(':foo', {}).and_yield subject.route_param('foo', {}, &proc {}) end let(:regex) { /(.*)/ } let!(:options) { { requirements: regex } } it 'nests requirements option under param name' do expect(subject).to receive(:namespace) do |_param, options| expect(options[:requirements][:foo]).to eq regex end subject.route_param('foo', options, &proc {}) end it 'does not modify options parameter' do allow(subject).to receive(:namespace) expect { subject.route_param('foo', options, &proc {}) } .to_not change { options } end end describe '.versions' do it 'returns last defined version' do subject.version 'v1' subject.version 'v2' expect(subject.version).to eq('v2') end end end end end grape-1.5.2/spec/grape/dsl/validations_spec.rb0000644000004100000410000000401014016744305021332 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module ValidationsSpec class Dummy include Grape::DSL::Validations end end describe Validations do subject { ValidationsSpec::Dummy } describe '.reset_validations!' do before do subject.namespace_stackable :declared_params, ['dummy'] subject.namespace_stackable :validations, ['dummy'] subject.namespace_stackable :params, ['dummy'] subject.route_setting :description, description: 'lol', params: ['dummy'] subject.reset_validations! end after do subject.unset_route_setting :description end it 'resets declared params' do expect(subject.namespace_stackable(:declared_params)).to eq [] end it 'resets validations' do expect(subject.namespace_stackable(:validations)).to eq [] end it 'resets params' do expect(subject.namespace_stackable(:params)).to eq [] end it 'resets documentation params' do expect(subject.route_setting(:description)[:params]).to be_nil end it 'does not reset documentation description' do expect(subject.route_setting(:description)[:description]).to eq 'lol' end end describe '.params' do it 'returns a ParamsScope' do expect(subject.params).to be_a Grape::Validations::ParamsScope end it 'evaluates block' do expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo' end end describe '.document_attribute' do before do subject.document_attribute([full_name: 'xxx'], foo: 'bar') end it 'creates a param documentation' do expect(subject.namespace_stackable(:params)).to eq(['xxx' => { foo: 'bar' }]) expect(subject.route_setting(:description)).to eq(params: { 'xxx' => { foo: 'bar' } }) end end end end end grape-1.5.2/spec/grape/dsl/callbacks_spec.rb0000644000004100000410000000225714016744305020747 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module CallbacksSpec class Dummy include Grape::DSL::Callbacks end end describe Callbacks do subject { Class.new(CallbacksSpec::Dummy) } let(:proc) { ->() {} } describe '.before' do it 'adds a block to "before"' do expect(subject).to receive(:namespace_stackable).with(:befores, proc) subject.before(&proc) end end describe '.before_validation' do it 'adds a block to "before_validation"' do expect(subject).to receive(:namespace_stackable).with(:before_validations, proc) subject.before_validation(&proc) end end describe '.after_validation' do it 'adds a block to "after_validation"' do expect(subject).to receive(:namespace_stackable).with(:after_validations, proc) subject.after_validation(&proc) end end describe '.after' do it 'adds a block to "after"' do expect(subject).to receive(:namespace_stackable).with(:afters, proc) subject.after(&proc) end end end end end grape-1.5.2/spec/grape/dsl/logger_spec.rb0000644000004100000410000000105614016744305020303 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module DSL module LoggerSpec class Dummy extend Grape::DSL::Logger end end describe Logger do subject { Class.new(LoggerSpec::Dummy) } let(:logger) { double(:logger) } describe '.logger' do it 'sets a logger' do subject.logger logger expect(subject.logger).to eq logger end it 'returns a logger' do expect(subject.logger(logger)).to eq logger end end end end end grape-1.5.2/spec/grape/config_spec.rb0000644000004100000410000000046614016744305017513 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe '.configure' do before do Grape.configure do |config| config.param_builder = 42 end end after do Grape.config.reset end it 'is configured to the new value' do expect(Grape.config.param_builder).to eq 42 end end grape-1.5.2/spec/grape/endpoint_spec.rb0000644000004100000410000010307414016744305020065 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end describe '.before_each' do after { Grape::Endpoint.before_each.clear } it 'is settable via block' do block = ->(_endpoint) { 'noop' } Grape::Endpoint.before_each(&block) expect(Grape::Endpoint.before_each.first).to eq(block) end it 'is settable via reference' do block = ->(_endpoint) { 'noop' } Grape::Endpoint.before_each block expect(Grape::Endpoint.before_each.first).to eq(block) end it 'is able to override a helper' do subject.get('/') { current_user } expect { get '/' }.to raise_error(NameError) Grape::Endpoint.before_each do |endpoint| allow(endpoint).to receive(:current_user).and_return('Bob') end get '/' expect(last_response.body).to eq('Bob') Grape::Endpoint.before_each(nil) expect { get '/' }.to raise_error(NameError) end it 'is able to stack helper' do subject.get('/') do authenticate_user! current_user end expect { get '/' }.to raise_error(NameError) Grape::Endpoint.before_each do |endpoint| allow(endpoint).to receive(:current_user).and_return('Bob') end Grape::Endpoint.before_each do |endpoint| allow(endpoint).to receive(:authenticate_user!).and_return(true) end get '/' expect(last_response.body).to eq('Bob') Grape::Endpoint.before_each(nil) expect { get '/' }.to raise_error(NameError) end end describe '#initialize' do it 'takes a settings stack, options, and a block' do p = proc {} expect do Grape::Endpoint.new(Grape::Util::InheritableSetting.new, { path: '/', method: :get }, &p) end.not_to raise_error end end it 'sets itself in the env upon call' do subject.get('/') { 'Hello world.' } get '/' expect(last_request.env['api.endpoint']).to be_kind_of(Grape::Endpoint) end describe '#status' do it 'is callable from within a block' do subject.get('/home') do status 206 'Hello' end get '/home' expect(last_response.status).to eq(206) expect(last_response.body).to eq('Hello') end it 'is set as default to 200 for get' do memoized_status = nil subject.get('/home') do memoized_status = status 'Hello' end get '/home' expect(last_response.status).to eq(200) expect(memoized_status).to eq(200) expect(last_response.body).to eq('Hello') end it 'is set as default to 201 for post' do memoized_status = nil subject.post('/home') do memoized_status = status 'Hello' end post '/home' expect(last_response.status).to eq(201) expect(memoized_status).to eq(201) expect(last_response.body).to eq('Hello') end end describe '#header' do it 'is callable from within a block' do subject.get('/hey') do header 'X-Awesome', 'true' 'Awesome' end get '/hey' expect(last_response.headers['X-Awesome']).to eq('true') end end describe '#headers' do before do subject.get('/headers') do headers.to_json end end it 'includes request headers' do get '/headers' expect(JSON.parse(last_response.body)).to eq( 'Host' => 'example.org', 'Cookie' => '' ) end it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' expect(JSON.parse(last_response.body)['X-Grape-Client']).to eq('1') end it 'includes headers passed as symbols' do env = Rack::MockRequest.env_for('/headers') env['HTTP_SYMBOL_HEADER'.to_sym] = 'Goliath passes symbols' body = read_chunks(subject.call(env)[2]).join expect(JSON.parse(body)['Symbol-Header']).to eq('Goliath passes symbols') end end describe '#cookies' do it 'is callable from within a block' do subject.get('/get/cookies') do cookies['my-awesome-cookie1'] = 'is cool' cookies['my-awesome-cookie2'] = { value: 'is cool too', domain: 'my.example.com', path: '/', secure: true } cookies[:cookie3] = 'symbol' cookies['cookie4'] = 'secret code here' end get('/get/cookies') expect(last_response.headers['Set-Cookie'].split("\n").sort).to eql [ 'cookie3=symbol', 'cookie4=secret+code+here', 'my-awesome-cookie1=is+cool', 'my-awesome-cookie2=is+cool+too; domain=my.example.com; path=/; secure' ] end it 'sets browser cookies and does not set response cookies' do subject.get('/username') do cookies[:username] end get('/username', {}, 'HTTP_COOKIE' => 'username=mrplum; sandbox=true') expect(last_response.body).to eq('mrplum') expect(last_response.headers['Set-Cookie']).to be_nil end it 'sets and update browser cookies' do subject.get('/username') do cookies[:sandbox] = true if cookies[:sandbox] == 'false' cookies[:username] += '_test' end get('/username', {}, 'HTTP_COOKIE' => 'username=user; sandbox=false') expect(last_response.body).to eq('user_test') expect(last_response.headers['Set-Cookie']).to match(/username=user_test/) expect(last_response.headers['Set-Cookie']).to match(/sandbox=true/) end it 'deletes cookie' do subject.get('/test') do sum = 0 cookies.each do |name, val| sum += val.to_i cookies.delete name end sum end get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2' expect(last_response.body).to eq('3') cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] end] expect(cookies.size).to eq(2) %w[and_this delete_this_cookie].each do |cookie_name| cookie = cookies[cookie_name] expect(cookie).not_to be_nil expect(cookie.value).to eq('deleted') expect(cookie.expired?).to be true end end it 'deletes cookies with path' do subject.get('/test') do sum = 0 cookies.each do |name, val| sum += val.to_i cookies.delete name, path: '/test' end sum end get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2') expect(last_response.body).to eq('3') cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie| cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie [cookie.name, cookie] end] expect(cookies.size).to eq(2) %w[and_this delete_this_cookie].each do |cookie_name| cookie = cookies[cookie_name] expect(cookie).not_to be_nil expect(cookie.value).to eq('deleted') expect(cookie.path).to eq('/test') expect(cookie.expired?).to be true end end end describe '#params' do context 'default class' do it 'should be a ActiveSupport::HashWithIndifferentAccess' do subject.get '/foo' do params.class end get '/foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') end end context 'sets a value to params' do it 'params' do subject.params do requires :a, type: String end subject.get '/foo' do params[:a] = 'bar' end get '/foo', a: 'foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('bar') end end end describe '#params' do it 'is available to the caller' do subject.get('/hey') do params[:howdy] end get '/hey?howdy=hey' expect(last_response.body).to eq('hey') end it 'parses from path segments' do subject.get('/hey/:id') do params[:id] end get '/hey/12' expect(last_response.body).to eq('12') end it 'deeply converts nested params' do subject.get '/location' do params[:location][:city] end get '/location?location[city]=Dallas' expect(last_response.body).to eq('Dallas') end context 'with special requirements' do it 'parses email param with provided requirements for params' do subject.get('/:person_email', requirements: { person_email: /.*/ }) do params[:person_email] end get '/someone@example.com' expect(last_response.body).to eq('someone@example.com') get 'someone@example.com.pl' expect(last_response.body).to eq('someone@example.com.pl') end it 'parses many params with provided regexps' do subject.get('/:person_email/test/:number', requirements: { person_email: /someone@(.*).com/, number: /[0-9]/ }) do params[:person_email] << params[:number] end get '/someone@example.com/test/1' expect(last_response.body).to eq('someone@example.com1') get '/someone@testing.wrong/test/1' expect(last_response.status).to eq(404) get 'someone@test.com/test/wrong_number' expect(last_response.status).to eq(404) get 'someone@test.com/wrong_middle/1' expect(last_response.status).to eq(404) end context 'namespace requirements' do before :each do subject.namespace :outer, requirements: { person_email: /abc@(.*).com/ } do get('/:person_email') do params[:person_email] end namespace :inner, requirements: { number: /[0-9]/, person_email: /someone@(.*).com/ } do get '/:person_email/test/:number' do params[:person_email] << params[:number] end end end end it 'parse email param with provided requirements for params' do get '/outer/abc@example.com' expect(last_response.body).to eq('abc@example.com') end it "should override outer namespace's requirements" do get '/outer/inner/someone@testing.wrong/test/1' expect(last_response.status).to eq(404) get '/outer/inner/someone@testing.com/test/1' expect(last_response.status).to eq(200) expect(last_response.body).to eq('someone@testing.com1') end end end context 'from body parameters' do before(:each) do subject.post '/request_body' do params[:user] end subject.put '/request_body' do params[:user] end end it 'converts JSON bodies to params' do post '/request_body', ::Grape::Json.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('Bobby T.') end it 'does not convert empty JSON bodies to params' do put '/request_body', '', 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('') end if Object.const_defined? :MultiXml it 'converts XML bodies to params' do post '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq('Bobby T.') end it 'converts XML bodies to params' do put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq('Bobby T.') end else it 'converts XML bodies to params' do post '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq('{"__content__"=>"Bobby T."}') end it 'converts XML bodies to params' do put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq('{"__content__"=>"Bobby T."}') end end it 'does not include parameters not defined by the body' do subject.post '/omitted_params' do error! 400, 'expected nil' if params[:version] params[:user] end post '/omitted_params', ::Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(last_response.body).to eq('Bob') end end it 'responds with a 415 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(415) expect(last_response.body).to eq('{"error":"The provided content-type \'application/xml\' is not supported."}') end it 'does not accept text/plain in JSON format if application/json is specified as content type' do subject.format :json subject.default_format :json subject.put '/request_body' do params[:user] end put '/request_body', ::Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq(415) expect(last_response.body).to eq('{"error":"The provided content-type \'text/plain\' is not supported."}') end context 'content type with params' do before do subject.format :json subject.content_type :json, 'application/json; charset=utf-8' subject.post do params[:data] end post '/', ::Grape::Json.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json' end it 'should not response with 406 for same type without params' do expect(last_response.status).not_to be 406 end it 'should response with given content type in headers' do expect(last_response.headers['Content-Type']).to eq 'application/json; charset=utf-8' end end context 'precedence' do before do subject.format :json subject.namespace '/:id' do get do { params: params[:id] } end post do { params: params[:id] } end put do { params: params[:id] } end end end it 'route string params have higher precedence than body params' do post '/123', { id: 456 }.to_json expect(JSON.parse(last_response.body)['params']).to eq '123' put '/123', { id: 456 }.to_json expect(JSON.parse(last_response.body)['params']).to eq '123' end it 'route string params have higher precedence than URL params' do get '/123?id=456' expect(JSON.parse(last_response.body)['params']).to eq '123' post '/123?id=456' expect(JSON.parse(last_response.body)['params']).to eq '123' end end context 'sets a value to params' do it 'params' do subject.params do requires :a, type: String end subject.get '/foo' do params[:a] = 'bar' end get '/foo', a: 'foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('bar') end end end describe '#error!' do it 'accepts a message' do subject.get('/hey') do error! 'This is not valid.' 'This is valid.' end get '/hey' expect(last_response.status).to eq(500) expect(last_response.body).to eq('This is not valid.') end it 'accepts a code' do subject.get('/hey') do error! 'Unauthorized.', 401 end get '/hey' expect(last_response.status).to eq(401) expect(last_response.body).to eq('Unauthorized.') end it 'accepts an object and render it in format' do subject.get '/hey' do error!({ 'dude' => 'rad' }, 403) end get '/hey.json' expect(last_response.status).to eq(403) expect(last_response.body).to eq('{"dude":"rad"}') end it 'accepts a frozen object' do subject.get '/hey' do error!({ 'dude' => 'rad' }.freeze, 403) end get '/hey.json' expect(last_response.status).to eq(403) expect(last_response.body).to eq('{"dude":"rad"}') end it 'can specifiy headers' do subject.get '/hey' do error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value') end get '/hey.json' expect(last_response.status).to eq(403) expect(last_response.headers['X-Custom']).to eq('value') end it 'merges additional headers with headers set before call' do subject.before do header 'X-Before-Test', 'before-sample' end subject.get '/hey' do header 'X-Test', 'test-sample' error!({ 'dude' => 'rad' }, 403, 'X-Error' => 'error') end get '/hey.json' expect(last_response.headers['X-Before-Test']).to eq('before-sample') expect(last_response.headers['X-Test']).to eq('test-sample') expect(last_response.headers['X-Error']).to eq('error') end it 'does not merges additional headers with headers set after call' do subject.after do header 'X-After-Test', 'after-sample' end subject.get '/hey' do error!({ 'dude' => 'rad' }, 403, 'X-Error' => 'error') end get '/hey.json' expect(last_response.headers['X-Error']).to eq('error') expect(last_response.headers['X-After-Test']).to be_nil end it 'sets the status code for the endpoint' do memoized_endpoint = nil subject.get '/hey' do memoized_endpoint = self error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value') end get '/hey.json' expect(memoized_endpoint.status).to eq(403) end end describe '#redirect' do it 'redirects to a url with status 302' do subject.get('/hey') do redirect '/ha' end get '/hey' expect(last_response.status).to eq 302 expect(last_response.headers['Location']).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved temporarily to /ha.' end it 'has status code 303 if it is not get request and it is http 1.1' do subject.post('/hey') do redirect '/ha' end post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1' expect(last_response.status).to eq 303 expect(last_response.headers['Location']).to eq '/ha' expect(last_response.body).to eq 'An alternate resource is located at /ha.' end it 'support permanent redirect' do subject.get('/hey') do redirect '/ha', permanent: true end get '/hey' expect(last_response.status).to eq 301 expect(last_response.headers['Location']).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved permanently to /ha.' end it 'allows for an optional redirect body override' do subject.get('/hey') do redirect '/ha', body: 'test body' end get '/hey' expect(last_response.body).to eq 'test body' end end it 'does not persist params between calls' do subject.post('/new') do params[:text] end post '/new', text: 'abc' expect(last_response.body).to eq('abc') post '/new', text: 'def' expect(last_response.body).to eq('def') end it 'resets all instance variables (except block) between calls' do subject.helpers do def memoized @memoized ||= params[:howdy] end end subject.get('/hello') do memoized end get '/hello?howdy=hey' expect(last_response.body).to eq('hey') get '/hello?howdy=yo' expect(last_response.body).to eq('yo') end it 'allows explicit return calls' do subject.get('/home') do return 'Hello' end get '/home' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello') end describe '.generate_api_method' do it 'raises NameError if the method name is already in use' do expect do Grape::Endpoint.generate_api_method('version', &proc {}) end.to raise_error(NameError) end it 'raises ArgumentError if a block is not given' do expect do Grape::Endpoint.generate_api_method('GET without a block method') end.to raise_error(ArgumentError) end it 'returns a Proc' do expect(Grape::Endpoint.generate_api_method('GET test for a proc', &proc {})).to be_a Proc end end context 'filters' do describe 'before filters' do it 'runs the before filter if set' do subject.before { env['before_test'] = 'OK' } subject.get('/before_test') { env['before_test'] } get '/before_test' expect(last_response.body).to eq('OK') end end describe 'after filters' do it 'overrides the response body if it sets it' do subject.after { body 'after' } subject.get('/after_test') { 'during' } get '/after_test' expect(last_response.body).to eq('after') end it 'does not override the response body with its return' do subject.after { 'after' } subject.get('/after_test') { 'body' } get '/after_test' expect(last_response.body).to eq('body') end end it 'allows adding to response with present' do subject.format :json subject.before { present :before, 'before' } subject.before_validation { present :before_validation, 'before_validation' } subject.after_validation { present :after_validation, 'after_validation' } subject.after { present :after, 'after' } subject.get :all_filters do present :endpoint, 'endpoint' end get '/all_filters' json = JSON.parse(last_response.body) expect(json.keys).to match_array %w[before before_validation after_validation endpoint after] end context 'when terminating the response with error!' do it 'breaks normal call chain' do called = [] subject.before { called << 'before' } subject.before_validation { called << 'before_validation' } subject.after_validation { error! :oops, 500 } subject.after { called << 'after' } subject.get :error_filters do called << 'endpoint' '' end get '/error_filters' expect(last_response.status).to eql 500 expect(called).to match_array %w[before before_validation] end it 'allows prior and parent filters of same type to run' do called = [] subject.before { called << 'parent' } subject.namespace :parent do before { called << 'prior' } before { error! :oops, 500 } before { called << 'subsequent' } get :hello do called << :endpoint 'Hello!' end end get '/parent/hello' expect(last_response.status).to eql 500 expect(called).to match_array %w[parent prior] end end end context 'anchoring' do describe 'delete 204' do it 'allows for the anchoring option with a delete method' do subject.send(:delete, '/example', anchor: true) {} send(:delete, '/example/and/some/more') expect(last_response.status).to eql 404 end it 'anchors paths by default for the delete method' do subject.send(:delete, '/example') {} send(:delete, '/example/and/some/more') expect(last_response.status).to eql 404 end it 'responds to /example/and/some/more for the non-anchored delete method' do subject.send(:delete, '/example', anchor: false) {} send(:delete, '/example/and/some/more') expect(last_response.status).to eql 204 expect(last_response.body).to be_empty end end describe 'delete 200, with response body' do it 'responds to /example/and/some/more for the non-anchored delete method' do subject.send(:delete, '/example', anchor: false) do status 200 body 'deleted' end send(:delete, '/example/and/some/more') expect(last_response.status).to eql 200 expect(last_response.body).not_to be_empty end end describe 'delete 200, with a return value (no explicit body)' do it 'responds to /example delete method' do subject.delete(:example) { 'deleted' } delete '/example' expect(last_response.status).to eql 200 expect(last_response.body).not_to be_empty end end describe 'delete 204, with nil has return value (no explicit body)' do it 'responds to /example delete method' do subject.delete(:example) { nil } delete '/example' expect(last_response.status).to eql 204 expect(last_response.body).to be_empty end end describe 'delete 204, with empty array has return value (no explicit body)' do it 'responds to /example delete method' do subject.delete(:example) { '' } delete '/example' expect(last_response.status).to eql 204 expect(last_response.body).to be_empty end end describe 'all other' do %w[post get head put options patch].each do |verb| it "allows for the anchoring option with a #{verb.upcase} method" do subject.send(verb, '/example', anchor: true) do verb end send(verb, '/example/and/some/more') expect(last_response.status).to eql 404 end it "anchors paths by default for the #{verb.upcase} method" do subject.send(verb, '/example') do verb end send(verb, '/example/and/some/more') expect(last_response.status).to eql 404 end it "responds to /example/and/some/more for the non-anchored #{verb.upcase} method" do subject.send(verb, '/example', anchor: false) do verb end send(verb, '/example/and/some/more') expect(last_response.status).to eql verb == 'post' ? 201 : 200 expect(last_response.body).to eql verb == 'head' ? '' : verb end end end end context 'request' do it 'is set to the url requested' do subject.get('/url') do request.url end get '/url' expect(last_response.body).to eq('http://example.org/url') end ['v1', :v1].each do |version| it "should include version #{version}" do subject.version version, using: :path subject.get('/url') do request.url end get "/#{version}/url" expect(last_response.body).to eq("http://example.org/#{version}/url") end end it 'should include prefix' do subject.version 'v1', using: :path subject.prefix 'api' subject.get('/url') do request.url end get '/api/v1/url' expect(last_response.body).to eq('http://example.org/api/v1/url') end end context 'version headers' do before do # NOTE: a 404 is returned instead of the 406 if cascade: false is not set. subject.version 'v1', using: :header, vendor: 'ohanapi', cascade: false subject.get '/test' do 'Hello!' end end it 'result in a 406 response if they are invalid' do get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json' expect(last_response.status).to eq(406) end it 'result in a 406 response if they cannot be parsed by rack-accept' do get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1' expect(last_response.status).to eq(406) end end context 'binary' do before do subject.get do file FileStreamer.new(__FILE__) end end it 'suports stream objects in response' do get '/' expect(last_response.status).to eq 200 expect(last_response.body).to eq File.read(__FILE__) end end context 'validation errors' do before do subject.before do header['Access-Control-Allow-Origin'] = '*' end subject.params do requires :id, type: String end subject.get do 'should not get here' end end it 'returns the errors, and passes headers' do get '/' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'id is missing' expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') end end context 'instrumentation' do before do subject.before do # Placeholder end subject.get do 'hello' end @events = [] @subscriber = ActiveSupport::Notifications.subscribe(/grape/) do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end end after do ActiveSupport::Notifications.unsubscribe(@subscriber) end it 'notifies AS::N' do get '/' # In order that the events finalized (time each block ended) expect(@events).to contain_exactly( have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :before_validation }), have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), validators: [], request: a_kind_of(Grape::Request) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :after_validation }), have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(Grape::Endpoint) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :after }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :finally }), have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), env: an_instance_of(Hash) }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), formatter: a_kind_of(Module) }) ) # In order that events were initialized expect(@events.sort_by(&:time)).to contain_exactly( have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), env: an_instance_of(Hash) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :before_validation }), have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), validators: [], request: a_kind_of(Grape::Request) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :after_validation }), have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(Grape::Endpoint) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :after }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :finally }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), formatter: a_kind_of(Module) }) ) end end end grape-1.5.2/spec/grape/util/0000755000004100000410000000000014016744305015656 5ustar www-datawww-datagrape-1.5.2/spec/grape/util/inheritable_values_spec.rb0000644000004100000410000000432314016744305023064 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module Util describe InheritableValues do let(:parent) { InheritableValues.new } subject { InheritableValues.new(parent) } describe '#delete' do it 'deletes a key' do subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to be_nil end it 'does not delete parent values' do parent[:some_thing] = :foo subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq :foo end end describe '#[]' do it 'returns a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq :foo end it 'returns parent value when no value is set' do parent[:some_thing] = :foo expect(subject[:some_thing]).to eq :foo end it 'overwrites parent value with the current one' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(subject[:some_thing]).to eq :foo_bar end it 'parent values are not changed' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(parent[:some_thing]).to eq :foo end end describe '#[]=' do it 'sets a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq :foo end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq(some_thing: :foo, some_thing_more: :foo_bar) end end describe '#clone' do let(:obj_cloned) { subject.clone } context 'complex (i.e. not primitive) data types (ex. entity classes, please see bug #891)' do let(:description) { { entity: double } } before { subject[:description] = description } it 'copies values; does not duplicate them' do expect(obj_cloned[:description]).to eq description end end end end end end grape-1.5.2/spec/grape/util/inheritable_setting_spec.rb0000644000004100000410000002412214016744305023241 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module Util describe InheritableSetting do before :each do InheritableSetting.reset_global! end let(:parent) do Grape::Util::InheritableSetting.new.tap do |settings| settings.global[:global_thing] = :global_foo_bar settings.namespace[:namespace_thing] = :namespace_foo_bar settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar settings.route[:route_thing] = :route_foo_bar end end let(:other_parent) do Grape::Util::InheritableSetting.new.tap do |settings| settings.namespace[:namespace_thing] = :namespace_foo_bar_other settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar_other settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar_other settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar_other settings.route[:route_thing] = :route_foo_bar_other end end before :each do subject.inherit_from parent end describe '#global' do it 'sets a global value' do subject.global[:some_thing] = :foo_bar expect(subject.global[:some_thing]).to eq :foo_bar subject.global[:some_thing] = :foo_bar_next expect(subject.global[:some_thing]).to eq :foo_bar_next end it 'sets the global inherited values' do expect(subject.global[:global_thing]).to eq :global_foo_bar end it 'overrides global values' do subject.global[:global_thing] = :global_new_foo_bar expect(parent.global[:global_thing]).to eq :global_new_foo_bar end it 'should handle different parents' do subject.global[:global_thing] = :global_new_foo_bar subject.inherit_from other_parent expect(parent.global[:global_thing]).to eq :global_new_foo_bar expect(other_parent.global[:global_thing]).to eq :global_new_foo_bar end end describe '#api_class' do it 'is specific to the class' do subject.api_class[:some_thing] = :foo_bar parent.api_class[:some_thing] = :some_thing expect(subject.api_class[:some_thing]).to eq :foo_bar expect(parent.api_class[:some_thing]).to eq :some_thing end end describe '#namespace' do it 'sets a value until the end of a namespace' do subject.namespace[:some_thing] = :foo_bar expect(subject.namespace[:some_thing]).to eq :foo_bar end it 'uses new values when a new namespace starts' do subject.namespace[:namespace_thing] = :new_namespace_foo_bar expect(subject.namespace[:namespace_thing]).to eq :new_namespace_foo_bar expect(parent.namespace[:namespace_thing]).to eq :namespace_foo_bar end end describe '#namespace_inheritable' do it 'works with inheritable values' do expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar end it 'should handle different parents' do expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.inherit_from other_parent expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar_other subject.inherit_from parent expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.inherit_from other_parent subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing subject.inherit_from parent expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing end end describe '#namespace_stackable' do it 'works with stackable values' do expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] subject.inherit_from other_parent expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar_other] end end describe '#namespace_reverse_stackable' do it 'works with reverse stackable values' do expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] subject.inherit_from other_parent expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar_other] end end describe '#route' do it 'sets a value until the next route' do subject.route[:some_thing] = :foo_bar expect(subject.route[:some_thing]).to eq :foo_bar subject.route_end expect(subject.route[:some_thing]).to be_nil end it 'works with route values' do expect(subject.route[:route_thing]).to eq :route_foo_bar end end describe '#api_class' do it 'is specific to the class' do subject.api_class[:some_thing] = :foo_bar expect(subject.api_class[:some_thing]).to eq :foo_bar end end describe '#inherit_from' do it 'notifies clones' do new_settings = subject.point_in_time_copy expect(new_settings).to receive(:inherit_from).with(other_parent) subject.inherit_from other_parent end end describe '#point_in_time_copy' do let!(:cloned_obj) { subject.point_in_time_copy } it 'resets point_in_time_copies' do expect(cloned_obj.point_in_time_copies).to be_empty end it 'decouples namespace values' do subject.namespace[:namespace_thing] = :namespace_foo_bar cloned_obj.namespace[:namespace_thing] = :new_namespace_foo_bar expect(subject.namespace[:namespace_thing]).to eq :namespace_foo_bar end it 'decouples namespace inheritable values' do expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar cloned_obj.namespace_inheritable[:namespace_inheritable_thing] = :my_cloned_thing expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_cloned_thing expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing end it 'decouples namespace stackable values' do expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] subject.namespace_stackable[:namespace_stackable_thing] = :other_thing expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq %i[namespace_stackable_foo_bar other_thing] expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] end it 'decouples namespace reverse stackable values' do expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :other_thing expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq %i[other_thing namespace_reverse_stackable_foo_bar] expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] end it 'decouples route values' do expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar subject.route[:route_thing] = :new_route_foo_bar expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar end it 'adds itself to original as clone' do expect(subject.point_in_time_copies).to include(cloned_obj) end end describe '#to_hash' do it 'return all settings as a hash' do subject.global[:global_thing] = :global_foo_bar subject.namespace[:namespace_thing] = :namespace_foo_bar subject.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar subject.namespace_stackable[:namespace_stackable_thing] = [:namespace_stackable_foo_bar] subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = [:namespace_reverse_stackable_foo_bar] subject.route[:route_thing] = :route_foo_bar expect(subject.to_hash).to include(global: { global_thing: :global_foo_bar }) expect(subject.to_hash).to include(namespace: { namespace_thing: :namespace_foo_bar }) expect(subject.to_hash).to include(namespace_inheritable: { namespace_inheritable_thing: :namespace_inheritable_foo_bar }) expect(subject.to_hash).to include(namespace_stackable: { namespace_stackable_thing: [:namespace_stackable_foo_bar, [:namespace_stackable_foo_bar]] }) expect(subject.to_hash).to include(namespace_reverse_stackable: { namespace_reverse_stackable_thing: [[:namespace_reverse_stackable_foo_bar], :namespace_reverse_stackable_foo_bar] }) expect(subject.to_hash).to include(route: { route_thing: :route_foo_bar }) end end end end end grape-1.5.2/spec/grape/util/reverse_stackable_values_spec.rb0000644000004100000410000000770614016744305024272 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module Util describe ReverseStackableValues do let(:parent) { described_class.new } subject { described_class.new(parent) } describe '#keys' do it 'returns all keys' do subject[:some_thing] = :foo_bar subject[:some_thing_else] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else].sort end it 'returns merged keys with parent' do parent[:some_thing] = :foo parent[:some_thing_else] = :foo subject[:some_thing] = :foo_bar subject[:some_thing_more] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort end end describe '#delete' do it 'deletes a key' do subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [] end it 'does not delete parent values' do parent[:some_thing] = :foo subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [:foo] end end describe '#[]' do it 'returns an array of values' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'returns parent value when no value is set' do parent[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'combines parent and actual values (actual first)' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(subject[:some_thing]).to eq %i[foo_bar foo] end it 'parent values are not changed' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(parent[:some_thing]).to eq [:foo] end end describe '#[]=' do it 'sets a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'pushes further values' do subject[:some_thing] = :foo subject[:some_thing] = :bar expect(subject[:some_thing]).to eq %i[foo bar] end it 'can handle array values' do subject[:some_thing] = :foo subject[:some_thing] = %i[bar more] expect(subject[:some_thing]).to eq [:foo, %i[bar more]] parent[:some_thing_else] = %i[foo bar] subject[:some_thing_else] = %i[some bar foo] expect(subject[:some_thing_else]).to eq [%i[some bar foo], %i[foo bar]] end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing] = %i[bar more] subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq( some_thing: [%i[bar more], :foo], some_thing_more: [:foo_bar] ) end end describe '#clone' do let(:obj_cloned) { subject.clone } it 'copies all values' do parent = described_class.new child = described_class.new parent grandchild = described_class.new child parent[:some_thing] = :foo child[:some_thing] = %i[bar more] grandchild[:some_thing] = :grand_foo_bar grandchild[:some_thing_more] = :foo_bar expect(grandchild.clone.to_hash).to eq( some_thing: [:grand_foo_bar, %i[bar more], :foo], some_thing_more: [:foo_bar] ) end context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do let(:middleware) { double } before { subject[:middleware] = middleware } it 'copies values; does not duplicate them' do expect(obj_cloned[:middleware]).to eq [middleware] end end end end end end grape-1.5.2/spec/grape/util/stackable_values_spec.rb0000644000004100000410000000755014016744305022534 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module Util describe StackableValues do let(:parent) { StackableValues.new } subject { StackableValues.new(parent) } describe '#keys' do it 'returns all keys' do subject[:some_thing] = :foo_bar subject[:some_thing_else] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else].sort end it 'returns merged keys with parent' do parent[:some_thing] = :foo parent[:some_thing_else] = :foo subject[:some_thing] = :foo_bar subject[:some_thing_more] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort end end describe '#delete' do it 'deletes a key' do subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [] end it 'does not delete parent values' do parent[:some_thing] = :foo subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [:foo] end end describe '#[]' do it 'returns an array of values' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'returns parent value when no value is set' do parent[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'combines parent and actual values' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(subject[:some_thing]).to eq %i[foo foo_bar] end it 'parent values are not changed' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(parent[:some_thing]).to eq [:foo] end end describe '#[]=' do it 'sets a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'pushes further values' do subject[:some_thing] = :foo subject[:some_thing] = :bar expect(subject[:some_thing]).to eq %i[foo bar] end it 'can handle array values' do subject[:some_thing] = :foo subject[:some_thing] = %i[bar more] expect(subject[:some_thing]).to eq [:foo, %i[bar more]] parent[:some_thing_else] = %i[foo bar] subject[:some_thing_else] = %i[some bar foo] expect(subject[:some_thing_else]).to eq [%i[foo bar], %i[some bar foo]] end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing] = %i[bar more] subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq(some_thing: [:foo, %i[bar more]], some_thing_more: [:foo_bar]) end end describe '#clone' do let(:obj_cloned) { subject.clone } it 'copies all values' do parent = StackableValues.new child = StackableValues.new parent grandchild = StackableValues.new child parent[:some_thing] = :foo child[:some_thing] = %i[bar more] grandchild[:some_thing] = :grand_foo_bar grandchild[:some_thing_more] = :foo_bar expect(grandchild.clone.to_hash).to eq(some_thing: [:foo, %i[bar more], :grand_foo_bar], some_thing_more: [:foo_bar]) end context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do let(:middleware) { double } before { subject[:middleware] = middleware } it 'copies values; does not duplicate them' do expect(obj_cloned[:middleware]).to eq [middleware] end end end end end end grape-1.5.2/spec/grape/util/strict_hash_configuration_spec.rb0000644000004100000410000000171514016744305024463 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module Util describe 'StrictHashConfiguration' do subject do Class.new do include Grape::Util::StrictHashConfiguration.module(:config1, :config2, config3: [:config4], config5: [config6: %i[config7 config8]]) end end it 'set nested configs' do subject.configure do config1 'alpha' config2 'beta' config3 do config4 'gamma' end local_var = 8 config5 do config6 do config7 7 config8 local_var end end end expect(subject.settings).to eq(config1: 'alpha', config2: 'beta', config3: { config4: 'gamma' }, config5: { config6: { config7: 7, config8: 8 } }) end end end end grape-1.5.2/spec/grape/entity_spec.rb0000644000004100000410000002164014016744305017557 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'grape_entity' describe Grape::Entity do subject { Class.new(Grape::API) } def app subject end describe '#present' do it 'sets the object as the body if no options are provided' do inner_body = nil subject.get '/example' do present(abc: 'def') inner_body = body end get '/example' expect(inner_body).to eql(abc: 'def') end it 'calls through to the provided entity class if one is given' do entity_mock = Object.new allow(entity_mock).to receive(:represent) subject.get '/example' do present Object.new, with: entity_mock end get '/example' end it 'pulls a representation from the class options if it exists' do entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subject.represent Object, with: entity subject.get '/example' do present Object.new end get '/example' expect(last_response.body).to eq('Hiya') end it 'pulls a representation from the class options if the presented object is a collection of objects' do entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') module EntitySpec class TestObject end class FakeCollection def first TestObject.new end end end subject.represent EntitySpec::TestObject, with: entity subject.get '/example' do present [EntitySpec::TestObject.new] end subject.get '/example2' do present EntitySpec::FakeCollection.new end get '/example' expect(last_response.body).to eq('Hiya') get '/example2' expect(last_response.body).to eq('Hiya') end it 'pulls a representation from the class ancestor if it exists' do entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subclass = Class.new(Object) subject.represent Object, with: entity subject.get '/example' do present subclass.new end get '/example' expect(last_response.body).to eq('Hiya') end it 'automatically uses Klass::Entity if that exists' do some_model = Class.new entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity subject.get '/example' do present some_model.new end get '/example' expect(last_response.body).to eq('Auto-detect!') end it 'automatically uses Klass::Entity based on the first object in the collection being presented' do some_model = Class.new entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity subject.get '/example' do present [some_model.new] end get '/example' expect(last_response.body).to eq('Auto-detect!') end it 'does not run autodetection for Entity when explicitly provided' do entity = Class.new(Grape::Entity) some_array = [] subject.get '/example' do present some_array, with: entity end expect(some_array).not_to receive(:first) get '/example' end it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do entity = Class.new(Grape::Entity) some_relation = Class.new some_model = Class.new allow(entity).to receive(:represent).and_return('Auto-detect!') allow(some_relation).to receive(:first) allow(some_relation).to receive(:klass).and_return(some_model) some_model.const_set :Entity, entity subject.get '/example' do present some_relation end expect(some_relation).not_to receive(:first) get '/example' expect(last_response.body).to eq('Auto-detect!') end it 'autodetection does not use Entity if it is not a presenter' do some_model = Class.new entity = Class.new some_model.class.const_set :Entity, entity subject.get '/example' do present some_model end get '/example' expect(entity).not_to receive(:represent) end it 'adds a root key to the output if one is given' do inner_body = nil subject.get '/example' do present({ abc: 'def' }, root: :root) inner_body = body end get '/example' expect(inner_body).to eql(root: { abc: 'def' }) end %i[json serializable_hash].each do |format| it "presents with #{format}" do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :id subject.format format subject.get '/example' do c = Class.new do attr_reader :id def initialize(id) @id = id end end present c.new(1), with: entity end get '/example' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"example":{"id":1}}') end it "presents with #{format} collection" do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :id subject.format format subject.get '/examples' do c = Class.new do attr_reader :id def initialize(id) @id = id end end examples = [c.new(1), c.new(2)] present examples, with: entity end get '/examples' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"examples":[{"id":1},{"id":2}]}') end end it 'presents with xml' do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name subject.format :xml subject.get '/example' do c = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end present c.new(name: 'johnnyiller'), with: entity end get '/example' expect(last_response.status).to eq(200) expect(last_response.headers['Content-type']).to eq('application/xml') expect(last_response.body).to eq <<-XML johnnyiller XML end it 'presents with json' do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name subject.format :json subject.get '/example' do c = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end present c.new(name: 'johnnyiller'), with: entity end get '/example' expect(last_response.status).to eq(200) expect(last_response.headers['Content-type']).to eq('application/json') expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}') end it 'presents with jsonp utilising Rack::JSONP' do # Include JSONP middleware subject.use Rack::JSONP entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name # Rack::JSONP expects a standard JSON response in UTF-8 format subject.format :json subject.formatter :json, lambda { |object, _| object.to_json.encode('utf-8') } subject.get '/example' do c = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end present c.new(name: 'johnnyiller'), with: entity end get '/example?callback=abcDef' expect(last_response.status).to eq(200) expect(last_response.headers['Content-type']).to eq('application/javascript') expect(last_response.body).to include 'abcDef({"example":{"name":"johnnyiller"}})' end context 'present with multiple entities' do it 'present with multiple entities using optional symbol' do user = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end user1 = user.new(name: 'user1') user2 = user.new(name: 'user2') entity = Class.new(Grape::Entity) entity.expose :name subject.format :json subject.get '/example' do present :page, 1 present :user1, user1, with: entity present :user2, user2, with: entity end get '/example' expect_response_json = { 'page' => 1, 'user1' => { 'name' => 'user1' }, 'user2' => { 'name' => 'user2' } } expect(JSON(last_response.body)).to eq(expect_response_json) end end end end grape-1.5.2/spec/grape/api_remount_spec.rb0000644000004100000410000003474514016744305020577 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'shared/versioning_examples' describe Grape::API do subject(:a_remounted_api) { Class.new(Grape::API) } let(:root_api) { Class.new(Grape::API) } def app root_api end describe 'remounting an API' do context 'with a defined route' do before do a_remounted_api.get '/votes' do '10 votes' end end context 'when mounting one instance' do before do root_api.mount a_remounted_api end it 'can access the endpoint' do get '/votes' expect(last_response.body).to eql '10 votes' end end context 'when mounting twice' do before do root_api.mount a_remounted_api => '/posts' root_api.mount a_remounted_api => '/comments' end it 'can access the votes in both places' do get '/posts/votes' expect(last_response.body).to eql '10 votes' get '/comments/votes' expect(last_response.body).to eql '10 votes' end end context 'when mounting on namespace' do before do stub_const('StaticRefToAPI', a_remounted_api) root_api.namespace 'posts' do mount StaticRefToAPI end root_api.namespace 'comments' do mount StaticRefToAPI end end it 'can access the votes in both places' do get '/posts/votes' expect(last_response.body).to eql '10 votes' get '/comments/votes' expect(last_response.body).to eql '10 votes' end end end describe 'with dynamic configuration' do context 'when mounting an endpoint conditional on a configuration' do subject(:a_remounted_api) do Class.new(Grape::API) do get 'always' do 'success' end given configuration[:mount_sometimes] do get 'sometimes' do 'sometimes' end end end end it 'mounts the endpoints only when configured to do so' do root_api.mount({ a_remounted_api => 'with_conditional' }, with: { mount_sometimes: true }) root_api.mount({ a_remounted_api => 'without_conditional' }, with: { mount_sometimes: false }) get '/with_conditional/always' expect(last_response.body).to eq 'success' get '/with_conditional/sometimes' expect(last_response.body).to eq 'sometimes' get '/without_conditional/always' expect(last_response.body).to eq 'success' get '/without_conditional/sometimes' expect(last_response.status).to eq 404 end end context 'when using an expression derived from a configuration' do subject(:a_remounted_api) do Class.new(Grape::API) do get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end end end before do root_api.mount a_remounted_api, with: { api_name: 'a_name' } end it 'mounts the endpoint with the name' do get 'api_name_a_name' expect(last_response.body).to eq 'success' end it 'does not mount the endpoint with a null name' do get 'api_name_' expect(last_response.body).not_to eq 'success' end context 'when the expression lives in a namespace' do subject(:a_remounted_api) do Class.new(Grape::API) do namespace :base do get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end end end end it 'mounts the endpoint with the name' do get 'base/api_name_a_name' expect(last_response.body).to eq 'success' end it 'does not mount the endpoint with a null name' do get 'base/api_name_' expect(last_response.body).not_to eq 'success' end end end context 'when executing a standard block within a `mounted` block with all dynamic params' do subject(:a_remounted_api) do Class.new(Grape::API) do mounted do desc configuration[:description] do headers configuration[:headers] end get configuration[:endpoint] do configuration[:response] end end end end let(:api_endpoint) { 'custom_endpoint' } let(:api_response) { 'custom response' } let(:endpoint_description) { 'this is a custom API' } let(:headers) do { 'XAuthToken' => { 'description' => 'Validates your identity', 'required' => true } } end it 'mounts the API and obtains the description and headers definition' do root_api.mount a_remounted_api, with: { description: endpoint_description, headers: headers, endpoint: api_endpoint, response: api_response } get api_endpoint expect(last_response.body).to eq api_response expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:description]) .to eq endpoint_description expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:headers]) .to eq headers end end context 'when executing a custom block on mount' do subject(:a_remounted_api) do Class.new(Grape::API) do get 'always' do 'success' end mounted do configuration[:endpoints].each do |endpoint_name, endpoint_response| get endpoint_name do endpoint_response end end end end end it 'mounts the endpoints only when configured to do so' do root_api.mount a_remounted_api, with: { endpoints: { 'api_name' => 'api_response' } } get 'api_name' expect(last_response.body).to eq 'api_response' end end context 'when the configuration is part of the arguments of a method' do subject(:a_remounted_api) do Class.new(Grape::API) do get configuration[:endpoint_name] do 'success' end end end it 'mounts the endpoint in the location it is configured' do root_api.mount a_remounted_api, with: { endpoint_name: 'some_location' } get '/some_location' expect(last_response.body).to eq 'success' get '/different_location' expect(last_response.status).to eq 404 root_api.mount a_remounted_api, with: { endpoint_name: 'new_location' } get '/new_location' expect(last_response.body).to eq 'success' end context 'when the configuration is the value in a key-arg pair' do subject(:a_remounted_api) do Class.new(Grape::API) do version 'v1', using: :param, parameter: configuration[:version_param] get 'endpoint' do 'version 1' end version 'v2', using: :param, parameter: configuration[:version_param] get 'endpoint' do 'version 2' end end end it 'takes the param from the configuration' do root_api.mount a_remounted_api, with: { version_param: 'param_name' } get '/endpoint?param_name=v1' expect(last_response.body).to eq 'version 1' get '/endpoint?param_name=v2' expect(last_response.body).to eq 'version 2' get '/endpoint?wrong_param_name=v2' expect(last_response.body).to eq 'version 1' end end end context 'on the DescSCope' do subject(:a_remounted_api) do Class.new(Grape::API) do desc 'The description of this' do tags ['not_configurable_tag', configuration[:a_configurable_tag]] end get 'location' do 'success' end end end it 'mounts the endpoint with the appropiate tags' do root_api.mount({ a_remounted_api => 'integer' }, with: { a_configurable_tag: 'a configured tag' }) end end context 'on the ParamScope' do subject(:a_remounted_api) do Class.new(Grape::API) do params do requires configuration[:required_param], type: configuration[:required_type] end get 'location' do 'success' end end end it 'mounts the endpoint in the location it is configured' do root_api.mount({ a_remounted_api => 'string' }, with: { required_param: 'param_key', required_type: String }) root_api.mount({ a_remounted_api => 'integer' }, with: { required_param: 'param_integer', required_type: Integer }) get '/string/location', param_key: 'a' expect(last_response.body).to eq 'success' get '/string/location', param_integer: 1 expect(last_response.status).to eq 400 get '/integer/location', param_integer: 1 expect(last_response.body).to eq 'success' get '/integer/location', param_integer: 'a' expect(last_response.status).to eq 400 end context 'on dynamic checks' do subject(:a_remounted_api) do Class.new(Grape::API) do params do optional :restricted_values, values: -> { [configuration[:allowed_value], 'always'] } end get 'location' do 'success' end end end it 'can read the configuration on lambdas' do root_api.mount a_remounted_api, with: { allowed_value: 'sometimes' } get '/location', restricted_values: 'always' expect(last_response.body).to eq 'success' get '/location', restricted_values: 'sometimes' expect(last_response.body).to eq 'success' get '/location', restricted_values: 'never' expect(last_response.status).to eq 400 end end end context 'when the configuration is read within a namespace' do before do a_remounted_api.namespace 'api' do params do requires configuration[:required_param] end get "/#{configuration[:path]}" do '10 votes' end end root_api.mount a_remounted_api, with: { path: 'votes', required_param: 'param_key' } root_api.mount a_remounted_api, with: { path: 'scores', required_param: 'param_key' } end it 'will use the dynamic configuration on all routes' do get 'api/votes', param_key: 'a' expect(last_response.body).to eql '10 votes' get 'api/scores', param_key: 'a' expect(last_response.body).to eql '10 votes' get 'api/votes' expect(last_response.status).to eq 400 end end context 'a very complex configuration example' do before do top_level_api = Class.new(Grape::API) do remounted_api = Class.new(Grape::API) do get configuration[:endpoint_name] do configuration[:response] end end expression_namespace = mounted { configuration[:namespace].to_s * 2 } given(mounted { configuration[:should_mount_expressed] != false }) do namespace expression_namespace do mount remounted_api, with: { endpoint_name: configuration[:endpoint_name], response: configuration[:endpoint_response] } end end end root_api.mount top_level_api, with: configuration_options end context 'when the namespace should be mounted' do let(:configuration_options) do { should_mount_expressed: true, namespace: 'bang', endpoint_name: 'james', endpoint_response: 'bond' } end it 'gets a response' do get 'bangbang/james' expect(last_response.body).to eq 'bond' end end context 'when should be mounted is nil' do let(:configuration_options) do { should_mount_expressed: nil, namespace: 'bang', endpoint_name: 'james', endpoint_response: 'bond' } end it 'gets a response' do get 'bangbang/james' expect(last_response.body).to eq 'bond' end end context 'when it should not be mounted' do let(:configuration_options) do { should_mount_expressed: false, namespace: 'bang', endpoint_name: 'james', endpoint_response: 'bond' } end it 'gets a response' do get 'bangbang/james' expect(last_response.body).not_to eq 'bond' end end end context 'when the configuration is read in a helper' do subject(:a_remounted_api) do Class.new(Grape::API) do helpers do def printed_response configuration[:some_value] end end get 'location' do printed_response end end end it 'will use the dynamic configuration on all routes' do root_api.mount(a_remounted_api, with: { some_value: 'response value' }) get '/location' expect(last_response.body).to eq 'response value' end end context 'when the configuration is read within the response block' do subject(:a_remounted_api) do Class.new(Grape::API) do get 'location' do configuration[:some_value] end end end it 'will use the dynamic configuration on all routes' do root_api.mount(a_remounted_api, with: { some_value: 'response value' }) get '/location' expect(last_response.body).to eq 'response value' end end end end end grape-1.5.2/spec/grape/path_spec.rb0000644000004100000410000001642514016744305017204 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape describe Path do describe '#initialize' do it 'remembers the path' do path = Path.new('/:id', anything, anything) expect(path.raw_path).to eql('/:id') end it 'remembers the namespace' do path = Path.new(anything, '/users', anything) expect(path.namespace).to eql('/users') end it 'remebers the settings' do path = Path.new(anything, anything, foo: 'bar') expect(path.settings).to eql(foo: 'bar') end end describe '#mount_path' do it 'is nil when no mount path setting exists' do path = Path.new(anything, anything, {}) expect(path.mount_path).to be_nil end it 'is nil when the mount path is nil' do path = Path.new(anything, anything, mount_path: nil) expect(path.mount_path).to be_nil end it 'splits the mount path' do path = Path.new(anything, anything, mount_path: %w[foo bar]) expect(path.mount_path).to eql(%w[foo bar]) end end describe '#root_prefix' do it 'is nil when no root prefix setting exists' do path = Path.new(anything, anything, {}) expect(path.root_prefix).to be_nil end it 'is nil when the mount path is nil' do path = Path.new(anything, anything, root_prefix: nil) expect(path.root_prefix).to be_nil end it 'splits the mount path' do path = Path.new(anything, anything, root_prefix: 'hello/world') expect(path.root_prefix).to eql(%w[hello world]) end end describe '#uses_path_versioning?' do it 'is false when the version setting is nil' do path = Path.new(anything, anything, version: nil) expect(path.uses_path_versioning?).to be false end it 'is false when the version option is header' do path = Path.new( anything, anything, version: 'v1', version_options: { using: :header } ) expect(path.uses_path_versioning?).to be false end it 'is true when the version option is path' do path = Path.new( anything, anything, version: 'v1', version_options: { using: :path } ) expect(path.uses_path_versioning?).to be true end end describe '#namespace?' do it 'is false when the namespace is nil' do path = Path.new(anything, nil, anything) expect(path.namespace?).to be_falsey end it 'is false when the namespace starts with whitespace' do path = Path.new(anything, ' /foo', anything) expect(path.namespace?).to be_falsey end it 'is false when the namespace is the root path' do path = Path.new(anything, '/', anything) expect(path.namespace?).to be false end it 'is true otherwise' do path = Path.new(anything, '/world', anything) expect(path.namespace?).to be true end end describe '#path?' do it 'is false when the path is nil' do path = Path.new(nil, anything, anything) expect(path.path?).to be_falsey end it 'is false when the path starts with whitespace' do path = Path.new(' /foo', anything, anything) expect(path.path?).to be_falsey end it 'is false when the path is the root path' do path = Path.new('/', anything, anything) expect(path.path?).to be false end it 'is true otherwise' do path = Path.new('/hello', anything, anything) expect(path.path?).to be true end end describe '#path' do context 'mount_path' do it 'is not included when it is nil' do path = Path.new(nil, nil, mount_path: '/foo/bar') expect(path.path).to eql '/foo/bar' end it 'is included when it is not nil' do path = Path.new(nil, nil, {}) expect(path.path).to eql('/') end end context 'root_prefix' do it 'is not included when it is nil' do path = Path.new(nil, nil, {}) expect(path.path).to eql('/') end it 'is included after the mount path' do path = Path.new( nil, nil, mount_path: '/foo', root_prefix: '/hello' ) expect(path.path).to eql('/foo/hello') end end it 'uses the namespace after the mount path and root prefix' do path = Path.new( nil, 'namespace', mount_path: '/foo', root_prefix: '/hello' ) expect(path.path).to eql('/foo/hello/namespace') end it 'uses the raw path after the namespace' do path = Path.new( 'raw_path', 'namespace', mount_path: '/foo', root_prefix: '/hello' ) expect(path.path).to eql('/foo/hello/namespace/raw_path') end end describe '#suffix' do context 'when using a specific format' do it 'accepts specified format' do path = Path.new(nil, nil, {}) allow(path).to receive(:uses_specific_format?) { true } allow(path).to receive(:settings) { { format: :json } } expect(path.suffix).to eql('(.json)') end end context 'when path versioning is used' do it "includes a '/'" do path = Path.new(nil, nil, {}) allow(path).to receive(:uses_specific_format?) { false } allow(path).to receive(:uses_path_versioning?) { true } expect(path.suffix).to eql('(/.:format)') end end context 'when path versioning is not used' do it "does not include a '/' when the path has a namespace" do path = Path.new(nil, 'namespace', {}) allow(path).to receive(:uses_specific_format?) { false } allow(path).to receive(:uses_path_versioning?) { true } expect(path.suffix).to eql('(.:format)') end it "does not include a '/' when the path has a path" do path = Path.new('/path', nil, {}) allow(path).to receive(:uses_specific_format?) { false } allow(path).to receive(:uses_path_versioning?) { true } expect(path.suffix).to eql('(.:format)') end it "includes a '/' otherwise" do path = Path.new(nil, nil, {}) allow(path).to receive(:uses_specific_format?) { false } allow(path).to receive(:uses_path_versioning?) { true } expect(path.suffix).to eql('(/.:format)') end end end describe '#path_with_suffix' do it 'combines the path and suffix' do path = Path.new(nil, nil, {}) allow(path).to receive(:path) { '/the/path' } allow(path).to receive(:suffix) { 'suffix' } expect(path.path_with_suffix).to eql('/the/pathsuffix') end context 'when using a specific format' do it 'might have a suffix with specified format' do path = Path.new(nil, nil, {}) allow(path).to receive(:path) { '/the/path' } allow(path).to receive(:uses_specific_format?) { true } allow(path).to receive(:settings) { { format: :json } } expect(path.path_with_suffix).to eql('/the/path(.json)') end end end end end grape-1.5.2/spec/grape/endpoint/0000755000004100000410000000000014016744305016521 5ustar www-datawww-datagrape-1.5.2/spec/grape/endpoint/declared_spec.rb0000644000004100000410000004405314016744305021631 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end describe '#declared' do before do subject.format :json subject.params do requires :first optional :second optional :third, default: 'third-default' optional :multiple_types, types: [Integer, String] optional :nested, type: Hash do optional :fourth optional :fifth optional :nested_two, type: Hash do optional :sixth optional :nested_three, type: Hash do optional :seventh end end optional :nested_arr, type: Array do optional :eighth end optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end optional :arr, type: Array do optional :nineth end optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end end context 'when params are not built with default class' do it 'returns an object that corresponds with the params class - hash with indifferent access' do subject.params do build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder end subject.get '/declared' do d = declared(params, include_missing: true) { declared_class: d.class.to_s } end get '/declared?first=present' expect(JSON.parse(last_response.body)['declared_class']).to eq('ActiveSupport::HashWithIndifferentAccess') end it 'returns an object that corresponds with the params class - hashie mash' do subject.params do build_with Grape::Extensions::Hashie::Mash::ParamBuilder end subject.get '/declared' do d = declared(params, include_missing: true) { declared_class: d.class.to_s } end get '/declared?first=present' expect(JSON.parse(last_response.body)['declared_class']).to eq('Hashie::Mash') end it 'returns an object that corresponds with the params class - hash' do subject.params do build_with Grape::Extensions::Hash::ParamBuilder end subject.get '/declared' do d = declared(params, include_missing: true) { declared_class: d.class.to_s } end get '/declared?first=present' expect(JSON.parse(last_response.body)['declared_class']).to eq('Hash') end end it 'should show nil for nested params if include_missing is true' do subject.get '/declared' do declared(params, include_missing: true) end get '/declared?first=present' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil end it 'should show nil for multiple allowed types if include_missing is true' do subject.get '/declared' do declared(params, include_missing: true) end get '/declared?first=present' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['multiple_types']).to be_nil end it 'does not work in a before filter' do subject.before do declared(params) end subject.get('/declared') { declared(params) } expect { get('/declared') }.to raise_error( Grape::DSL::InsideRoute::MethodNotYetAvailable ) end it 'has as many keys as there are declared params' do subject.get '/declared' do declared(params) end get '/declared?first=present' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body).keys.size).to eq(11) end it 'has a optional param with default value all the time' do subject.get '/declared' do declared(params) end get '/declared?first=one' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['third']).to eql('third-default') end it 'builds nested params' do subject.get '/declared' do declared(params) end get '/declared?first=present&nested[fourth]=1' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 9 end it 'builds arrays correctly' do subject.params do requires :first optional :second, type: Array end subject.post('/declared') { declared(params) } post '/declared', first: 'present', second: ['present'] expect(last_response.status).to eq(201) body = JSON.parse(last_response.body) expect(body['second']).to eq(['present']) end it 'builds nested params when given array' do subject.get '/dummy' do end subject.params do requires :first optional :second optional :third, default: 'third-default' optional :nested, type: Array do optional :fourth end end subject.get '/declared' do declared(params) end get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['nested'].size).to eq 2 end context 'when the param is missing and include_missing=false' do before do subject.get('/declared') { declared(params, include_missing: false) } end it 'sets nested objects to be nil' do get '/declared?first=present' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['nested']).to be_nil end end context 'when the param is missing and include_missing=true' do before do subject.get('/declared') { declared(params, include_missing: true) } end it 'sets objects with type=Hash to be a hash' do get '/declared?first=present' expect(last_response.status).to eq(200) body = JSON.parse(last_response.body) expect(body['empty_hash']).to eq({}) expect(body['nested']).to be_a(Hash) expect(body['nested']['empty_hash']).to eq({}) expect(body['nested']['nested_two']).to be_a(Hash) end it 'sets objects with type=Set to be a set' do get '/declared?first=present' expect(last_response.status).to eq(200) body = JSON.parse(last_response.body) expect(['#', []]).to include(body['empty_set']) expect(['#', []]).to include(body['empty_typed_set']) expect(['#', []]).to include(body['nested']['empty_set']) expect(['#', []]).to include(body['nested']['empty_typed_set']) end it 'sets objects with type=Array to be an array' do get '/declared?first=present' expect(last_response.status).to eq(200) body = JSON.parse(last_response.body) expect(body['empty_arr']).to eq([]) expect(body['empty_typed_arr']).to eq([]) expect(body['arr']).to eq([]) expect(body['nested']['empty_arr']).to eq([]) expect(body['nested']['empty_typed_arr']).to eq([]) expect(body['nested']['nested_arr']).to eq([]) end it 'includes all declared children when type=Hash' do get '/declared?first=present' expect(last_response.status).to eq(200) body = JSON.parse(last_response.body) expect(body['nested'].keys).to eq(%w[fourth fifth nested_two nested_arr empty_arr empty_typed_arr empty_hash empty_set empty_typed_set]) expect(body['nested']['nested_two'].keys).to eq(%w[sixth nested_three]) expect(body['nested']['nested_two']['nested_three'].keys).to eq(%w[seventh]) end end it 'filters out any additional params that are given' do subject.get '/declared' do declared(params) end get '/declared?first=one&other=two' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body).key?(:other)).to eq false end it 'stringifies if that option is passed' do subject.get '/declared' do declared(params, stringify: true) end get '/declared?first=one&other=two' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)['first']).to eq 'one' end it 'does not include missing attributes if that option is passed' do subject.get '/declared' do error! 'expected nil', 400 if declared(params, include_missing: false).key?(:second) '' end get '/declared?first=one&other=two' expect(last_response.status).to eq(200) end it 'does not include renamed missing attributes if that option is passed' do subject.params do optional :renamed_original, as: :renamed end subject.get '/declared' do error! 'expected nil', 400 if declared(params, include_missing: false).key?(:renamed) '' end get '/declared?first=one&other=two' expect(last_response.status).to eq(200) end it 'includes attributes with value that evaluates to false' do subject.params do requires :first optional :boolean end subject.post '/declared' do error!('expected false', 400) if declared(params, include_missing: false)[:boolean] != false '' end post '/declared', ::Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) end it 'includes attributes with value that evaluates to nil' do subject.params do requires :first optional :second end subject.post '/declared' do error!('expected nil', 400) unless declared(params, include_missing: false)[:second].nil? '' end post '/declared', ::Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) end it 'includes missing attributes with defaults when there are nested hashes' do subject.get '/dummy' do end subject.params do requires :first optional :second optional :third, default: nil optional :nested, type: Hash do optional :fourth, default: nil optional :fifth, default: nil requires :nested_nested, type: Hash do optional :sixth, default: 'sixth-default' optional :seven, default: nil end end end subject.get '/declared' do declared(params, include_missing: false) end get '/declared?first=present&nested[fourth]=&nested[nested_nested][sixth]=sixth' json = JSON.parse(last_response.body) expect(last_response.status).to eq(200) expect(json['first']).to eq 'present' expect(json['nested'].keys).to eq %w[fourth fifth nested_nested] expect(json['nested']['fourth']).to eq '' expect(json['nested']['nested_nested'].keys).to eq %w[sixth seven] expect(json['nested']['nested_nested']['sixth']).to eq 'sixth' end it 'does not include missing attributes when there are nested hashes' do subject.get '/dummy' do end subject.params do requires :first optional :second optional :third optional :nested, type: Hash do optional :fourth optional :fifth end end subject.get '/declared' do declared(params, include_missing: false) end get '/declared?first=present&nested[fourth]=4' json = JSON.parse(last_response.body) expect(last_response.status).to eq(200) expect(json['first']).to eq 'present' expect(json['nested'].keys).to eq %w[fourth] expect(json['nested']['fourth']).to eq '4' end end describe '#declared; call from child namespace' do before do subject.format :json subject.namespace :parent do params do requires :parent_name, type: String end namespace ':parent_name' do params do requires :child_name, type: String requires :child_age, type: Integer end namespace ':child_name' do params do requires :grandchild_name, type: String end get ':grandchild_name' do { 'params' => params, 'without_parent_namespaces' => declared(params, include_parent_namespaces: false), 'with_parent_namespaces' => declared(params, include_parent_namespaces: true) } end end end end get '/parent/foo/bar/baz', child_age: 5, extra: 'hello' end let(:parsed_response) { JSON.parse(last_response.body, symbolize_names: true) } it { expect(last_response.status).to eq 200 } context 'with include_parent_namespaces: false' do it 'returns declared parameters only from current namespace' do expect(parsed_response[:without_parent_namespaces]).to eq( grandchild_name: 'baz' ) end end context 'with include_parent_namespaces: true' do it 'returns declared parameters from every parent namespace' do expect(parsed_response[:with_parent_namespaces]).to eq( parent_name: 'foo', child_name: 'bar', grandchild_name: 'baz', child_age: 5 ) end end context 'without declaration' do it 'returns all requested parameters' do expect(parsed_response[:params]).to eq( parent_name: 'foo', child_name: 'bar', grandchild_name: 'baz', child_age: 5, extra: 'hello' ) end end end describe '#declared; from a nested mounted endpoint' do before do doubly_mounted = Class.new(Grape::API) doubly_mounted.namespace :more do params do requires :y, type: Integer end route_param :y do get do { params: params, declared_params: declared(params) } end end end mounted = Class.new(Grape::API) mounted.namespace :another do params do requires :mount_space, type: Integer end route_param :mount_space do mount doubly_mounted end end subject.format :json subject.namespace :something do params do requires :id, type: Integer end resource ':id' do mount mounted end end end it 'can access parent attributes' do get '/something/123/another/456/more/789' expect(last_response.status).to eq 200 json = JSON.parse(last_response.body, symbolize_names: true) # test all three levels of params expect(json[:declared_params][:y]).to eq 789 expect(json[:declared_params][:mount_space]).to eq 456 expect(json[:declared_params][:id]).to eq 123 end end describe '#declared; mixed nesting' do before do subject.format :json subject.resource :users do route_param :id, type: Integer, desc: 'ID desc' do # Adding this causes route_setting(:declared_params) to be nil for the # get block in namespace 'foo' below get do end namespace 'foo' do get do { params: params, declared_params: declared(params), declared_params_no_parent: declared(params, include_parent_namespaces: false) } end end end end end it 'can access parent route_param' do get '/users/123/foo', bar: 'bar' expect(last_response.status).to eq 200 json = JSON.parse(last_response.body, symbolize_names: true) expect(json[:declared_params][:id]).to eq 123 expect(json[:declared_params_no_parent][:id]).to eq nil end end describe '#declared; with multiple route_param' do before do mounted = Class.new(Grape::API) mounted.namespace :albums do get do declared(params) end end subject.format :json subject.namespace :artists do route_param :id, type: Integer do get do declared(params) end params do requires :filter, type: String end get :some_route do declared(params) end end route_param :artist_id, type: Integer do namespace :compositions do get do declared(params) end end end route_param :compositor_id, type: Integer do mount mounted end end end it 'return only :id without :artist_id' do get '/artists/1' json = JSON.parse(last_response.body, symbolize_names: true) expect(json.key?(:id)).to be_truthy expect(json.key?(:artist_id)).not_to be_truthy end it 'return only :artist_id without :id' do get '/artists/1/compositions' json = JSON.parse(last_response.body, symbolize_names: true) expect(json.key?(:artist_id)).to be_truthy expect(json.key?(:id)).not_to be_truthy end it 'return :filter and :id parameters in declared for second enpoint inside route_param' do get '/artists/1/some_route', filter: 'some_filter' json = JSON.parse(last_response.body, symbolize_names: true) expect(json.key?(:filter)).to be_truthy expect(json.key?(:id)).to be_truthy expect(json.key?(:artist_id)).not_to be_truthy end it 'return :compositor_id for mounter in route_param' do get '/artists/1/albums' json = JSON.parse(last_response.body, symbolize_names: true) expect(json.key?(:compositor_id)).to be_truthy expect(json.key?(:id)).not_to be_truthy expect(json.key?(:artist_id)).not_to be_truthy end end end grape-1.5.2/spec/grape/presenters/0000755000004100000410000000000014016744305017073 5ustar www-datawww-datagrape-1.5.2/spec/grape/presenters/presenter_spec.rb0000644000004100000410000000325014016744305022441 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module Grape module Presenters module PresenterSpec class Dummy include Grape::DSL::InsideRoute attr_reader :env, :request, :new_settings def initialize @env = {} @header = {} @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } end end end describe Presenter do describe 'represent' do let(:object_mock) do Object.new end it 'represent object' do expect(Presenter.represent(object_mock)).to eq object_mock end end subject { PresenterSpec::Dummy.new } describe 'present' do let(:hash_mock) do { key: :value } end describe 'instance' do before do subject.present hash_mock, with: Grape::Presenters::Presenter end it 'presents dummy hash' do expect(subject.body).to eq hash_mock end end describe 'multiple presenter' do let(:hash_mock1) do { key1: :value1 } end let(:hash_mock2) do { key2: :value2 } end describe 'instance' do before do subject.present hash_mock1, with: Grape::Presenters::Presenter subject.present hash_mock2, with: Grape::Presenters::Presenter end it 'presents both dummy presenter' do expect(subject.body[:key1]).to eq hash_mock1[:key1] expect(subject.body[:key2]).to eq hash_mock2[:key2] end end end end end end end grape-1.5.2/spec/grape/middleware/0000755000004100000410000000000014016744305017016 5ustar www-datawww-datagrape-1.5.2/spec/grape/middleware/base_spec.rb0000644000004100000410000001210714016744305021270 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Base do subject { Grape::Middleware::Base.new(blank_app) } let(:blank_app) { ->(_) { [200, {}, 'Hi there.'] } } before do # Keep it one object for testing. allow(subject).to receive(:dup).and_return(subject) end it 'has the app as an accessor' do expect(subject.app).to eq(blank_app) end it 'calls through to the app' do expect(subject.call({})).to eq([200, {}, 'Hi there.']) end context 'callbacks' do it 'calls #before' do expect(subject).to receive(:before) end it 'calls #after' do expect(subject).to receive(:after) end after { subject.call!({}) } end context 'callbacks on error' do let(:blank_app) { ->(_) { raise StandardError } } it 'calls #after' do expect(subject).to receive(:after) expect { subject.call({}) }.to raise_error(StandardError) end end context 'after callback' do before do allow(subject).to receive(:after).and_return([200, {}, 'Hello from after callback']) end it 'overwrites application response' do expect(subject.call!({}).last).to eq('Hello from after callback') end end context 'after callback with errors' do it 'does not overwrite the application response' do expect(subject.call({})).to eq([200, {}, 'Hi there.']) end context 'with patched warnings' do before do @warnings = warnings = [] allow_any_instance_of(Grape::Middleware::Base).to receive(:warn) { |m| warnings << m } allow(subject).to receive(:after).and_raise(StandardError) end it 'does show a warning' do expect { subject.call({}) }.to raise_error(StandardError) expect(@warnings).not_to be_empty end end end it 'is able to access the response' do subject.call({}) expect(subject.response).to be_kind_of(Rack::Response) end describe '#response' do subject { Grape::Middleware::Base.new(response) } context Array do let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } } it 'status' do subject.call({}) expect(subject.response.status).to eq(204) end it 'body' do subject.call({}) expect(subject.response.body).to eq(['test']) end it 'header' do subject.call({}) expect(subject.response.header).to have_key(:abc) end end context Rack::Response do let(:response) { ->(_) { Rack::Response.new('test', 204, abc: 1) } } it 'status' do subject.call({}) expect(subject.response.status).to eq(204) end it 'body' do subject.call({}) expect(subject.response.body).to eq(['test']) end it 'header' do subject.call({}) expect(subject.response.header).to have_key(:abc) end end end describe '#context' do subject { Grape::Middleware::Base.new(blank_app) } it 'allows access to response context' do subject.call(Grape::Env::API_ENDPOINT => { header: 'some header' }) expect(subject.context).to eq(header: 'some header') end end context 'options' do it 'persists options passed at initialization' do expect(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true end context 'defaults' do module BaseSpec class ExampleWare < Grape::Middleware::Base def default_options { monkey: true } end end end it 'persists the default options' do expect(BaseSpec::ExampleWare.new(blank_app).options[:monkey]).to be true end it 'overrides default options when provided' do expect(BaseSpec::ExampleWare.new(blank_app, monkey: false).options[:monkey]).to be false end end end context 'header' do module HeaderSpec class ExampleWare < Grape::Middleware::Base def before header 'X-Test-Before', 'Hi' end def after header 'X-Test-After', 'Bye' nil end end end def app Rack::Builder.app do use HeaderSpec::ExampleWare run ->(_) { [200, {}, ['Yeah']] } end end it 'is able to set a header' do get '/' expect(last_response.headers['X-Test-Before']).to eq('Hi') expect(last_response.headers['X-Test-After']).to eq('Bye') end end context 'header overwrite' do module HeaderOverwritingSpec class ExampleWare < Grape::Middleware::Base def before header 'X-Test-Overwriting', 'Hi' end def after header 'X-Test-Overwriting', 'Bye' nil end end class API < Grape::API get('/') do header 'X-Test-Overwriting', 'Yeah' 'Hello' end end end def app Rack::Builder.app do use HeaderOverwritingSpec::ExampleWare run HeaderOverwritingSpec::API.new end end it 'overwrites header by after headers' do get '/' expect(last_response.headers['X-Test-Overwriting']).to eq('Bye') end end end grape-1.5.2/spec/grape/middleware/exception_spec.rb0000644000004100000410000002323714016744305022362 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Error do # raises a text exception module ExceptionSpec class ExceptionApp class << self def call(_env) raise 'rain!' end end end # raises a non-StandardError (ScriptError) exception class OtherExceptionApp class << self def call(_env) raise NotImplementedError, 'snow!' end end end # raises a hash error class ErrorHashApp class << self def error!(message, status) throw :error, message: { error: message, detail: 'missing widget' }, status: status end def call(_env) error!('rain!', 401) end end end # raises an error! class AccessDeniedApp class << self def error!(message, status) throw :error, message: message, status: status end def call(_env) error!('Access Denied', 401) end end end # raises a custom error class CustomError < Grape::Exceptions::Base end class CustomErrorApp class << self def call(_env) raise CustomError.new(status: 400, message: 'failed validation') end end end end def app subject end context 'with defaults' do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error run ExceptionSpec::ExceptionApp end end it 'does not trap errors by default' do expect { get '/' }.to raise_error(RuntimeError, 'rain!') end end context 'with rescue_all' do context 'StandardError exception' do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true run ExceptionSpec::ExceptionApp end end it 'sets the message appropriately' do get '/' expect(last_response.body).to eq('rain!') end it 'defaults to a 500 status' do get '/' expect(last_response.status).to eq(500) end end context 'Non-StandardError exception' do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true run ExceptionSpec::OtherExceptionApp end end it 'does not trap errors other than StandardError' do expect { get '/' }.to raise_error(NotImplementedError, 'snow!') end end end context 'Non-StandardError exception with a provided rescue handler' do context 'default error response' do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_handlers: { NotImplementedError => nil } run ExceptionSpec::OtherExceptionApp end end it 'rescues the exception using the default handler' do get '/' expect(last_response.body).to eq('snow!') end end context 'custom error response' do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_handlers: { NotImplementedError => -> { Rack::Response.new('rescued', 200, {}) } } run ExceptionSpec::OtherExceptionApp end end it 'rescues the exception using the provided handler' do get '/' expect(last_response.body).to eq('rescued') end end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, default_status: 500 run ExceptionSpec::ExceptionApp end end it 'is possible to specify a different default status code' do get '/' expect(last_response.status).to eq(500) end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :json run ExceptionSpec::ExceptionApp end end it 'is possible to return errors in json format' do get '/' expect(last_response.body).to eq('{"error":"rain!"}') end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :json run ExceptionSpec::ErrorHashApp end end it 'is possible to return hash errors in json format' do get '/' expect(['{"error":"rain!","detail":"missing widget"}', '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body) end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :jsonapi run ExceptionSpec::ExceptionApp end end it 'is possible to return errors in jsonapi format' do get '/' expect(last_response.body).to eq('{"error":"rain!"}') end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :jsonapi run ExceptionSpec::ErrorHashApp end end it 'is possible to return hash errors in jsonapi format' do get '/' expect(['{"error":"rain!","detail":"missing widget"}', '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body) end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :xml run ExceptionSpec::ExceptionApp end end it 'is possible to return errors in xml format' do get '/' expect(last_response.body).to eq("\n\n rain!\n\n") end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :xml run ExceptionSpec::ErrorHashApp end end it 'is possible to return hash errors in xml format' do get '/' expect(["\n\n missing widget\n rain!\n\n", "\n\n rain!\n missing widget\n\n"]).to include(last_response.body) end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :custom, error_formatters: { custom: lambda do |message, _backtrace, _options, _env, _original_exception| { custom_formatter: message }.inspect end } run ExceptionSpec::ExceptionApp end end it 'is possible to specify a custom formatter' do get '/' expect(last_response.body).to eq('{:custom_formatter=>"rain!"}') end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error run ExceptionSpec::AccessDeniedApp end end it 'does not trap regular error! codes' do get '/' expect(last_response.status).to eq(401) end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: false run ExceptionSpec::CustomErrorApp end end it 'responds to custom Grape exceptions appropriately' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('failed validation') end end context 'with rescue_options :backtrace and :exception set to true' do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :json, rescue_options: { backtrace: true, original_exception: true } run ExceptionSpec::ExceptionApp end end it 'is possible to return the backtrace and the original exception in json format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original_exception', 'RuntimeError') end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :xml, rescue_options: { backtrace: true, original_exception: true } run ExceptionSpec::ExceptionApp end end it 'is possible to return the backtrace and the original exception in xml format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original-exception', 'RuntimeError') end end context do subject do Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, rescue_all: true, format: :txt, rescue_options: { backtrace: true, original_exception: true } run ExceptionSpec::ExceptionApp end end it 'is possible to return the backtrace and the original exception in txt format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original exception', 'RuntimeError') end end end grape-1.5.2/spec/grape/middleware/versioner_spec.rb0000644000004100000410000000117014016744305022370 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Versioner do let(:klass) { Grape::Middleware::Versioner } it 'recognizes :path' do expect(klass.using(:path)).to eq(Grape::Middleware::Versioner::Path) end it 'recognizes :header' do expect(klass.using(:header)).to eq(Grape::Middleware::Versioner::Header) end it 'recognizes :param' do expect(klass.using(:param)).to eq(Grape::Middleware::Versioner::Param) end it 'recognizes :accept_version_header' do expect(klass.using(:accept_version_header)).to eq(Grape::Middleware::Versioner::AcceptVersionHeader) end end grape-1.5.2/spec/grape/middleware/error_spec.rb0000644000004100000410000000344614016744305021515 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'grape-entity' describe Grape::Middleware::Error do module ErrorSpec class ErrorEntity < Grape::Entity expose :code expose :static def static 'static text' end end class ErrApp class << self attr_accessor :error attr_accessor :format def call(_env) throw :error, error end end end end def app opts = options Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, **opts run ErrorSpec::ErrApp end end let(:options) { { default_message: 'Aww, hamburgers.' } } it 'sets the status code appropriately' do ErrorSpec::ErrApp.error = { status: 410 } get '/' expect(last_response.status).to eq(410) end it 'sets the error message appropriately' do ErrorSpec::ErrApp.error = { message: 'Awesome stuff.' } get '/' expect(last_response.body).to eq('Awesome stuff.') end it 'defaults to a 500 status' do ErrorSpec::ErrApp.error = {} get '/' expect(last_response.status).to eq(500) end it 'has a default message' do ErrorSpec::ErrApp.error = {} get '/' expect(last_response.body).to eq('Aww, hamburgers.') end context 'with http code' do let(:options) { { default_message: 'Aww, hamburgers.' } } it 'adds the status code if wanted' do ErrorSpec::ErrApp.error = { message: { code: 200 } } get '/' expect(last_response.body).to eq({ code: 200 }.to_json) end it 'presents an error message' do ErrorSpec::ErrApp.error = { message: { code: 200, with: ErrorSpec::ErrorEntity } } get '/' expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json) end end end grape-1.5.2/spec/grape/middleware/formatter_spec.rb0000644000004100000410000004025414016744305022365 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Formatter do subject { Grape::Middleware::Formatter.new(app) } before { allow(subject).to receive(:dup).and_return(subject) } let(:body) { { 'foo' => 'bar' } } let(:app) { ->(_env) { [200, {}, [body]] } } context 'serialization' do let(:body) { { 'abc' => 'def' } } it 'looks at the bodies for possibly serializable data' do _, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json') bodies.each { |b| expect(b).to eq(::Grape::Json.dump(body)) } end context 'default format' do let(:body) { ['foo'] } it 'calls #to_json since default format is json' do body.instance_eval do def to_json '"bar"' end end subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') } end end context 'jsonapi' do let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } } it 'calls #to_json if the content type is jsonapi' do body.instance_eval do def to_json '{"foos":[{"bar":"baz"}] }' end end subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') } end end context 'xml' do let(:body) { +'string' } it 'calls #to_xml if the content type is xml' do body.instance_eval do def to_xml '' end end subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('') } end end end context 'error handling' do let(:formatter) { double(:formatter) } before do allow(Grape::Formatter).to receive(:formatter_for) { formatter } end it 'rescues formatter-specific exceptions' do allow(formatter).to receive(:call) { raise Grape::Exceptions::InvalidFormatter.new(String, 'xml') } expect do catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } end.to_not raise_error end it 'does not rescue other exceptions' do allow(formatter).to receive(:call) { raise StandardError } expect do catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } end.to raise_error(StandardError) end end context 'detection' do it 'uses the xml extension if one is provided' do subject.call('PATH_INFO' => '/info.xml') expect(subject.env['api.format']).to eq(:xml) end it 'uses the json extension if one is provided' do subject.call('PATH_INFO' => '/info.json') expect(subject.env['api.format']).to eq(:json) end it 'uses the format parameter if one is provided' do subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=json') expect(subject.env['api.format']).to eq(:json) subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=xml') expect(subject.env['api.format']).to eq(:xml) end it 'uses the default format if none is provided' do subject.call('PATH_INFO' => '/info') expect(subject.env['api.format']).to eq(:txt) end it 'uses the requested format if provided in headers' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json') expect(subject.env['api.format']).to eq(:json) end it 'uses the file extension format if provided before headers' do subject.call('PATH_INFO' => '/info.txt', 'HTTP_ACCEPT' => 'application/json') expect(subject.env['api.format']).to eq(:txt) end end context 'accept header detection' do it 'detects from the Accept header' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml') expect(subject.env['api.format']).to eq(:xml) end it 'uses quality rankings to determine formats' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0') expect(subject.env['api.format']).to eq(:xml) subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3') expect(subject.env['api.format']).to eq(:json) end it 'handles quality rankings mixed with nothing' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0') expect(subject.env['api.format']).to eq(:xml) end it 'parses headers with other attributes' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') expect(subject.env['api.format']).to eq(:json) end it 'parses headers with vendor and api version' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test-v1+xml') expect(subject.env['api.format']).to eq(:xml) end context 'with custom vendored content types' do before do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/vnd.test+json' end it 'it uses the custom type' do subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(subject.env['api.format']).to eq(:custom) end end it 'parses headers with symbols as hash keys' do subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml', system_time: '091293') expect(subject.env[:system_time]).to eq('091293') end end context 'content-type' do it 'is set for json' do _, headers, = subject.call('PATH_INFO' => '/info.json') expect(headers['Content-type']).to eq('application/json') end it 'is set for xml' do _, headers, = subject.call('PATH_INFO' => '/info.xml') expect(headers['Content-type']).to eq('application/xml') end it 'is set for txt' do _, headers, = subject.call('PATH_INFO' => '/info.txt') expect(headers['Content-type']).to eq('text/plain') end it 'is set for custom' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/x-custom' _, headers, = subject.call('PATH_INFO' => '/info.custom') expect(headers['Content-type']).to eq('application/x-custom') end it 'is set for vendored with registered type' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = 'application/vnd.test+json' _, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(headers['Content-type']).to eq('application/vnd.test+json') end it 'is set to closest generic for custom vendored/versioned without registered type' do _, headers, = subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(headers['Content-type']).to eq('application/json') end end context 'format' do it 'uses custom formatter' do subject.options[:content_types] = {} subject.options[:content_types][:custom] = "don't care" subject.options[:formatters][:custom] = ->(_obj, _env) { 'CUSTOM FORMAT' } _, _, body = subject.call('PATH_INFO' => '/info.custom') expect(read_chunks(body)).to eq(['CUSTOM FORMAT']) end context 'default' do let(:body) { ['blah'] } it 'uses default json formatter' do _, _, body = subject.call('PATH_INFO' => '/info.json') expect(read_chunks(body)).to eq(['["blah"]']) end end it 'uses custom json formatter' do subject.options[:formatters][:json] = ->(_obj, _env) { 'CUSTOM JSON FORMAT' } _, _, body = subject.call('PATH_INFO' => '/info.json') expect(read_chunks(body)).to eq(['CUSTOM JSON FORMAT']) end end context 'no content responses' do let(:no_content_response) { ->(status) { [status, {}, ['']] } } STATUSES_WITHOUT_BODY = if Gem::Version.new(Rack.release) >= Gem::Version.new('2.1.0') Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.keys else Rack::Utils::STATUS_WITH_NO_ENTITY_BODY end STATUSES_WITHOUT_BODY.each do |status| it "does not modify a #{status} response" do expected_response = no_content_response[status] allow(app).to receive(:call).and_return(expected_response) expect(subject.call({})).to eq(expected_response) end end end context 'input' do %w[POST PATCH PUT DELETE].each do |method| context 'when body is not nil or empty' do context 'when Content-Type is supported' do let(:io) { StringIO.new('{"is_boolean":true,"string":"thing"}') } let(:content_type) { 'application/json' } it "parses the body from #{method} and copies values into rack.request.form_hash" do 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 context 'when Content-Type is not supported' do let(:io) { StringIO.new('{"is_boolean":true,"string":"thing"}') } let(:content_type) { 'application/atom+xml' } it 'returns a 415 HTTP error status' do error = catch(:error) do subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => content_type, 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) end expect(error[:status]).to eq(415) expect(error[:message]).to eq("The provided content-type 'application/atom+xml' is not supported.") end end end context 'when body is nil' do let(:io) { double } before do allow(io).to receive_message_chain(:rewind, :read).and_return(nil) end it 'does not read and parse the body' do expect(subject).not_to receive(:read_rack_input) subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => 'application/json', 'rack.input' => io, 'CONTENT_LENGTH' => 0 ) end end context 'when body is empty' do let(:io) { double } before do allow(io).to receive_message_chain(:rewind, :read).and_return('') end it 'does not read and parse the body' do expect(subject).not_to receive(:read_rack_input) subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => 'application/json', 'rack.input' => io, 'CONTENT_LENGTH' => 0 ) end end ['application/json', 'application/json; charset=utf-8'].each do |content_type| context content_type do it "parses the body from #{method} and copies values into rack.request.form_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => content_type, 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) expect(subject.env['rack.request.form_hash']['is_boolean']).to be true expect(subject.env['rack.request.form_hash']['string']).to eq('thing') end end end it "parses the chunked body from #{method} and copies values into rack.request.from_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') subject.call( 'PATH_INFO' => '/infol', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => 'application/json', 'rack.input' => io, 'HTTP_TRANSFER_ENCODING' => 'chunked' ) expect(subject.env['rack.request.form_hash']['is_boolean']).to be true expect(subject.env['rack.request.form_hash']['string']).to eq('thing') end it 'rewinds IO' do io = StringIO.new('{"is_boolean":true,"string":"thing"}') io.read subject.call( 'PATH_INFO' => '/infol', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => 'application/json', 'rack.input' => io, 'HTTP_TRANSFER_ENCODING' => 'chunked' ) expect(subject.env['rack.request.form_hash']['is_boolean']).to be true expect(subject.env['rack.request.form_hash']['string']).to eq('thing') end it "parses the body from an xml #{method} and copies values into rack.request.from_hash" do io = StringIO.new('Test') subject.call( 'PATH_INFO' => '/info.xml', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => 'application/xml', 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) if Object.const_defined? :MultiXml expect(subject.env['rack.request.form_hash']['thing']['name']).to eq('Test') else expect(subject.env['rack.request.form_hash']['thing']['name']['__content__']).to eq('Test') end end [Rack::Request::FORM_DATA_MEDIA_TYPES, Rack::Request::PARSEABLE_DATA_MEDIA_TYPES].flatten.each do |content_type| it "ignores #{content_type}" do io = StringIO.new('name=Other+Test+Thing') subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => method, 'CONTENT_TYPE' => content_type, 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) expect(subject.env['rack.request.form_hash']).to be_nil end end end end context 'send file' do let(:file) { double(File) } let(:file_body) { Grape::ServeStream::StreamResponse.new(file) } let(:app) { ->(_env) { [200, {}, file_body] } } it 'returns a file response' do expect(file).to receive(:each).and_yield('data') env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } status, headers, body = subject.call(env) expect(status).to be == 200 expect(headers).to be == { 'Content-Type' => 'application/json' } expect(read_chunks(body)).to be == ['data'] end end context 'inheritable formatters' do class InvalidFormatter def self.call(_, _) { message: 'invalid' }.to_json end end let(:app) { ->(_env) { [200, {}, ['']] } } before do Grape::Formatter.register :invalid, InvalidFormatter Grape::ContentTypes.register :invalid, 'application/x-invalid' end after do Grape::ContentTypes.default_elements.delete(:invalid) Grape::Formatter.default_elements.delete(:invalid) end it 'returns response by invalid formatter' do env = { 'PATH_INFO' => '/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid' } _, _, body = *subject.call(env) expect(read_chunks(body).join).to eq({ message: 'invalid' }.to_json) end end context 'custom parser raises exception and rescue options are enabled for backtrace and original_exception' do it 'adds the backtrace and original_exception to the error output' do subject = Grape::Middleware::Formatter.new( app, rescue_options: { backtrace: true, original_exception: true }, parsers: { json: ->(_object, _env) { raise StandardError, 'fail' } } ) io = StringIO.new('{invalid}') error = catch(:error) do subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => 'POST', 'CONTENT_TYPE' => 'application/json', 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) end expect(error[:message]).to eq 'fail' expect(error[:backtrace].size).to be >= 1 expect(error[:original_exception].class).to eq StandardError end end end grape-1.5.2/spec/grape/middleware/stack_spec.rb0000644000004100000410000001255314016744305021470 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Stack do module StackSpec class FooMiddleware; end class BarMiddleware; end class BlockMiddleware attr_reader :block def initialize(&block) @block = block end end end let(:proc) { ->() {} } let(:others) { [[:use, StackSpec::BarMiddleware], [:insert_before, StackSpec::BarMiddleware, StackSpec::BlockMiddleware, proc]] } subject { Grape::Middleware::Stack.new } before do subject.use StackSpec::FooMiddleware end describe '#use' do it 'pushes a middleware class onto the stack' do expect { subject.use StackSpec::BarMiddleware } .to change { subject.size }.by(1) expect(subject.last).to eq(StackSpec::BarMiddleware) end it 'pushes a middleware class with arguments onto the stack' do expect { subject.use StackSpec::BarMiddleware, false, my_arg: 42 } .to change { subject.size }.by(1) expect(subject.last).to eq(StackSpec::BarMiddleware) expect(subject.last.args).to eq([false, { my_arg: 42 }]) end it 'pushes a middleware class with block arguments onto the stack' do expect { subject.use StackSpec::BlockMiddleware, &proc } .to change { subject.size }.by(1) expect(subject.last).to eq(StackSpec::BlockMiddleware) expect(subject.last.args).to eq([]) expect(subject.last.block).to eq(proc) end end describe '#insert' do it 'inserts a middleware class at the integer index' do expect { subject.insert 0, StackSpec::BarMiddleware } .to change { subject.size }.by(1) expect(subject[0]).to eq(StackSpec::BarMiddleware) expect(subject[1]).to eq(StackSpec::FooMiddleware) end end describe '#insert_before' do it 'inserts a middleware before another middleware class' do expect { subject.insert_before StackSpec::FooMiddleware, StackSpec::BarMiddleware } .to change { subject.size }.by(1) expect(subject[0]).to eq(StackSpec::BarMiddleware) expect(subject[1]).to eq(StackSpec::FooMiddleware) end it 'inserts a middleware before an anonymous class given by its superclass' do subject.use Class.new(StackSpec::BlockMiddleware) expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware } .to change { subject.size }.by(1) expect(subject[1]).to eq(StackSpec::BarMiddleware) expect(subject[2]).to eq(StackSpec::BlockMiddleware) end it 'raises an error on an invalid index' do expect { subject.insert_before StackSpec::BlockMiddleware, StackSpec::BarMiddleware } .to raise_error(RuntimeError, 'No such middleware to insert before: StackSpec::BlockMiddleware') end end describe '#insert_after' do it 'inserts a middleware after another middleware class' do expect { subject.insert_after StackSpec::FooMiddleware, StackSpec::BarMiddleware } .to change { subject.size }.by(1) expect(subject[1]).to eq(StackSpec::BarMiddleware) expect(subject[0]).to eq(StackSpec::FooMiddleware) end it 'inserts a middleware after an anonymous class given by its superclass' do subject.use Class.new(StackSpec::BlockMiddleware) expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware } .to change { subject.size }.by(1) expect(subject[1]).to eq(StackSpec::BlockMiddleware) expect(subject[2]).to eq(StackSpec::BarMiddleware) end it 'raises an error on an invalid index' do expect { subject.insert_after StackSpec::BlockMiddleware, StackSpec::BarMiddleware } .to raise_error(RuntimeError, 'No such middleware to insert after: StackSpec::BlockMiddleware') end end describe '#merge_with' do it 'applies a collection of operations and middlewares' do expect { subject.merge_with(others) } .to change { subject.size }.by(2) expect(subject[0]).to eq(StackSpec::FooMiddleware) expect(subject[1]).to eq(StackSpec::BlockMiddleware) expect(subject[2]).to eq(StackSpec::BarMiddleware) end context 'middleware spec with proc declaration exists' do let(:middleware_spec_with_proc) { [:use, StackSpec::FooMiddleware, proc] } it 'properly forwards spec arguments' do expect(subject).to receive(:use).with(StackSpec::FooMiddleware) subject.merge_with([middleware_spec_with_proc]) end end end describe '#build' do it 'returns a rack builder instance' do expect(subject.build).to be_instance_of(Rack::Builder) end context 'when @others are present' do let(:others) { [[:insert_after, Grape::Middleware::Formatter, StackSpec::BarMiddleware]] } it 'applies the middleware specs stored in @others' do subject.concat others subject.use Grape::Middleware::Formatter subject.build expect(subject[0]).to eq StackSpec::FooMiddleware expect(subject[1]).to eq Grape::Middleware::Formatter expect(subject[2]).to eq StackSpec::BarMiddleware end end end describe '#concat' do it 'adds non :use specs to @others' do expect { subject.concat others }.to change(subject, :others).from([]).to([[others.last]]) end it 'calls +merge_with+ with the :use specs' do expect(subject).to receive(:merge_with).with [[:use, StackSpec::BarMiddleware]] subject.concat others end end end grape-1.5.2/spec/grape/middleware/auth/0000755000004100000410000000000014016744305017757 5ustar www-datawww-datagrape-1.5.2/spec/grape/middleware/auth/base_spec.rb0000644000004100000410000000141014016744305022224 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'base64' describe Grape::Middleware::Auth::Base do subject do Class.new(Grape::API) do http_basic realm: 'my_realm' do |user, password| user && password && user == password end get '/authorized' do 'DONE' end end end def app subject end it 'authenticates if given valid creds' do get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') expect(last_response.status).to eq(200) expect(last_response.body).to eq('DONE') end it 'throws a 401 is wrong auth is given' do get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') expect(last_response.status).to eq(401) end end grape-1.5.2/spec/grape/middleware/auth/dsl_spec.rb0000644000004100000410000000335214016744305022103 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Auth::DSL do subject { Class.new(Grape::API) } let(:block) { ->() {} } let(:settings) do { opaque: 'secret', proc: block, realm: 'API Authorization', type: :http_digest } end describe '.auth' do it 'stets auth parameters' do expect(subject.base_instance).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.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings) expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings.merge(realm: 'super_secret')) subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc] first_settings = subject.auth subject.auth :http_digest, realm: 'super_secret', opaque: settings[:opaque], &settings[:proc] expect(subject.auth).to eq(settings.merge(realm: 'super_secret')) expect(subject.auth.object_id).not_to eq(first_settings.object_id) end end describe '.http_basic' do it 'stets auth parameters' do subject.http_basic realm: 'my_realm', &settings[:proc] expect(subject.auth).to eq(realm: 'my_realm', type: :http_basic, proc: block) end end describe '.http_digest' do it 'stets auth parameters' do subject.http_digest realm: 'my_realm', opaque: 'my_opaque', &settings[:proc] expect(subject.auth).to eq(realm: 'my_realm', type: :http_digest, proc: block, opaque: 'my_opaque') end end end grape-1.5.2/spec/grape/middleware/auth/strategies_spec.rb0000644000004100000410000000412714016744305023474 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'base64' describe Grape::Middleware::Auth::Strategies do context 'Basic Auth' do def app proc = ->(u, p) { u && p && u == p } Rack::Builder.new do |b| b.use Grape::Middleware::Error b.use(Grape::Middleware::Auth::Base, type: :http_basic, proc: proc) b.run ->(_env) { [200, {}, ['Hello there.']] } end end it 'throws a 401 if no auth is given' do @proc = -> { false } get '/whatever' expect(last_response.status).to eq(401) end it 'authenticates if given valid creds' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') expect(last_response.status).to eq(200) end it 'throws a 401 is wrong auth is given' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') expect(last_response.status).to eq(401) end end context 'Digest MD5 Auth' do RSpec::Matchers.define :be_challenge do match do |actual_response| actual_response.status == 401 && actual_response['WWW-Authenticate'].start_with?('Digest ') && actual_response.body.empty? end end module StrategiesSpec class Test < Grape::API http_digest(realm: 'Test Api', opaque: 'secret') do |username| { 'foo' => 'bar' }[username] end get '/test' do [{ hey: 'you' }, { there: 'bar' }, { foo: 'baz' }] end end end def app StrategiesSpec::Test end it 'is a digest authentication challenge' do get '/test' expect(last_response).to be_challenge end it 'throws a 401 if no auth is given' do get '/test' expect(last_response.status).to eq(401) end it 'authenticates if given valid creds' do digest_authorize 'foo', 'bar' get '/test' expect(last_response.status).to eq(200) end it 'throws a 401 if given invalid creds' do digest_authorize 'bar', 'foo' get '/test' expect(last_response.status).to eq(401) end end end grape-1.5.2/spec/grape/middleware/globals_spec.rb0000644000004100000410000000164114016744305022002 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Globals do subject { Grape::Middleware::Globals.new(blank_app) } before { allow(subject).to receive(:dup).and_return(subject) } let(:blank_app) { ->(_env) { [200, {}, 'Hi there.'] } } it 'calls through to the app' do expect(subject.call({})).to eq([200, {}, 'Hi there.']) end context 'environment' do it 'should set the grape.request environment' do subject.call({}) expect(subject.env['grape.request']).to be_a(Grape::Request) end it 'should set the grape.request.headers environment' do subject.call({}) expect(subject.env['grape.request.headers']).to be_a(Hash) end it 'should set the grape.request.params environment' do subject.call('QUERY_STRING' => 'test=1', 'rack.input' => StringIO.new) expect(subject.env['grape.request.params']).to be_a(Hash) end end end grape-1.5.2/spec/grape/middleware/versioner/0000755000004100000410000000000014016744305021032 5ustar www-datawww-datagrape-1.5.2/spec/grape/middleware/versioner/accept_version_header_spec.rb0000644000004100000410000000634714016744305026717 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Versioner::AcceptVersionHeader do let(:app) { ->(env) { [200, env, env] } } subject { Grape::Middleware::Versioner::AcceptVersionHeader.new(app, **(@options || {})) } before do @options = { version_options: { using: :accept_version_header } } end context 'api.version' do before do @options[:versions] = ['v1'] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') expect(env['api.version']).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') expect(env['api.version']).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is not supported' do expect do subject.call('HTTP_ACCEPT_VERSION' => 'v2').last end.to throw_symbol( :error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'The requested version is not supported.' ) end end it 'succeeds if :strict is not set' do expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end context 'when :strict is set' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true end it 'fails with 406 Not Acceptable if header is not set' do expect do subject.call({}).last end.to throw_symbol( :error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'Accept-Version header must be set.' ) end it 'fails with 406 Not Acceptable if header is empty' do expect do subject.call('HTTP_ACCEPT_VERSION' => '').last end.to throw_symbol( :error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'Accept-Version header must be set.' ) end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) end end context 'when :strict and cascade: false' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true @options[:version_options][:cascade] = false end it 'fails with 406 Not Acceptable if header is not set' do expect do subject.call({}).last end.to throw_symbol( :error, status: 406, headers: {}, message: 'Accept-Version header must be set.' ) end it 'fails with 406 Not Acceptable if header is empty' do expect do subject.call('HTTP_ACCEPT_VERSION' => '').last end.to throw_symbol( :error, status: 406, headers: {}, message: 'Accept-Version header must be set.' ) end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) end end end grape-1.5.2/spec/grape/middleware/versioner/header_spec.rb0000644000004100000410000002641114016744305023625 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Versioner::Header do let(:app) { ->(env) { [200, env, env] } } subject { Grape::Middleware::Versioner::Header.new(app, **(@options || {})) } before do @options = { version_options: { using: :header, vendor: 'vendor' } } end context 'api.type and api.subtype' do it 'sets type and subtype to first choice of content type if no preference given' do status, _, env = subject.call('HTTP_ACCEPT' => '*/*') expect(env['api.type']).to eql 'application' expect(env['api.subtype']).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/*') expect(env['api.type']).to eql 'application' expect(env['api.subtype']).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type and subtype' do status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain') expect(env['api.type']).to eql 'text' expect(env['api.subtype']).to eql 'plain' expect(status).to eq(200) end end context 'api.format' do it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') expect(env['api.format']).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') expect(env['api.format']).to eql nil expect(status).to eq(200) end ['v1', :v1].each do |version| context "when version is set to #{version}" do before do @options[:versions] = [version] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env['api.format']).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env['api.format']).to eql nil expect(status).to eq(200) end end end end context 'api.vendor' do it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') expect(env['api.vendor']).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') expect(env['api.vendor']).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include 'API vendor not found' end end context 'when version is set' do before do @options[:versions] = ['v1'] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env['api.vendor']).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env['api.vendor']).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('API vendor not found') end end end end context 'api.version' do before do @options[:versions] = ['v1'] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env['api.version']).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env['api.version']).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('API version not found') end end end it 'succeeds if :strict is not set' do expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false and given an invalid header' do @options[:version_options][:strict] = false expect(subject.call('HTTP_ACCEPT' => 'yaml').first).to eq(200) expect(subject.call({}).first).to eq(200) end context 'when :strict is set' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true end it 'fails with 406 Not Acceptable if header is not set' do expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header is empty' do expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('Accept header must be set.') end end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end end context 'when :strict and cascade: false' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true @options[:version_options][:cascade] = false end it 'fails with 406 Not Acceptable if header is not set' do expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to eql 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header is application/xml' do expect { subject.call('HTTP_ACCEPT' => 'application/xml').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to eql 406 expect(exception.message).to include('API vendor or version not found.') end end it 'fails with 406 Not Acceptable if header is empty' do expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to eql 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header contains a single invalid accept' do expect { subject.call('HTTP_ACCEPT' => 'application/json;application/vnd.vendor-v1+json').first } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to eql 406 expect(exception.message).to include('API vendor or version not found.') end end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end end context 'when multiple versions are specified' do before do @options[:versions] = %w[v1 v2] end it 'succeeds with v1' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end it 'succeeds with v2' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200) end it 'fails with another version' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to eql 406 expect(exception.message).to include('API version not found') end end end context 'when there are multiple versions with complex vendor specified with rescue_from :all' do subject do Class.new(Grape::API) do rescue_from :all end end let(:v1_app) do Class.new(Grape::API) do version 'v1', using: :header, vendor: 'test.a-cool_resource', cascade: false, strict: true content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json' formatter :v1_test, ->(object, _) { object } format :v1_test resources :users do get :hello do 'one' end end end end let(:v2_app) do Class.new(Grape::API) do version 'v2', using: :header, vendor: 'test.a-cool_resource', strict: true content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json' formatter :v2_test, ->(object, _) { object } format :v2_test resources :users do get :hello do 'two' end end end end def app subject.mount v2_app subject.mount v1_app subject end context 'with header versioned endpoints and a rescue_all block defined' do it 'responds correctly to a v1 request' do versioned_get '/users/hello', 'v1', using: :header, vendor: 'test.a-cool_resource' expect(last_response.body).to eq('one') expect(last_response.body).not_to include('API vendor or version not found') end it 'responds correctly to a v2 request' do versioned_get '/users/hello', 'v2', using: :header, vendor: 'test.a-cool_resource' expect(last_response.body).to eq('two') expect(last_response.body).not_to include('API vendor or version not found') end end end end grape-1.5.2/spec/grape/middleware/versioner/param_spec.rb0000644000004100000410000001233514016744305023475 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Versioner::Param do let(:app) { ->(env) { [200, env, env['api.version']] } } let(:options) { {} } subject { Grape::Middleware::Versioner::Param.new(app, **options) } it 'sets the API version based on the default param (apiver)' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1]['api.version']).to eq('v1') end it 'cuts (only) the version out of the params' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1', 'other_param' => '5' }) env['rack.request.query_hash'] = Rack::Utils.parse_nested_query(env['QUERY_STRING']) expect(subject.call(env)[1]['rack.request.query_hash']['apiver']).to be_nil expect(subject.call(env)[1]['rack.request.query_hash']['other_param']).to eq('5') end it 'provides a nil version if no version is given' do env = Rack::MockRequest.env_for('/') expect(subject.call(env).last).to be_nil end context 'with specified parameter name' do let(:options) { { version_options: { parameter: 'v' } } } it 'sets the API version based on the custom parameter name' do env = Rack::MockRequest.env_for('/awesome', params: { 'v' => 'v1' }) expect(subject.call(env)[1]['api.version']).to eq('v1') end it 'does not set the API version based on the default param' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1]['api.version']).to be_nil end end context 'with specified versions' do let(:options) { { versions: %w[v1 v2] } } it 'throws an error if a non-allowed version is specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v3' }) expect(catch(:error) { subject.call(env) }[:status]).to eq(404) end it 'allows versions that have been specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1]['api.version']).to eq('v1') end end context 'when no version is set' do let(:options) do { versions: ['v1'], version_options: { using: :header } } end it 'returns a 200 (matches the first version found)' do env = Rack::MockRequest.env_for('/awesome', params: {}) expect(subject.call(env).first).to eq(200) end end context 'when there are multiple versions without a custom param' do subject { Class.new(Grape::API) } let(:v1_app) do Class.new(Grape::API) do version 'v1', using: :param content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json' formatter :v1_test, ->(object, _) { object } format :v1_test resources :users do get :hello do 'one' end end end end let(:v2_app) do Class.new(Grape::API) do version 'v2', using: :param content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json' formatter :v2_test, ->(object, _) { object } format :v2_test resources :users do get :hello do 'two' end end end end def app subject.mount v2_app subject.mount v1_app subject end it 'responds correctly to a v1 request' do versioned_get '/users/hello', 'v1', using: :param, parameter: :apiver expect(last_response.body).to eq('one') expect(last_response.body).not_to include('API vendor or version not found') end it 'responds correctly to a v2 request' do versioned_get '/users/hello', 'v2', using: :param, parameter: :apiver expect(last_response.body).to eq('two') expect(last_response.body).not_to include('API vendor or version not found') end end context 'when there are multiple versions with a custom param' do subject { Class.new(Grape::API) } let(:v1_app) do Class.new(Grape::API) do version 'v1', using: :param, parameter: 'v' content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json' formatter :v1_test, ->(object, _) { object } format :v1_test resources :users do get :hello do 'one' end end end end let(:v2_app) do Class.new(Grape::API) do version 'v2', using: :param, parameter: 'v' content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json' formatter :v2_test, ->(object, _) { object } format :v2_test resources :users do get :hello do 'two' end end end end def app subject.mount v2_app subject.mount v1_app subject end it 'responds correctly to a v1 request' do versioned_get '/users/hello', 'v1', using: :param, parameter: 'v' expect(last_response.body).to eq('one') expect(last_response.body).not_to include('API vendor or version not found') end it 'responds correctly to a v2 request' do versioned_get '/users/hello', 'v2', using: :param, parameter: 'v' expect(last_response.body).to eq('two') expect(last_response.body).not_to include('API vendor or version not found') end end end grape-1.5.2/spec/grape/middleware/versioner/path_spec.rb0000644000004100000410000000370114016744305023326 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Middleware::Versioner::Path do let(:app) { ->(env) { [200, env, env['api.version']] } } let(:options) { {} } subject { Grape::Middleware::Versioner::Path.new(app, **options) } it 'sets the API version based on the first path' do expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1') end it 'does not cut the version out of the path' do expect(subject.call('PATH_INFO' => '/v1/awesome')[1]['PATH_INFO']).to eq('/v1/awesome') end it 'provides a nil version if no path is given' do expect(subject.call('PATH_INFO' => '/').last).to be_nil end context 'with a pattern' do let(:options) { { pattern: /v./i } } it 'sets the version if it matches' do expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1') end it 'ignores the version if it fails to match' do expect(subject.call('PATH_INFO' => '/awesome/radical').last).to be_nil end end [%w[v1 v2], %i[v1 v2], [:v1, 'v2'], ['v1', :v2]].each do |versions| context "with specified versions as #{versions}" do let(:options) { { versions: versions } } it 'throws an error if a non-allowed version is specified' do expect(catch(:error) { subject.call('PATH_INFO' => '/v3/awesome') }[:status]).to eq(404) end it 'allows versions that have been specified' do expect(subject.call('PATH_INFO' => '/v1/asoasd').last).to eq('v1') end end end context 'with prefix, but requested version is not matched' do let(:options) { { prefix: '/v1', pattern: /v./i } } it 'recognizes potential version' do expect(subject.call('PATH_INFO' => '/v3/foo').last).to eq('v3') end end context 'with mount path' do let(:options) { { mount_path: '/mounted', versions: [:v1] } } it 'recognizes potential version' do expect(subject.call('PATH_INFO' => '/mounted/v1/foo').last).to eq('v1') end end end grape-1.5.2/spec/grape/exceptions/0000755000004100000410000000000014016744305017062 5ustar www-datawww-datagrape-1.5.2/spec/grape/exceptions/base_spec.rb0000644000004100000410000000351414016744305021336 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::Base do describe '#compose_message' do subject { described_class.new.send(:compose_message, key, **attributes) } let(:key) { :invalid_formatter } let(:attributes) { { klass: String, to_format: 'xml' } } after do I18n.enforce_available_locales = true I18n.available_locales = %i[en] I18n.locale = :en I18n.default_locale = :en I18n.reload! end context 'when I18n enforces available locales' do before { I18n.enforce_available_locales = true } context 'when the fallback locale is available' do before do I18n.available_locales = %i[de en] I18n.default_locale = :de end it 'returns the translated message' do expect(subject).to eq('cannot convert String to xml') end end context 'when the fallback locale is not available' do before do I18n.available_locales = %i[de jp] I18n.locale = :de I18n.default_locale = :de end it 'returns the translation string' do expect(subject).to eq("grape.errors.messages.#{key}") end end end context 'when I18n does not enforce available locales' do before { I18n.enforce_available_locales = false } context 'when the fallback locale is available' do before { I18n.available_locales = %i[de en] } it 'returns the translated message' do expect(subject).to eq('cannot convert String to xml') end end context 'when the fallback locale is not available' do before { I18n.available_locales = %i[de jp] } it 'returns the translated message' do expect(subject).to eq('cannot convert String to xml') end end end end end grape-1.5.2/spec/grape/exceptions/invalid_accept_header_spec.rb0000644000004100000410000002466614016744305024714 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::InvalidAcceptHeader do shared_examples_for 'a valid request' do it 'does return with status 200' do expect(last_response.status).to eq 200 end it 'does return the expected result' do expect(last_response.body).to eq('beer received') end end shared_examples_for 'a cascaded request' do it 'does not find a matching route' do expect(last_response.status).to eq 404 end end shared_examples_for 'a not-cascaded request' do it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil end it 'does not accept the request' do expect(last_response.status).to eq 406 end end shared_examples_for 'a rescued request' do it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil end it 'does show rescue handler processing' do expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'API with cascade=false and rescue_from :all handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| rack_response 'message was processed', 400, e[:headers] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_should_behave_like 'a rescued request' end end end context 'API with cascade=false and without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_should_behave_like 'a not-cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_should_behave_like 'a not-cascaded request' end end end context 'API with cascade=false and with rescue_from :all handler and http_codes' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| rack_response 'message was processed', 400, e[:headers] end subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_should_behave_like 'a rescued request' end end end context 'API with cascade=false, http_codes but without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_should_behave_like 'a not-cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_should_behave_like 'a not-cascaded request' end end end context 'API with cascade=true and rescue_from :all handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| rack_response 'message was processed', 400, e[:headers] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end it_should_behave_like 'a cascaded request' end context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_should_behave_like 'a cascaded request' end end end context 'API with cascade=true and without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_should_behave_like 'a cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_should_behave_like 'a cascaded request' end end end context 'API with cascade=true and with rescue_from :all handler and http_codes' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| rack_response 'message was processed', 400, e[:headers] end subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end it_should_behave_like 'a cascaded request' end context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_should_behave_like 'a cascaded request' end end end context 'API with cascade=true, http_codes but without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_should_behave_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_should_behave_like 'a cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_should_behave_like 'a cascaded request' end end end end grape-1.5.2/spec/grape/exceptions/missing_option_spec.rb0000644000004100000410000000052514016744305023464 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::MissingOption do describe '#message' do let(:error) do described_class.new(:path) end it 'contains the problem in the message' do expect(error.message).to include( 'You must specify :path options.' ) end end end grape-1.5.2/spec/grape/exceptions/validation_spec.rb0000644000004100000410000000124614016744305022556 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::Validation do it 'fails when params are missing' do expect { Grape::Exceptions::Validation.new(message: 'presence') }.to raise_error(ArgumentError, /missing keyword:.+?params/) end context 'when message is a symbol' do it 'stores message_key' do expect(Grape::Exceptions::Validation.new(params: ['id'], message: :presence).message_key).to eq(:presence) end end context 'when message is a String' do it 'does not store the message_key' do expect(Grape::Exceptions::Validation.new(params: ['id'], message: 'presence').message_key).to eq(nil) end end end grape-1.5.2/spec/grape/exceptions/body_parse_errors_spec.rb0000644000004100000410000001025714016744305024151 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::ValidationErrors do context 'api with rescue_from :all handler' do subject { Class.new(Grape::API) } before do subject.rescue_from :all do |_e| rack_response 'message was processed', 400 end subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'with content_type json' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'with content_type xml' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'with content_type text' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'with no specific content_type' do it 'can recover from failed body parsing' do post '/beer', 'test', {} expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end end context 'api with rescue_from :grape_exceptions handler' do subject { Class.new(Grape::API) } before do subject.rescue_from :all do |_e| rack_response 'message was processed', 400 end subject.rescue_from :grape_exceptions subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'with content_type json' do it 'returns body parsing error message' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to include 'message body does not match declared format' end end context 'with content_type xml' do it 'returns body parsing error message' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to include 'message body does not match declared format' end end end context 'api without a rescue handler' do subject { Class.new(Grape::API) } before do subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'and with content_type json' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to include('message body does not match declared format') expect(last_response.body).to include('application/json') end end context 'with content_type xml' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to include('message body does not match declared format') expect(last_response.body).to include('application/xml') end end context 'with content_type text' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq 400 expect(last_response.body).to eq('beer is missing') end end context 'and with no specific content_type' do it 'can recover from failed body parsing' do post '/beer', 'test', {} expect(last_response.status).to eq 400 # plain response with text/html expect(last_response.body).to eq('beer is missing') end end end end grape-1.5.2/spec/grape/exceptions/unknown_options_spec.rb0000644000004100000410000000051214016744305023671 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::UnknownOptions do describe '#message' do let(:error) do described_class.new(%i[a b]) end it 'contains the problem in the message' do expect(error.message).to include( 'unknown options: ' ) end end end grape-1.5.2/spec/grape/exceptions/validation_errors_spec.rb0000644000004100000410000000520214016744305024146 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'ostruct' describe Grape::Exceptions::ValidationErrors do let(:validation_message) { 'FooBar is invalid' } let(:validation_error) { OpenStruct.new(params: [validation_message]) } context 'initialize' do let(:headers) do { 'A-Header-Key' => 'A-Header-Value' } end subject do described_class.new(errors: [validation_error], headers: headers) end it 'should assign headers through base class' do expect(subject.headers).to eq(headers) end end context 'message' do context 'is not repeated' do let(:error) do described_class.new(errors: [validation_error, validation_error]) end subject(:message) { error.message.split(',').map(&:strip) } it { expect(message).to include validation_message } it { expect(message.size).to eq 1 } end end describe '#full_messages' do context 'with errors' do let(:validation_error_1) { Grape::Exceptions::Validation.new(params: ['id'], message: :presence) } let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message: :presence) } subject { described_class.new(errors: [validation_error_1, validation_error_2]).full_messages } it 'returns an array with each errors full message' do expect(subject).to contain_exactly('id is missing', 'name is missing') end end context 'when attributes is an array of symbols' do let(:validation_error) { Grape::Exceptions::Validation.new(params: [:admin_field], message: 'Can not set admin-only field') } subject { described_class.new(errors: [validation_error]).full_messages } it 'returns an array with an error full message' do expect(subject.first).to eq('admin_field Can not set admin-only field') end end end context 'api' do subject { Class.new(Grape::API) } def app subject end it 'can return structured json with separate fields' do subject.format :json subject.rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e, 400) end subject.params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end subject.get '/exactly_one_of' do 'exactly_one_of works!' end get '/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(JSON.parse(last_response.body)).to eq( [ 'params' => %w[beer wine], 'messages' => ['are mutually exclusive'] ] ) end end end grape-1.5.2/spec/grape/exceptions/invalid_formatter_spec.rb0000644000004100000410000000053514016744305024135 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::InvalidFormatter do describe '#message' do let(:error) do described_class.new(String, 'xml') end it 'contains the problem in the message' do expect(error.message).to include( 'cannot convert String to xml' ) end end end grape-1.5.2/spec/grape/exceptions/unknown_validator_spec.rb0000644000004100000410000000052314016744305024165 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::UnknownValidator do describe '#message' do let(:error) do described_class.new('gt_10') end it 'contains the problem in the message' do expect(error.message).to include( 'unknown validator: gt_10' ) end end end grape-1.5.2/spec/grape/exceptions/invalid_versioner_option_spec.rb0000644000004100000410000000055014016744305025533 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::InvalidVersionerOption do describe '#message' do let(:error) do described_class.new('headers') end it 'contains the problem in the message' do expect(error.message).to include( 'Unknown :using for versioner: headers' ) end end end grape-1.5.2/spec/grape/exceptions/invalid_response_spec.rb0000644000004100000410000000044414016744305023767 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::InvalidResponse do describe '#message' do let(:error) { described_class.new } it 'contains the problem in the message' do expect(error.message).to include('Invalid response') end end end grape-1.5.2/spec/grape/exceptions/missing_mime_type_spec.rb0000644000004100000410000000076414016744305024151 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Exceptions::MissingMimeType do describe '#message' do let(:error) do described_class.new('new_json') end it 'contains the problem in the message' do expect(error.message).to include 'missing mime type for new_json' end it 'contains the resolution in the message' do expect(error.message).to include "or add your own with content_type :new_json, 'application/new_json' " end end end grape-1.5.2/spec/grape/validations_spec.rb0000644000004100000410000020055014016744305020557 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations do subject { Class.new(Grape::API) } def app subject end def declared_params subject.namespace_stackable(:declared_params).flatten end describe 'params' do context 'optional' do before do subject.params do optional :a_number, regexp: /^[0-9]+$/ optional :attachment, type: File end subject.get '/optional' do 'optional works!' end end it 'validates when params is present' do 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 get '/optional', a_number: nil, attachment: nil 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(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(declared_params).to eq(%i[field_a field_b]) end it 'works when field_a and field_b are not present' do get '/optional' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with using works') end it 'works when field_a is present' do get '/optional', field_a: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with using works') end it 'works when field_b is present' do get '/optional', field_b: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with using works') end end context 'required' do before do subject.params do requires :key, type: String end subject.get('/required') { 'required works' } subject.put('/required') { { key: params[:key] }.to_json } end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('key is missing') end it "doesn't throw a missing param when param is present" do get '/required', key: 'cool' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'adds to declared parameters' do subject.params do requires :some_param end expect(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 with nested params' do before do subject.params do requires :first_level, type: Hash do optional :second_level, type: Array do requires :value, type: Integer optional :name, type: String optional :third_level, type: Array do requires :value, type: Integer optional :name, type: String optional :fourth_level, type: Array do requires :value, type: Integer optional :name, type: String end end end end end subject.put('/required') { 'required works' } end let(:request_params) do { first_level: { second_level: [ { value: 1, name: 'Lisa' }, { value: 2, name: 'James', third_level: [ { value: 'three', name: 'Sophie' }, { value: 4, name: 'Jenny', fourth_level: [ { name: 'Samuel' }, { value: 6, name: 'Jane' } ] } ] } ] } } end it 'validates correctly in deep nested params' do put '/required', request_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'first_level[second_level][1][third_level][0][value] is invalid, ' \ 'first_level[second_level][1][third_level][1][fourth_level][0][value] is missing' ) 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(declared_params).to eq(%i[required_field optional_field]) end it 'errors when required_field is not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('required_field is missing') end it 'works when required_field is present' do get '/required', required_field: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end end context 'requires :none using Grape::Entity documentation' do def define_requires_none documentation = { required_field: { type: String }, optional_field: { type: String } } subject.params do requires :none, except: :required_field, using: documentation end end before do define_requires_none subject.get '/required' do 'required works' end end it 'adds entity documentation to declared params' do define_requires_none expect(declared_params).to eq(%i[required_field optional_field]) end it 'errors when required_field is not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('required_field is missing') end it 'works when required_field is present' do get '/required', required_field: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end end context 'requires :all or :none but except a non-existent field using Grape::Entity documentation' do context 'requires :all' do def define_requires_all documentation = { required_field: { type: String }, optional_field: { type: String } } subject.params do requires :all, except: :non_existent_field, using: documentation end end it 'adds only the entity documentation to declared params, nothing more' do define_requires_all expect(declared_params).to eq(%i[required_field optional_field]) end end context 'requires :none' do def define_requires_none documentation = { required_field: { type: String }, optional_field: { type: String } } subject.params do requires :none, except: :non_existent_field, using: documentation end end it 'adds only the entity documentation to declared params, nothing more' do expect { define_requires_none }.to raise_error(ArgumentError) end end end context 'required with an Array block' do before do subject.params do requires :items, type: Array do requires :key end end subject.get('/required') { 'required works' } subject.put('/required') { { items: params[:items] }.to_json } end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing') end it 'errors when param is not an Array' do get '/required', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') get '/required', items: { key: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') end it "doesn't throw a missing param when param is present" do get '/required', items: [{ key: 'hello' }, { key: 'world' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it "doesn't throw a missing param when param is present but empty" do put '/required', { items: [] }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq('items' => []) end it 'adds to declared parameters' do subject.params do requires :items, type: Array do requires :key end end expect(declared_params).to eq([items: [:key]]) end end # Ensure there is no leakage between declared Array types and # subsequent Hash types context 'required with an Array and a Hash block' do before do subject.params do requires :cats, type: Array[String], default: [] requires :items, type: Hash do requires :key end end subject.get '/required' do 'required works' end end it 'does not output index [0] for Hash types' do get '/required', cats: ['Garfield'], items: { foo: 'bar' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[key] is missing') end end context 'required with a Hash block' do before do subject.params do requires :items, type: Hash do requires :key end end subject.get '/required' do 'required works' end end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing, items[key] is missing') end it 'errors when nested param not present' do get '/required', items: { foo: 'bar' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[key] is missing') end it 'errors when param is not a Hash' do get '/required', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing') get '/required', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') end it "doesn't throw a missing param when param is present" do get '/required', items: { key: 'hello' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'adds to declared parameters' do subject.params do requires :items, type: Array do requires :key end end expect(declared_params).to eq([items: [:key]]) end end context 'hash with a required param with validation' do before do subject.params do requires :items, type: Hash do requires :key, type: String, values: %w[a b] end end subject.get '/required' do 'required works' end end it 'errors when param is not a Hash' do get '/required', items: 'not a hash' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing, items[key] is invalid') get '/required', items: [{ key: 'hash in array' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] does not have a valid value') end it 'works when all params match' do get '/required', items: { key: 'a' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end end context 'group' do before do subject.params do group :items, type: Array do requires :key end end subject.get '/required' do 'required works' end end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing') end it "doesn't throw a missing param when param is present" do get '/required', items: [key: 'hello'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'adds to declared parameters' do subject.params do group :items, type: Array do requires :key end end expect(declared_params).to eq([items: [:key]]) end end context 'group params with nested params which has a type' do let(:invalid_items) { { items: '' } } before do subject.params do optional :items, type: Array do optional :key1, type: String optional :key2, type: String end end subject.post '/group_with_nested' do 'group with nested works' end end it 'errors when group param is invalid' do post '/group_with_nested', items: invalid_items expect(last_response.status).to eq(400) end end context 'custom validator for a Hash' do module ValuesSpec module DateRangeValidations class DateRangeValidator < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name][:from] <= params[attr_name][:to] raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'") end end end end before do subject.params do optional :date_range, date_range: true, type: Hash do requires :from, type: Integer requires :to, type: Integer end end subject.get('/optional') do 'optional works' end subject.params do requires :date_range, date_range: true, type: Hash do requires :from, type: Integer requires :to, type: Integer end end subject.get('/required') do 'required works' end end context 'which is optional' do it "doesn't throw an error if the validation passes" do get '/optional', date_range: { from: 1, to: 2 } expect(last_response.status).to eq(200) end it 'errors if the validation fails' do get '/optional', date_range: { from: 2, to: 1 } expect(last_response.status).to eq(400) end end context 'which is required' do it "doesn't throw an error if the validation passes" do get '/required', date_range: { from: 1, to: 2 } expect(last_response.status).to eq(200) end it 'errors if the validation fails' do get '/required', date_range: { from: 2, to: 1 } expect(last_response.status).to eq(400) end end end context 'validation within arrays' do before do subject.params do group :children, type: Array do requires :name group :parents, type: Array do requires :name, allow_blank: false end end end subject.get '/within_array' do 'within array works' end end it 'can handle new scopes within child elements' do get '/within_array', children: [ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] }, { name: 'Joe', parents: [{ name: 'Josie' }] } ] expect(last_response.status).to eq(200) expect(last_response.body).to eq('within array works') end it 'errors when a parameter is not present' do get '/within_array', children: [ { name: 'Jim', parents: [{ name: 'Joy' }] }, { name: 'Job', parents: [{}] } ] # NOTE: with body parameters in json or XML or similar this # should actually fail with: children[parents][name] is missing. expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[1][parents] is missing, children[0][parents][1][name] is missing, children[0][parents][1][name] is empty') end it 'errors when a parameter is not present in array within array' do get '/within_array', children: [ { name: 'Jim', parents: [{ name: 'Joy' }] }, { name: 'Job', parents: [{ name: 'Bill' }, { name: '' }] } ] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[1][parents][1][name] is empty') end it 'handle errors for all array elements' do get '/within_array', children: [ { name: 'Jim', parents: [] }, { name: 'Job', parents: [] } ] expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'children[0][parents][0][name] is missing, ' \ 'children[1][parents][0][name] 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[0][name] is missing, ' \ 'children[0][parents] is missing, ' \ 'children[0][parents] is invalid, ' \ 'children[0][parents][0][name] is missing, ' \ 'children[0][parents][0][name] is empty' ) get '/within_array', children: [name: 'Jay'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing, children[0][parents][0][name] is empty') end it 'errors when param is not an Array' do get '/within_array', children: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid') get '/within_array', children: { name: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid') get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents] is invalid') end end context 'with block param' do before do subject.params do requires :planets, type: Array do requires :name end end subject.get '/req' do 'within array works' end subject.put '/req' do '' end subject.params do group :stars, type: Array do requires :name end end subject.get '/grp' do 'within array works' end subject.put '/grp' do '' end subject.params do requires :name optional :moons, type: Array do requires :name end end subject.get '/opt' do 'within array works' end subject.put '/opt' do '' end end it 'requires defaults to Array type' do get '/req', planets: 'Jupiter, Saturn' expect(last_response.status).to eq(400) expect(last_response.body).to eq('planets is invalid') get '/req', planets: { name: 'Jupiter' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('planets is invalid') get '/req', planets: [{ name: 'Venus' }, { name: 'Mars' }] expect(last_response.status).to eq(200) put_with_json '/req', planets: [] expect(last_response.status).to eq(200) end it 'optional defaults to Array type' do get '/opt', name: 'Jupiter', moons: 'Europa, Ganymede' expect(last_response.status).to eq(400) expect(last_response.body).to eq('moons is invalid') get '/opt', name: 'Jupiter', moons: { name: 'Ganymede' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('moons is invalid') get '/opt', name: 'Jupiter', moons: [{ name: 'Io' }, { name: 'Callisto' }] expect(last_response.status).to eq(200) put_with_json '/opt', name: 'Venus' expect(last_response.status).to eq(200) put_with_json '/opt', name: 'Mercury', moons: [] expect(last_response.status).to eq(200) end it 'group defaults to Array type' do get '/grp', stars: 'Sun' expect(last_response.status).to eq(400) expect(last_response.body).to eq('stars is invalid') get '/grp', stars: { name: 'Sun' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('stars is invalid') get '/grp', stars: [{ name: 'Sun' }] expect(last_response.status).to eq(200) put_with_json '/grp', stars: [] expect(last_response.status).to eq(200) end end context 'validation within arrays with JSON' do before do subject.params do group :children, type: Array do requires :name group :parents, type: Array do requires :name end end end subject.put '/within_array' do 'within array works' end end it 'can handle new scopes within child elements' do put_with_json '/within_array', children: [ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] }, { name: 'Joe', parents: [{ name: 'Josie' }] } ] expect(last_response.status).to eq(200) expect(last_response.body).to eq('within array works') end it 'errors when a parameter is not present' do put_with_json '/within_array', children: [ { name: 'Jim', parents: [{}] }, { name: 'Job', parents: [{ name: 'Joy' }] } ] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents][0][name] is missing') end it 'safely handles empty arrays and blank parameters' do put_with_json '/within_array', children: [] expect(last_response.status).to eq(200) put_with_json '/within_array', children: [name: 'Jay'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing') end end context 'optional with an Array block' do before do subject.params do optional :items, type: Array do requires :key end end subject.get '/optional_group' do 'optional group works' end end it "doesn't throw a missing param when the group isn't present" do get '/optional_group' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional group works') end it "doesn't throw a missing param when both group and param are given" do get '/optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional group works') end it 'errors when group is present, but required param is not' do get '/optional_group', items: [{ not_key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][key] is missing') end it "errors when param is present but isn't an Array" do get '/optional_group', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') get '/optional_group', items: { key: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') end it 'adds to declared parameters' do subject.params do optional :items, type: Array do requires :key end end expect(declared_params).to eq([items: [:key]]) end end context 'nested optional Array blocks' do before do subject.params do optional :items, type: Array do requires :key optional(:optional_subitems, type: Array) { requires :value } requires(:required_subitems, type: Array) { requires :value } end end subject.get('/nested_optional_group') { 'nested optional group works' } end it 'does no internal validations if the outer group is blank' do get '/nested_optional_group' expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') end it 'does internal validations if the outer group is present' do get '/nested_optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') end it 'handles deep nesting' do get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][optional_subitems][0][value] is missing') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ value: 'baz' }] }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') end it 'handles validation within arrays' do get '/nested_optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][optional_subitems][0][value] is missing') end it 'adds to declared parameters' do subject.params do optional :items, type: Array do requires :key optional(:optional_subitems, type: Array) { requires :value } requires(:required_subitems, type: Array) { requires :value } end end expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) end context <<~DESC do Issue occurs whenever: * param structure with at least three levels * 1st level item is a required Array that has >1 entry with an optional item present and >1 entry with an optional item missing * 2nd level is an optional Array or Hash * 3rd level is a required item (can be any type) * additional levels do not effect the issue from occuring DESC it "example based off actual real world use case" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Array do requires :batches, type: Array do requires :batch_no, type: String end end end end subject.get '/validate_required_arrays_under_optional_arrays' do 'validate_required_arrays_under_optional_arrays works!' end data = { orders: [ { id: 77, drugs: [{batches: [{batch_no: "A1234567"}]}]}, { id: 70 } ] } get '/validate_required_arrays_under_optional_arrays', data expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!") expect(last_response.status).to eq(200) end it "simplest example using Array -> Array -> Hash -> String" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Array do requires :batch_no, type: String end end end subject.get '/validate_required_arrays_under_optional_arrays' do 'validate_required_arrays_under_optional_arrays works!' end data = { orders: [ { id: 77, drugs: [{batch_no: "A1234567"}]}, { id: 70 } ] } get '/validate_required_arrays_under_optional_arrays', data expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!") expect(last_response.status).to eq(200) end it "simplest example using Array -> Hash -> String" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do requires :batch_no, type: String end end end subject.get '/validate_required_arrays_under_optional_arrays' do 'validate_required_arrays_under_optional_arrays works!' end data = { orders: [ { id: 77, drugs: {batch_no: "A1234567"}}, { id: 70 } ] } get '/validate_required_arrays_under_optional_arrays', data expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!") expect(last_response.status).to eq(200) end it "correctly indexes invalida data" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Array do requires :batch_no, type: String requires :quantity, type: Integer end end end subject.get '/correctly_indexes' do 'correctly_indexes works!' end data = { orders: [ { id: 70 }, { id: 77, drugs: [{batch_no: "A1234567", quantity: 12}, {batch_no: "B222222"}]} ] } get '/correctly_indexes', data expect(last_response.body).to eq("orders[1][drugs][1][quantity] is missing") expect(last_response.status).to eq(400) end context "multiple levels of optional and requires settings" do before do subject.params do requires :top, type: Array do requires :top_id, type: Integer, allow_blank: false optional :middle_1, type: Array do requires :middle_1_id, type: Integer, allow_blank: false optional :middle_2, type: Array do requires :middle_2_id, type: String, allow_blank: false optional :bottom, type: Array do requires :bottom_id, type: Integer, allow_blank: false end end end end end subject.get '/multi_level' do 'multi_level works!' end end it "with valid data" do data = { top: [ { top_id: 1, middle_1: [ {middle_1_id: 11}, {middle_1_id: 12, middle_2: [ {middle_2_id: 121}, {middle_2_id: 122, bottom: [{bottom_id: 1221}]}]}]}, { top_id: 2, middle_1: [ {middle_1_id: 21}, {middle_1_id: 22, middle_2: [ {middle_2_id: 221}]}]}, { top_id: 3, middle_1: [ {middle_1_id: 31}, {middle_1_id: 32}]}, { top_id: 4 } ] } get '/multi_level', data expect(last_response.body).to eq("multi_level works!") expect(last_response.status).to eq(200) end it "with invalid data" do data = { top: [ { top_id: 1, middle_1: [ {middle_1_id: 11}, {middle_1_id: 12, middle_2: [ {middle_2_id: 121}, {middle_2_id: 122, bottom: [{bottom_id: nil}]}]}]}, { top_id: 2, middle_1: [ {middle_1_id: 21}, {middle_1_id: 22, middle_2: [{middle_2_id: nil}]}]}, { top_id: 3, middle_1: [ {middle_1_id: nil}, {middle_1_id: 32}]}, { top_id: nil, missing_top_id: 4 } ] } # debugger get '/multi_level', data expect(last_response.body.split(", ")).to match_array([ "top[3][top_id] is empty", "top[2][middle_1][0][middle_1_id] is empty", "top[1][middle_1][1][middle_2][0][middle_2_id] is empty", "top[0][middle_1][1][middle_2][1][bottom][0][bottom_id] is empty" ]) expect(last_response.status).to eq(400) end end end it "exactly_one_of" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do optional :batch_no, type: String optional :batch_id, type: String exactly_one_of :batch_no, :batch_id end end end subject.get '/exactly_one_of' do 'exactly_one_of works!' end data = { orders: [ { id: 77, drugs: {batch_no: "A1234567"}}, { id: 70 } ] } get '/exactly_one_of', data expect(last_response.body).to eq("exactly_one_of works!") expect(last_response.status).to eq(200) end it "at_least_one_of" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do optional :batch_no, type: String optional :batch_id, type: String at_least_one_of :batch_no, :batch_id end end end subject.get '/at_least_one_of' do 'at_least_one_of works!' end data = { orders: [ { id: 77, drugs: {batch_no: "A1234567"}}, { id: 70 } ] } get '/at_least_one_of', data expect(last_response.body).to eq("at_least_one_of works!") expect(last_response.status).to eq(200) end it "all_or_none_of" do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do optional :batch_no, type: String optional :batch_id, type: String all_or_none_of :batch_no, :batch_id end end end subject.get '/all_or_none_of' do 'all_or_none_of works!' end data = { orders: [ { id: 77, drugs: {batch_no: "A1234567", batch_id: "12"}}, { id: 70 } ] } get '/all_or_none_of', data expect(last_response.body).to eq("all_or_none_of works!") expect(last_response.status).to eq(200) end end context 'multiple validation errors' do before do subject.params do requires :yolo requires :swag end subject.get '/two_required' do 'two required works' end end it 'throws the validation errors' do get '/two_required' expect(last_response.status).to eq(400) expect(last_response.body).to match(/yolo is missing/) expect(last_response.body).to match(/swag is missing/) end end context 'custom validation' do module CustomValidations class Customvalidator < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name] == 'im custom' raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: 'is not custom!') end end end context 'when using optional with a custom validator' do before do subject.params do optional :custom, customvalidator: true end subject.get '/optional_custom' do 'optional with custom works!' end end it 'validates when param is present' do get '/optional_custom', custom: 'im custom' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with custom works!') get '/optional_custom', custom: 'im wrong' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end it "skips validation when parameter isn't present" do get '/optional_custom' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with custom works!') end it 'validates with custom validator when param present and incorrect type' do subject.params do optional :custom, type: String, customvalidator: true end get '/optional_custom', custom: 123 expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end end context 'when using requires with a custom validator' do before do subject.params do requires :custom, customvalidator: true end subject.get '/required_custom' do 'required with custom works!' end end it 'validates when param is present' do get '/required_custom', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') get '/required_custom', custom: 'im custom' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required with custom works!') end it 'validates when param is not present' do get '/required_custom' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is missing, custom is not custom!') end context 'nested namespaces' do before do subject.params do requires :custom, customvalidator: true end subject.namespace 'nested' do get 'one' do 'validation failed' end namespace 'nested' do get 'two' do 'validation failed' end end end subject.namespace 'peer' do get 'one' do 'no validation required' end namespace 'nested' do get 'two' do 'no validation required' end end end subject.namespace 'unrelated' do params do requires :name end get 'one' do 'validation required' end namespace 'double' do get 'two' do 'no validation required' end end end end specify 'the parent namespace uses the validator' do get '/nested/one', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end specify 'the nested namespace inherits the custom validator' do get '/nested/nested/two', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end specify 'peer namespaces does not have the validator' do get '/peer/one', custom: 'im not validated' expect(last_response.status).to eq(200) expect(last_response.body).to eq('no validation required') end specify 'namespaces nested in peers should also not have the validator' do get '/peer/nested/two', custom: 'im not validated' expect(last_response.status).to eq(200) expect(last_response.body).to eq('no validation required') end specify 'when nested, specifying a route should clear out the validations for deeper nested params' do get '/unrelated/one' expect(last_response.status).to eq(400) get '/unrelated/double/two' expect(last_response.status).to eq(200) end end end context 'when using options on param' do module CustomValidations class CustomvalidatorWithOptions < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name] == @option[:text] raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message) end end end before do subject.params do optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' } end subject.get '/optional_custom' do 'optional with custom works!' end end it 'validates param with custom validator with options' do get '/optional_custom', custom: 'im custom with options' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with custom works!') get '/optional_custom', custom: 'im wrong' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom with options!') end end end context 'named' do context 'can be defined' do it 'in helpers' do subject.helpers do params :pagination do end end end it 'in helper module which kind of Grape::DSL::Helpers::BaseHelper' do shared_params = Module.new do extend Grape::DSL::Helpers::BaseHelper params :pagination do end end subject.helpers shared_params end end context 'can be included in usual params' do before do shared_params = Module.new do extend Grape::DSL::Helpers::BaseHelper params :period do optional :start_date optional :end_date end end subject.helpers shared_params subject.helpers do params :pagination do optional :page, type: Integer optional :per_page, type: Integer end end end it 'by #use' do subject.params do use :pagination end expect(declared_params).to eq %i[page per_page] end it 'by #use with multiple params' do subject.params do use :pagination, :period end expect(declared_params).to eq %i[page per_page start_date end_date] end end context 'with block' do before do subject.helpers do params :order do |options| optional :order, type: Symbol, values: %i[asc desc], default: options[:default_order] optional :order_by, type: Symbol, values: options[:order_by], default: options[:default_order_by] end end subject.format :json subject.params do use :order, default_order: :asc, order_by: %i[name created_at], default_order_by: :created_at end subject.get '/order' do { order: params[:order], order_by: params[:order_by] } end end it 'returns defaults' do get '/order' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :asc, order_by: :created_at }.to_json) end it 'overrides default value for order' do get '/order?order=desc' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :desc, order_by: :created_at }.to_json) end it 'overrides default value for order_by' do get '/order?order_by=name' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :asc, order_by: :name }.to_json) end it 'fails with invalid value' do get '/order?order=invalid' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"order does not have a valid value"}') end end end context 'documentation' do it 'can be included with a hash' do documentation = { example: 'Joe' } subject.params do requires 'first_name', documentation: documentation end subject.get '/' do end expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation) end end context 'all or none' do context 'optional params' do before :each do subject.resource :custom_message do params do optional :beer optional :wine optional :juice all_or_none_of :beer, :wine, :juice, message: 'all params are required or none is required' end get '/all_or_none' do 'all_or_none works!' end end end context 'with a custom validation message' do it 'errors when any one is present' do get '/custom_message/all_or_none', beer: 'string' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice all params are required or none is required' end it 'works when all params are present' do get '/custom_message/all_or_none', beer: 'string', wine: 'anotherstring', juice: 'anotheranotherstring' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'all_or_none works!' end it 'works when none are present' do get '/custom_message/all_or_none' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'all_or_none works!' end end end end context 'mutually exclusive' do context 'optional params' do context 'with custom validation message' do it 'errors when two or more are present' do subject.resources :custom_message do params do optional :beer optional :wine optional :juice mutually_exclusive :beer, :wine, :juice, message: 'are mutually exclusive cannot pass both params' end get '/mutually_exclusive' do 'mutually_exclusive works!' end end get '/custom_message/mutually_exclusive', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive cannot pass both params' end end it 'errors when two or more are present' do subject.params do optional :beer optional :wine optional :juice mutually_exclusive :beer, :wine, :juice end subject.get '/mutually_exclusive' do 'mutually_exclusive works!' end get '/mutually_exclusive', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive' end end context 'more than one set of mutually exclusive params' do context 'with a custom validation message' do it 'errors for all sets' do subject.resources :custom_message do params do optional :beer optional :wine mutually_exclusive :beer, :wine, message: 'are mutually exclusive pass only one' optional :nested, type: Hash do optional :scotch optional :aquavit mutually_exclusive :scotch, :aquavit, message: 'are mutually exclusive pass only one' end optional :nested2, type: Array do optional :scotch2 optional :aquavit2 mutually_exclusive :scotch2, :aquavit2, message: 'are mutually exclusive pass only one' end end get '/mutually_exclusive' do 'mutually_exclusive works!' end end get '/custom_message/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'beer, wine are mutually exclusive pass only one, nested[scotch], nested[aquavit] are mutually exclusive pass only one, nested2[1][scotch2], nested2[1][aquavit2] are mutually exclusive pass only one' ) end end it 'errors for all sets' do subject.params do optional :beer optional :wine mutually_exclusive :beer, :wine optional :nested, type: Hash do optional :scotch optional :aquavit mutually_exclusive :scotch, :aquavit end optional :nested2, type: Array do optional :scotch2 optional :aquavit2 mutually_exclusive :scotch2, :aquavit2 end end subject.get '/mutually_exclusive' do 'mutually_exclusive works!' end get '/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive, nested[scotch], nested[aquavit] are mutually exclusive, nested2[1][scotch2], nested2[1][aquavit2] are mutually exclusive' end end context 'in a group' do it 'works when only one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice mutually_exclusive :beer, :wine, :juice end end subject.get '/mutually_exclusive_group' do 'mutually_exclusive_group works!' end get '/mutually_exclusive_group', drink: { beer: 'true' } expect(last_response.status).to eq(200) end it 'errors when more than one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice mutually_exclusive :beer, :wine, :juice end end subject.get '/mutually_exclusive_group' do 'mutually_exclusive_group works!' end get '/mutually_exclusive_group', drink: { beer: 'true', juice: 'true', wine: 'true' } expect(last_response.status).to eq(400) end end context 'mutually exclusive params inside Hash group' do it 'invalidates if request param is invalid type' do subject.params do optional :wine, type: Hash do optional :grape optional :country mutually_exclusive :grape, :country end end subject.post '/mutually_exclusive' do 'mutually_exclusive works!' end post '/mutually_exclusive', wine: '2015 sauvignon' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'wine is invalid' end end end context 'exactly one of' do context 'params' do before :each do subject.resources :custom_message do params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice, message: 'are missing, exactly one parameter is required' end get '/exactly_one_of' do 'exactly_one_of works!' end end subject.params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end subject.get '/exactly_one_of' do 'exactly_one_of works!' end end context 'with a custom validation message' do it 'errors when none are present' do get '/custom_message/exactly_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter is required' end it 'succeeds when one is present' do get '/custom_message/exactly_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'exactly_one_of works!' end it 'errors when two or more are present' do get '/custom_message/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are missing, exactly one parameter is required' end end it 'errors when none are present' do get '/exactly_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter must be provided' end it 'succeeds when one is present' do get '/exactly_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'exactly_one_of works!' end it 'errors when two or more are present' do get '/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive' end end context 'nested params' do before :each do subject.params do requires :nested, type: Hash do optional :beer_nested optional :wine_nested optional :juice_nested exactly_one_of :beer_nested, :wine_nested, :juice_nested end optional :nested2, type: Array do optional :beer_nested2 optional :wine_nested2 optional :juice_nested2 exactly_one_of :beer_nested2, :wine_nested2, :juice_nested2 end end subject.get '/exactly_one_of_nested' do 'exactly_one_of works!' end end it 'errors when none are present' do get '/exactly_one_of_nested' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'nested is missing, nested[beer_nested], nested[wine_nested], 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 'nested2[0][beer_nested2], nested2[0][wine_nested2] are mutually exclusive' end end end context 'at least one of' do context 'params' do before :each do subject.resources :custom_message do params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice, message: 'are missing, please specify at least one param' end get '/at_least_one_of' do 'at_least_one_of works!' end end subject.params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end subject.get '/at_least_one_of' do 'at_least_one_of works!' end end context 'with a custom validation message' do it 'errors when none are present' do get '/custom_message/at_least_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, please specify at least one param' end it 'does not error when one is present' do get '/custom_message/at_least_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end it 'does not error when two are present' do get '/custom_message/at_least_one_of', beer: 'string', wine: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end end it 'errors when none are present' do get '/at_least_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, at least one parameter must be provided' end it 'does not error when one is present' do get '/at_least_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end it 'does not error when two are present' do get '/at_least_one_of', beer: 'string', wine: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end end context 'nested params' do before :each do subject.params do requires :nested, type: Hash do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end optional :nested2, type: Array do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice 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, nested[beer], nested[wine], nested[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_nested', nested: { beer: 'string' }, nested2: [{ 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_nested', nested: { beer: 'string', wine: 'string' }, nested2: [{ beer: 'string', wine: 'string' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end end end context 'in a group' do it 'works when only one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: { beer: 'true' } expect(last_response.status).to eq(200) end it 'errors when no parameter from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: {} expect(last_response.status).to eq(400) end it 'errors when more than one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: { beer: 'true', juice: 'true', wine: 'true' } expect(last_response.status).to eq(400) end it 'does not falsely think the param is there if it is provided outside the block' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: { foo: 'bar' }, beer: 'true' expect(last_response.status).to eq(400) end end end end grape-1.5.2/spec/grape/parser_spec.rb0000644000004100000410000000454414016744305017543 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Parser do subject { described_class } describe '.builtin_parsers' do it 'returns an instance of Hash' do expect(subject.builtin_parsers).to be_an_instance_of(Hash) end it 'includes json and xml parsers by default' do expect(subject.builtin_parsers).to include(json: Grape::Parser::Json, xml: Grape::Parser::Xml) end end describe '.parsers' do it 'returns an instance of Hash' do expect(subject.parsers(**{})).to be_an_instance_of(Hash) end it 'includes built-in parsers' do expect(subject.parsers(**{})).to include(subject.builtin_parsers) end context 'with :parsers option' do let(:parsers) { { customized: Class.new } } it 'includes passed :parsers values' do expect(subject.parsers(parsers: parsers)).to include(parsers) end end context 'with added parser by using `register` keyword' do let(:added_parser) { Class.new } before { subject.register :added, added_parser } it 'includes added parser' do expect(subject.parsers(**{})).to include(added: added_parser) end end end describe '.parser_for' do let(:options) { {} } it 'calls .parsers' do expect(subject).to receive(:parsers).with(any_args).and_return(subject.builtin_parsers) subject.parser_for(:json, **options) end it 'returns parser correctly' do expect(subject.parser_for(:json)).to eq(Grape::Parser::Json) end context 'when parser is available' do before { subject.register :customized_json, Grape::Parser::Json } it 'returns registered parser if available' do expect(subject.parser_for(:customized_json)).to eq(Grape::Parser::Json) end end context 'when parser is an instance of Symbol' do before do allow(subject).to receive(:foo).and_return(:bar) subject.register :foo, :foo end it 'returns an instance of Method' do expect(subject.parser_for(:foo)).to be_an_instance_of(Method) end it 'returns object which can be called' do method = subject.parser_for(:foo) expect(method.call).to eq(:bar) end end context 'when parser does not exist' do it 'returns nil' do expect(subject.parser_for(:undefined)).to be_nil end end end end grape-1.5.2/spec/grape/integration/0000755000004100000410000000000014016744305017224 5ustar www-datawww-datagrape-1.5.2/spec/grape/integration/rack_spec.rb0000644000004100000410000000216714016744305021511 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Rack do it 'correctly populates params from a Tempfile' do input = Tempfile.new 'rubbish' begin app = Class.new(Grape::API) do format :json post do { params_keys: params.keys } end end input.write({ test: '123' * 10_000 }.to_json) input.rewind options = { input: input, method: 'POST', 'CONTENT_TYPE' => 'application/json' } env = Rack::MockRequest.env_for('/', options) expect(JSON.parse(read_chunks(app.call(env)[2]).join)['params_keys']).to match_array('test') ensure input.close input.unlink end end context 'when the app is mounted' do def app @main_app ||= Class.new(Grape::API) do get 'ping' end end let!(:base) do app_to_mount = app Class.new(Grape::API) do namespace 'namespace' do mount app_to_mount end end end it 'finds the app on the namespace' do get '/namespace/ping' expect(last_response.status).to eq 200 end end end grape-1.5.2/spec/grape/integration/rack_sendfile_spec.rb0000644000004100000410000000211414016744305023352 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Rack::Sendfile do subject do content_object = file_object app = Class.new(Grape::API) do use Rack::Sendfile format :json get do if content_object.is_a?(String) sendfile content_object else stream content_object end end end options = { method: 'GET', 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', 'HTTP_X_ACCEL_MAPPING' => '/accel/mapping/=/replaced/' } env = Rack::MockRequest.env_for('/', options) app.call(env) end context 'when calling sendfile' do let(:file_object) do '/accel/mapping/some/path' end it 'contains Sendfile headers' do headers = subject[1] expect(headers).to include('X-Accel-Redirect') end end context 'when streaming non file content' do let(:file_object) do double(:file_object, each: nil) end it 'not contains Sendfile headers' do headers = subject[1] expect(headers).to_not include('X-Accel-Redirect') end end end grape-1.5.2/spec/grape/integration/global_namespace_function_spec.rb0000644000004100000410000000070314016744305025744 0ustar www-datawww-data# frozen_string_literal: true # see https://github.com/ruby-grape/grape/issues/1348 require 'spec_helper' def namespace raise end describe Grape::API do subject do Class.new(Grape::API) do format :json get do { ok: true } end end end def app subject end context 'with a global namespace function' do it 'works' do get '/' expect(last_response.status).to eq 200 end end end grape-1.5.2/spec/grape/validations/0000755000004100000410000000000014016744305017216 5ustar www-datawww-datagrape-1.5.2/spec/grape/validations/single_attribute_iterator_spec.rb0000644000004100000410000000410714016744305026034 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::SingleAttributeIterator do describe '#each' do subject(:iterator) { described_class.new(validator, scope, params) } let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) } let(:validator) { double(attrs: %i[first second]) } context 'when params is a hash' do let(:params) do { first: 'string', second: 'string' } end it 'yields params and every single attribute from the list' do expect { |b| iterator.each(&b) } .to yield_successive_args([params, :first, false, false], [params, :second, false, false]) end end context 'when params is an array' do let(:params) do [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }] end it 'yields every single attribute from the list for each of the array elements' do expect { |b| iterator.each(&b) }.to yield_successive_args( [params[0], :first, false, false], [params[0], :second, false, false], [params[1], :first, false, false], [params[1], :second, false, false] ) end context 'empty values' do let(:params) { [{}, '', 10] } it 'marks params with empty values' do expect { |b| iterator.each(&b) }.to yield_successive_args( [params[0], :first, true, false], [params[0], :second, true, false], [params[1], :first, true, false], [params[1], :second, true, false], [params[2], :first, false, false], [params[2], :second, false, false] ) end end context 'when missing optional value' do let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] } it 'marks params with skipped values' do expect { |b| iterator.each(&b) }.to yield_successive_args( [params[0], :first, false, true], [params[0], :second, false, true], [params[1], :first, false, false], [params[1], :second, false, false], ) end end end end end grape-1.5.2/spec/grape/validations/params_scope_spec.rb0000644000004100000410000010212714016744305023234 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::ParamsScope do subject do Class.new(Grape::API) end def app subject end context 'setting a default' do let(:documentation) { subject.routes.first.params } context 'when the default value is truthy' do before do subject.params do optional :int, type: Integer, default: 42 end subject.get end it 'adds documentation about the default value' do expect(documentation).to have_key('int') expect(documentation['int']).to have_key(:default) expect(documentation['int'][:default]).to eq(42) end end context 'when the default value is false' do before do subject.params do optional :bool, type: Grape::API::Boolean, default: false end subject.get end it 'adds documentation about the default value' do expect(documentation).to have_key('bool') expect(documentation['bool']).to have_key(:default) expect(documentation['bool'][:default]).to eq(false) end end context 'when the default value is nil' do before do subject.params do optional :object, type: Object, default: nil end subject.get end it 'adds documentation about the default value' do expect(documentation).to have_key('object') expect(documentation['object']).to have_key(:default) expect(documentation['object'][:default]).to eq(nil) end end end context 'without a default' do before do subject.params do optional :object, type: Object end subject.get end it 'does not add documentation for the default value' do documentation = subject.routes.first.params expect(documentation).to have_key('object') expect(documentation['object']).not_to have_key(:default) end end context 'setting description' do %i[desc description].each do |description_type| it "allows setting #{description_type}" do subject.params do requires :int, type: Integer, description_type => 'My very nice integer' end subject.get '/single' do 'int works' end get '/single', int: 420 expect(last_response.status).to eq(200) expect(last_response.body).to eq('int works') end end end context 'when using custom types' do module ParamsScopeSpec class CustomType attr_reader :value def self.parse(value) raise if value == 'invalid' new(value) end def initialize(value) @value = value end end end it 'coerces the parameter via the type\'s parse method' do subject.params do requires :foo, type: ParamsScopeSpec::CustomType end subject.get('/types') { params[:foo].value } get '/types', foo: 'valid' expect(last_response.status).to eq(200) expect(last_response.body).to eq('valid') get '/types', foo: 'invalid' expect(last_response.status).to eq(400) expect(last_response.body).to match(/foo is invalid/) end end context 'param renaming' do it do subject.params do requires :foo, as: :bar optional :super, as: :hiper end subject.get('/renaming') { "#{declared(params)['bar']}-#{declared(params)['hiper']}" } get '/renaming', foo: 'any', super: 'any2' expect(last_response.status).to eq(200) expect(last_response.body).to eq('any-any2') end it do subject.params do requires :foo, as: :bar, type: String, coerce_with: ->(c) { c.strip } end subject.get('/renaming-coerced') { "#{params['bar']}-#{params['foo']}" } get '/renaming-coerced', foo: ' there we go ' expect(last_response.status).to eq(200) expect(last_response.body).to eq('there we go-') end it do subject.params do requires :foo, as: :bar, allow_blank: false end subject.get('/renaming-not-blank') {} get '/renaming-not-blank', foo: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('foo is empty') end it do subject.params do requires :foo, as: :bar, allow_blank: false end subject.get('/renaming-not-blank-with-value') {} get '/renaming-not-blank-with-value', foo: 'any' expect(last_response.status).to eq(200) end it do subject.params do requires :foo, as: :baz, type: Hash do requires :bar, as: :qux end end subject.get('/nested-renaming') { declared(params).to_json } get '/nested-renaming', foo: { bar: 'any' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"baz":{"qux":"any"}}') end end context 'array without coerce type explicitly given' do it 'sets the type based on first element' do subject.params do requires :periods, type: Array, values: -> { %w[day month] } end subject.get('/required') { 'required works' } get '/required', periods: %w[day month] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'fails to call API without Array type' do subject.params do requires :periods, type: Array, values: -> { %w[day month] } end subject.get('/required') { 'required works' } get '/required', periods: 'day' expect(last_response.status).to eq(400) expect(last_response.body).to eq('periods is invalid') end it 'raises exception when values are of different type' do expect do subject.params { requires :numbers, type: Array, values: [1, 'definitely not a number', 3] } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises exception when range values have different endpoint types' do expect do subject.params { requires :numbers, type: Array, values: 0.0..10 } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context 'coercing values validation with proc' do it 'allows the proc to pass validation without checking' do subject.params { requires :numbers, type: Integer, values: -> { [0, 1, 2] } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '1' expect(last_response.status).to eq(201) expect(last_response.body).to eq('coercion with proc works') end it 'allows the proc to pass validation without checking in value' do subject.params { requires :numbers, type: Integer, values: { value: -> { [0, 1, 2] } } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '1' expect(last_response.status).to eq(201) expect(last_response.body).to eq('coercion with proc works') end it 'allows the proc to pass validation without checking in except' do subject.params { requires :numbers, type: Integer, values: { except: -> { [0, 1, 2] } } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '10' expect(last_response.status).to eq(201) expect(last_response.body).to eq('coercion with proc works') end end context 'with range values' do context "when left range endpoint isn't #kind_of? the type" do it 'raises exception' do expect do subject.params { requires :latitude, type: Integer, values: -90.0..90 } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context "when right range endpoint isn't #kind_of? the type" do it 'raises exception' do expect do subject.params { requires :latitude, type: Integer, values: -90..90.0 } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context 'when the default is an array' do context 'and is the entire range of allowed values' do it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Array[Integer], values: 0..2, default: 0..2 } end.to_not raise_error end end context 'and is a subset of allowed values' do it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Array[Integer], values: [0, 1, 2], default: [1, 0] } end.to_not raise_error end end end context 'when both range endpoints are #kind_of? the type' do it 'accepts values in the range' do subject.params do requires :letter, type: String, values: 'a'..'z' end subject.get('/letter') { params[:letter] } get '/letter', letter: 'j' expect(last_response.status).to eq(200) expect(last_response.body).to eq('j') end it 'rejects values outside the range' do subject.params do requires :letter, type: String, values: 'a'..'z' end subject.get('/letter') { params[:letter] } get '/letter', letter: 'J' expect(last_response.status).to eq(400) expect(last_response.body).to eq('letter does not have a valid value') end end end context 'parameters in group' do it 'errors when no type is provided' do expect do subject.params do group :a do requires :b end end end.to raise_error Grape::Exceptions::MissingGroupTypeError expect do subject.params do optional :a do requires :b end end end.to raise_error Grape::Exceptions::MissingGroupTypeError end it 'allows Hash as type' do subject.params do group :a, type: Hash do requires :b end end subject.get('/group') { 'group works' } get '/group', a: { b: true } expect(last_response.status).to eq(200) expect(last_response.body).to eq('group works') subject.params do optional :a, type: Hash do requires :b end end get '/optional_type_hash' end it 'allows Array as type' do subject.params do group :a, type: Array do requires :b end end subject.get('/group') { 'group works' } get '/group', a: [{ b: true }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('group works') subject.params do optional :a, type: Array do requires :b end end get '/optional_type_array' end it 'handles missing optional Array type' do subject.params do optional :a, type: Array do requires :b end end subject.get('/test') { declared(params).to_json } get '/test' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"a":[]}') end it 'errors with an unsupported type' do expect do subject.params do group :a, type: Set do requires :b end end end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError expect do subject.params do optional :a, type: Set do requires :b end end end.to raise_error Grape::Exceptions::UnsupportedGroupTypeError end end context 'when validations are dependent on a parameter' do before do subject.params do optional :a given :a do requires :b end end subject.get('/test') { declared(params).to_json } end it 'applies the validations only if the parameter is present' do get '/test' expect(last_response.status).to eq(200) get '/test', a: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('b is missing') get '/test', a: true, b: true expect(last_response.status).to eq(200) end it 'applies the validations of multiple parameters' do subject.params do optional :a, :b given :a, :b do requires :c end end subject.get('/multiple') { declared(params).to_json } get '/multiple' expect(last_response.status).to eq(200) get '/multiple', a: true expect(last_response.status).to eq(200) get '/multiple', b: true expect(last_response.status).to eq(200) get '/multiple', a: true, b: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('c is missing') get '/multiple', a: true, b: true, c: true expect(last_response.status).to eq(200) end it 'applies only the appropriate validation' do subject.params do optional :a optional :b mutually_exclusive :a, :b given :a do requires :c, type: String end given :b do requires :c, type: Integer end end subject.get('/multiple') { declared(params).to_json } get '/multiple' expect(last_response.status).to eq(200) get '/multiple', a: true, c: 'test' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body).symbolize_keys).to eq a: 'true', b: nil, c: 'test' get '/multiple', b: true, c: '3' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body).symbolize_keys).to eq a: nil, b: 'true', c: 3 get '/multiple', a: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('c is missing') get '/multiple', b: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('c is missing') get '/multiple', a: true, b: true, c: 'test' expect(last_response.status).to eq(400) expect(last_response.body).to eq('a, b are mutually exclusive, c is invalid') end it 'raises an error if the dependent parameter was never specified' do expect do subject.params do given :c do end end end.to raise_error(Grape::Exceptions::UnknownParameter) end it 'does not raise an error if the dependent parameter is a Hash' do expect do subject.params do optional :a, type: Hash do requires :b end given :a do requires :c end end end.to_not raise_error end it 'does not raise an error if when using nested given' do expect do subject.params do optional :a, type: Hash do requires :b end given :a do requires :c given :c do requires :d end end end end.to_not raise_error end it 'allows nested dependent parameters' do subject.params do optional :a given a: ->(val) { val == 'a' } do optional :b given b: ->(val) { val == 'b' } do optional :c given c: ->(val) { val == 'c' } do requires :d end end end end subject.get('/') { declared(params).to_json } get '/' expect(last_response.status).to eq 200 get '/', a: 'a', b: 'b', c: 'c' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'd is missing' get '/', a: 'a', b: 'b', c: 'c', d: 'd' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ a: 'a', b: 'b', c: 'c', d: 'd' }.to_json) end it 'allows renaming of dependent parameters' do subject.params do optional :a given :a do requires :b, as: :c end end subject.get('/multiple') { declared(params).to_json } get '/multiple', a: 'a', b: 'b' body = JSON.parse(last_response.body) expect(body.keys).to include('c') expect(body.keys).to_not include('b') end it 'allows renaming of dependent on parameter' do subject.params do optional :a, as: :b given b: ->(val) { val == 'x' } do requires :c end end subject.get('/') { declared(params) } get '/', a: 'x' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'c is missing' get '/', a: 'y' expect(last_response.status).to eq 200 end it 'raises an error if the dependent parameter is not the renamed one' do expect do subject.params do optional :a, as: :b given :a do requires :c end end end.to raise_error(Grape::Exceptions::UnknownParameter) end it 'does not validate nested requires when given is false' do subject.params do requires :a, type: String, allow_blank: false, values: %w[x y z] given a: ->(val) { val == 'x' } do requires :inner1, type: Hash, allow_blank: false do requires :foo, type: Integer, allow_blank: false end end given a: ->(val) { val == 'y' } do requires :inner2, type: Hash, allow_blank: false do requires :bar, type: Integer, allow_blank: false requires :baz, type: Array, allow_blank: false do requires :baz_category, type: String, allow_blank: false end end end given a: ->(val) { val == 'z' } do requires :inner3, type: Array, allow_blank: false do requires :bar, type: Integer, allow_blank: false requires :baz, type: Array, allow_blank: false do requires :baz_category, type: String, allow_blank: false end end end end subject.get('/varying') { declared(params).to_json } get '/varying', a: 'x', inner1: { foo: 1 } expect(last_response.status).to eq(200) get '/varying', a: 'y', inner2: { bar: 2, baz: [{ baz_category: 'barstools' }] } expect(last_response.status).to eq(200) get '/varying', a: 'y', inner2: { bar: 2, baz: [{ unrelated: 'yep' }] } expect(last_response.status).to eq(400) get '/varying', a: 'z', inner3: [{ bar: 3, baz: [{ baz_category: 'barstools' }] }] expect(last_response.status).to eq(200) end it 'detect unmet nested dependency' do subject.params do requires :a, type: String, allow_blank: false, values: %w[x y z] given a: ->(val) { val == 'z' } do requires :inner3, type: Array, allow_blank: false do requires :bar, type: String, allow_blank: false given bar: ->(val) { val == 'b' } do requires :baz, type: Array do optional :baz_category, type: String end end given bar: ->(val) { val == 'c' } do requires :baz, type: Array do requires :baz_category, type: String end end end end end subject.get('/nested-dependency') { declared(params).to_json } get '/nested-dependency', a: 'z', inner3: [{ bar: 'c', baz: [{ unrelated: 'nope' }] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq 'inner3[0][baz][0][baz_category] is missing' end it 'includes the parameter within #declared(params)' do get '/test', a: true, b: true expect(JSON.parse(last_response.body)).to eq('a' => 'true', 'b' => 'true') end it 'returns a sensible error message within a nested context' do subject.params do requires :bar, type: Hash do optional :a given :a do requires :b end end end subject.get('/nested') { 'worked' } get '/nested', bar: { a: true } expect(last_response.status).to eq(400) expect(last_response.body).to eq('bar[b] is missing') end it 'includes the nested parameter within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given :a do requires :b end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: true, b: 'yes' } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'b' => 'yes' }) end it 'includes level 2 nested parameters outside the given within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given :a do requires :c, type: Hash do requires :b end end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: true, c: { b: 'yes' } } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } }) end end context 'default value in given block' do before do subject.params do optional :a, values: %w[a b] given a: ->(val) { val == 'a' } do optional :b, default: 'default' end end subject.get('/') { params.to_json } end context 'when dependency meets' do it 'sets default value for dependent parameter' do get '/', a: 'a' expect(last_response.body).to eq({ a: 'a', b: 'default' }.to_json) end end context 'when dependency does not meet' do it 'does not set default value for dependent parameter' do get '/', a: 'b' expect(last_response.body).to eq({ a: 'b' }.to_json) end end end context 'when validations are dependent on a parameter within an array param' do before do subject.params do requires :foos, type: Array do optional :foo given :foo do requires :bar end end end subject.get('/test') { 'ok' } end it 'should pass none Hash params' do get '/test', foos: [''] expect(last_response.status).to eq(200) expect(last_response.body).to eq('ok') end end context 'when validations are dependent on a parameter within an array param within #declared(params).to_json' do before do subject.params do requires :foos, type: Array do optional :foo_type, :baz_type given :foo_type do requires :bar end end end subject.post('/test') { declared(params).to_json } end it 'applies the constraint within each value' do post '/test', { foos: [{ foo_type: 'a' }, { baz_type: 'c' }] }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) expect(last_response.body).to eq('foos[0][bar] is missing') end end context 'when validations are dependent on a parameter with specific value' do # build test cases from all combinations of declarations and options a_decls = %i[optional requires] a_options = [{}, { values: %w[x y z] }] b_options = [{}, { type: String }, { allow_blank: false }, { type: String, allow_blank: false }] combinations = a_decls.product(a_options, b_options) combinations.each_with_index do |combination, i| a_decl, a_opts, b_opts = combination context "(case #{i})" do before do # puts "a_decl: #{a_decl}, a_opts: #{a_opts}, b_opts: #{b_opts}" subject.params do send a_decl, :a, **a_opts given(a: ->(val) { val == 'x' }) { requires :b, **b_opts } given(a: ->(val) { val == 'y' }) { requires :c, **b_opts } end subject.get('/test') { declared(params).to_json } end if a_decl == :optional it 'skips validation when base param is missing' do get '/test' expect(last_response.status).to eq(200) end end it 'skips validation when base param does not have a specified value' do get '/test', a: 'z' expect(last_response.status).to eq(200) get '/test', a: 'z', b: '' expect(last_response.status).to eq(200) end it 'applies the validation when base param has the specific value' do get '/test', a: 'x' expect(last_response.status).to eq(400) expect(last_response.body).to include('b is missing') get '/test', a: 'x', b: true expect(last_response.status).to eq(200) get '/test', a: 'x', b: true, c: '' expect(last_response.status).to eq(200) end it 'includes the parameter within #declared(params)' do get '/test', a: 'x', b: true expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => 'true', 'c' => nil) end end end end it 'raises an error if the dependent parameter was never specified' do expect do subject.params do given :c do end end end.to raise_error(Grape::Exceptions::UnknownParameter) end it 'returns a sensible error message within a nested context' do subject.params do requires :bar, type: Hash do optional :a given a: ->(val) { val == 'x' } do requires :b end end end subject.get('/nested') { 'worked' } get '/nested', bar: { a: 'x' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('bar[b] is missing') end it 'includes the nested parameter within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given a: ->(val) { val == 'x' } do requires :b end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: 'x', b: 'yes' } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'b' => 'yes' }) end it 'includes level 2 nested parameters outside the given within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given a: ->(val) { val == 'x' } do requires :c, type: Hash do requires :b end end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: 'x', c: { b: 'yes' } } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'c' => { 'b' => 'yes' } }) end it 'includes deeply nested parameters within #declared(params)' do subject.params do requires :arr1, type: Array do requires :hash1, type: Hash do requires :arr2, type: Array do requires :hash2, type: Hash do requires :something, type: String end end end end end subject.get('/nested_deep') { declared(params).to_json } get '/nested_deep', arr1: [{ hash1: { arr2: [{ hash2: { something: 'value' } }] } }] expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq('arr1' => [{ 'hash1' => { 'arr2' => [{ 'hash2' => { 'something' => 'value' } }] } }]) end context 'failing fast' do context 'when fail_fast is not defined' do it 'does not stop validation' do subject.params do requires :one requires :two requires :three end subject.get('/fail-fast') { declared(params).to_json } get '/fail-fast' expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is missing, two is missing, three is missing') end end context 'when fail_fast is defined it stops the validation' do it 'of other params' do subject.params do requires :one, fail_fast: true requires :two end subject.get('/fail-fast') { declared(params).to_json } get '/fail-fast' expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is missing') end it 'for a single param' do subject.params do requires :one, allow_blank: false, regexp: /[0-9]+/, fail_fast: true end subject.get('/fail-fast') { declared(params).to_json } get '/fail-fast', one: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is empty') end end end context 'when params have group attributes' do context 'with validations' do before do subject.params do with(allow_blank: false) do requires :id optional :name optional :address, allow_blank: true end end subject.get('test') end context 'when data is invalid' do before do get 'test', id: '', name: '' end it 'returns a validation error' do expect(last_response.status).to eq(400) end it 'applies group validations for every parameter' do expect(last_response.body).to eq('id is empty, name is empty') end end context 'when parameter has the same validator as a group' do before do get 'test', id: 'id', address: '' end it 'returns a successful response' do expect(last_response.status).to eq(200) end it 'prioritizes parameter validation over group validation' do expect(last_response.body).to_not include('address is empty') end end end context 'with types' do before do subject.params do with(type: Date) do requires :created_at end end subject.get('test') { params[:created_at] } end context 'when invalid date provided' do before do get 'test', created_at: 'not_a_date' end it 'responds with HTTP error' do expect(last_response.status).to eq(400) end it 'returns a validation error' do expect(last_response.body).to eq('created_at is invalid') end end context 'when created_at receives a valid date' do before do get 'test', created_at: '2016-01-01' end it 'returns a successful response' do expect(last_response.status).to eq(200) end it 'returns a date' do expect(last_response.body).to eq('2016-01-01') end end end context 'with several group attributes' do before do subject.params do with(values: [1]) do requires :id, type: Integer end with(allow_blank: false) do optional :address, type: String end requires :name end subject.get('test') end context 'when data is invalid' do before do get 'test', id: 2, address: '' end it 'responds with HTTP error' do expect(last_response.status).to eq(400) end it 'returns a validation error' do expect(last_response.body).to eq('id does not have a valid value, address is empty, name is missing') end end context 'when correct data is provided' do before do get 'test', id: 1, address: 'Some street', name: 'John' end it 'returns a successful response' do expect(last_response.status).to eq(200) end end end context 'with nested groups' do before do subject.params do with(type: Integer) do requires :id with(type: Date) do requires :created_at optional :updated_at end end end subject.get('test') end context 'when data is invalid' do before do get 'test', id: 'wrong', created_at: 'not_a_date', updated_at: '2016-01-01' end it 'responds with HTTP error' do expect(last_response.status).to eq(400) end it 'returns a validation error' do expect(last_response.body).to eq('id is invalid, created_at is invalid') end end context 'when correct data is provided' do before do get 'test', id: 1, created_at: '2016-01-01' end it 'returns a successful response' do expect(last_response.status).to eq(200) end end end end context 'with exactly_one_of validation for optional parameters within an Hash param' do before do subject.params do optional :memo, type: Hash do optional :text, type: String optional :custom_body, type: Hash, coerce_with: JSON exactly_one_of :text, :custom_body end end subject.get('test') end context 'when correct data is provided' do it 'returns a successful response' do get 'test', memo: {} expect(last_response.status).to eq(200) get 'test', memo: { text: 'HOGEHOGE' } expect(last_response.status).to eq(200) get 'test', memo: { custom_body: '{ "xxx": "yyy" }' } expect(last_response.status).to eq(200) end end context 'when invalid data is provided' do it 'returns a failure response' do get 'test', memo: { text: 'HOGEHOGE', custom_body: '{ "xxx": "yyy" }' } expect(last_response.status).to eq(400) get 'test', memo: '{ "custom_body": "HOGE" }' expect(last_response.status).to eq(400) end end end end grape-1.5.2/spec/grape/validations/types/0000755000004100000410000000000014016744305020362 5ustar www-datawww-datagrape-1.5.2/spec/grape/validations/types/primitive_coercer_spec.rb0000644000004100000410000000623014016744305025434 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::Types::PrimitiveCoercer do let(:strict) { false } subject { described_class.new(type, strict) } describe '#call' do context 'BigDecimal' do let(:type) { BigDecimal } it 'coerces to BigDecimal' do expect(subject.call(5)).to eq(BigDecimal(5)) end it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Boolean' do let(:type) { Grape::API::Boolean } [true, 'true', 1].each do |val| it "coerces '#{val}' to true" do expect(subject.call(val)).to eq(true) end end [false, 'false', 0].each do |val| it "coerces '#{val}' to false" do expect(subject.call(val)).to eq(false) end end it 'returns an error when the given value cannot be coerced' do expect(subject.call(123)).to be_instance_of(Grape::Validations::Types::InvalidValue) end it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'DateTime' do let(:type) { DateTime } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Float' do let(:type) { Float } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Integer' do let(:type) { Integer } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Numeric' do let(:type) { Numeric } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Time' do let(:type) { Time } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'String' do let(:type) { String } it 'coerces to String' do expect(subject.call(10)).to eq('10') end it 'does not coerce an empty string to nil' do expect(subject.call('')).to eq('') end end context 'Symbol' do let(:type) { Symbol } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'the strict mode' do let(:strict) { true } context 'Boolean' do let(:type) { Grape::API::Boolean } it 'returns an error when the given value is not Boolean' do expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue) end it 'returns a value as it is when the given value is Boolean' do expect(subject.call(true)).to eq(true) end end context 'BigDecimal' do let(:type) { BigDecimal } it 'returns an error when the given value is not BigDecimal' do expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue) end it 'returns a value as it is when the given value is BigDecimal' do expect(subject.call(BigDecimal(0))).to eq(BigDecimal(0)) end end end end end grape-1.5.2/spec/grape/validations/types/set_coercer_spec.rb0000644000004100000410000000157614016744305024227 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::Types::SetCoercer do subject { described_class.new(type) } describe '#call' do context 'a set of primitives' do let(:type) { Set[String] } it 'coerces elements to the set' do expect(subject.call([10, 20])).to eq(Set['10', '20']) end end context 'a set of sets' do let(:type) { Set[Set[Integer]] } it 'coerces elements in the nested set' do expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]]) expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]]) end end context 'a set of sets of arrays' do let(:type) { Set[Set[Array[Integer]]] } it 'coerces elements in the nested set' do expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]]) end end end end grape-1.5.2/spec/grape/validations/types/array_coercer_spec.rb0000644000004100000410000000164214016744305024544 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::Types::ArrayCoercer do subject { described_class.new(type) } describe '#call' do context 'an array of primitives' do let(:type) { Array[String] } it 'coerces elements in the array' do expect(subject.call([10, 20])).to eq(%w[10 20]) end end context 'an array of arrays' do let(:type) { Array[Array[Integer]] } it 'coerces elements in the nested array' do expect(subject.call([%w[10 20]])).to eq([[10, 20]]) expect(subject.call([['10'], ['20']])).to eq([[10], [20]]) end end context 'an array of sets' do let(:type) { Array[Set[Integer]] } it 'coerces elements in the nested set' do expect(subject.call([%w[10 20]])).to eq([Set[10, 20]]) expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]]) end end end end grape-1.5.2/spec/grape/validations/validators/0000755000004100000410000000000014016744305021366 5ustar www-datawww-datagrape-1.5.2/spec/grape/validations/validators/presence_spec.rb0000644000004100000410000002177614016744305024546 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::PresenceValidator do subject do Class.new(Grape::API) do format :json end end def app subject end context 'without validation' do before do subject.resource :bacons do get do 'All the bacon' end end end it 'does not validate for any params' do get '/bacons' expect(last_response.status).to eq(200) expect(last_response.body).to eq('All the bacon'.to_json) end end context 'with a custom validation message' do before do subject.resource :requires do params do requires :email, type: String, allow_blank: { value: false, message: 'has no value' }, regexp: { value: /^\S+$/, message: 'format is invalid' }, message: 'is required' end get do 'Hello' end end end it 'requires when missing' do get '/requires' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is required, email has no value"}') end it 'requires when empty' do get '/requires', email: '' expect(last_response.body).to eq('{"error":"email has no value, email format is invalid"}') end it 'valid when set' do get '/requires', email: 'bob@example.com' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello'.to_json) end end context 'with a required regexp parameter supplied in the POST body' do before do subject.format :json subject.params do requires :id, regexp: /^[0-9]+$/ end subject.post do { ret: params[:id] } end end it 'validates id' do post '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"id is missing"}') io = StringIO.new('{"id" : "a56b"}') post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length expect(last_response.body).to eq('{"error":"id is invalid"}') expect(last_response.status).to eq(400) io = StringIO.new('{"id" : 56}') post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length expect(last_response.body).to eq('{"ret":56}') expect(last_response.status).to eq(201) end end context 'with a required non-empty string' do before do subject.params do requires :email, type: String, allow_blank: false, regexp: /^\S+$/ end subject.get do 'Hello' end end it 'requires when missing' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is missing, email is empty"}') end it 'requires when empty' do get '/', email: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is empty, email is invalid"}') end it 'valid when set' do get '/', email: 'bob@example.com' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello'.to_json) end end context 'with multiple parameters per requires' do before do subject.params do requires :one, :two end subject.get '/single-requires' do 'Hello' end subject.params do requires :one requires :two end subject.get '/multiple-requires' do 'Hello' end end it 'validates for all defined params' do get '/single-requires' expect(last_response.status).to eq(400) single_requires_error = last_response.body get '/multiple-requires' expect(last_response.status).to eq(400) expect(last_response.body).to eq(single_requires_error) end end context 'with required parameters and no type' do before do subject.params do requires :name, :company end subject.get do 'Hello' end end it 'validates name, company' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is missing, company is missing"}') get '/', name: 'Bob' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"company is missing"}') get '/', name: 'Bob', company: 'TestCorp' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello'.to_json) end end context 'with nested parameters' do before do subject.params do requires :user, type: Hash do requires :first_name requires :last_name end end subject.get '/nested' do 'Nested' end end it 'validates nested parameters' do get '/nested' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}') get '/nested', user: { first_name: 'Billy' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[last_name] is missing"}') get '/nested', user: { first_name: 'Billy', last_name: 'Bob' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('Nested'.to_json) end end context 'with triply nested required parameters' do before do subject.params do requires :admin, type: Hash do requires :admin_name requires :super, type: Hash do requires :user, type: Hash do requires :first_name requires :last_name end end end end subject.get '/nested_triple' do 'Nested triple' end end it 'validates triple nested parameters' do get '/nested_triple' expect(last_response.status).to eq(400) expect(last_response.body).to include '{"error":"admin is missing' get '/nested_triple', user: { first_name: 'Billy' } expect(last_response.status).to eq(400) expect(last_response.body).to include '{"error":"admin is missing' get '/nested_triple', admin: { super: { first_name: 'Billy' } } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}') get '/nested_triple', super: { user: { first_name: 'Billy', last_name: 'Bob' } } expect(last_response.status).to eq(400) expect(last_response.body).to include '{"error":"admin is missing' get '/nested_triple', admin: { super: { user: { first_name: 'Billy' } } } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user][last_name] is missing"}') get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: 'Billy' } } } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"admin[super][user][last_name] is missing"}') get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: 'Billy', last_name: 'Bob' } } } expect(last_response.status).to eq(200) expect(last_response.body).to eq('Nested triple'.to_json) end end context 'with reused parameter documentation once required and once optional' do before do docs = { name: { type: String, desc: 'some name' } } subject.params do requires :all, using: docs end subject.get '/required' do 'Hello required' end subject.params do optional :all, using: docs end subject.get '/optional' do 'Hello optional' end end it 'works with required' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is missing"}') get '/required', name: 'Bob' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello required'.to_json) end it 'works with optional' do get '/optional' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello optional'.to_json) get '/optional', name: 'Bob' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello optional'.to_json) end end context 'with a custom type' do it 'does not validate their type when it is missing' do class CustomType def self.parse(value) return if value.blank? new end end subject.params do requires :custom, type: CustomType end subject.get '/custom' do 'custom' end get 'custom' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"custom is missing"}') get 'custom', custom: 'filled' expect(last_response.status).to eq(200) end end end grape-1.5.2/spec/grape/validations/validators/same_as_spec.rb0000644000004100000410000000324614016744305024342 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::SameAsValidator do module ValidationsSpec module SameAsValidatorSpec class API < Grape::API params do requires :password requires :password_confirmation, same_as: :password end post do end params do requires :password requires :password_confirmation, same_as: { value: :password, message: 'not match' } end post '/custom-message' do end end end end def app ValidationsSpec::SameAsValidatorSpec::API end describe '/' do context 'is the same' do it do post '/', password: '987654', password_confirmation: '987654' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'is not the same' do it do post '/', password: '123456', password_confirmation: 'whatever' expect(last_response.status).to eq(400) expect(last_response.body).to eq('password_confirmation is not the same as password') end end end describe '/custom-message' do context 'is the same' do it do post '/custom-message', password: '987654', password_confirmation: '987654' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'is not the same' do it do post '/custom-message', password: '123456', password_confirmation: 'whatever' expect(last_response.status).to eq(400) expect(last_response.body).to eq('password_confirmation not match') end end end end grape-1.5.2/spec/grape/validations/validators/mutual_exclusion_spec.rb0000644000004100000410000001513014016744305026325 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::MutualExclusionValidator do describe '#validate!' do subject(:validate) { post path, params } module ValidationsSpec module MutualExclusionValidatorSpec class API < Grape::API rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end post do end params do optional :beer optional :wine optional :grapefruit optional :other mutually_exclusive :beer, :wine, :grapefruit end post 'mixed-params' do end params do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine' end post '/custom-message' do end params do requires :item, type: Hash do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end end post '/nested-hash' do end params do optional :item, type: Hash do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end end post '/nested-optional-hash' do end params do requires :items, type: Array do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end end post '/nested-array' do end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, :grapefruit, type: Boolean mutually_exclusive :beer, :wine, :grapefruit end end end post '/deeply-nested-array' do end end end end def app ValidationsSpec::MutualExclusionValidatorSpec::API end context 'when all mutually exclusive params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end context 'mixed with other params' do let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end end context 'when a subset of mutually exclusive params are present' do let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,grapefruit' => ['are mutually exclusive'] ) end end context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true, wine: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['you should not mix beer and wine'] ) end end context 'when no mutually exclusive params are present' do let(:path) { '/' } let(:params) { { beer: true, somethingelse: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when mutually exclusive params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end context 'when mutually exclusive params are nested inside optional hash' do let(:path) { '/nested-optional-hash' } context 'when params are passed' do let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end context 'when params are empty' do let(:params) { {} } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end context 'when mutually exclusive params are nested inside array' do let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][beer],items[0][wine]' => ['are mutually exclusive'], 'items[1][wine],items[1][grapefruit]' => ['are mutually exclusive'] ) end end context 'when mutually exclusive params are deeply nested' do let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => ['are mutually exclusive'] ) end end end end grape-1.5.2/spec/grape/validations/validators/exactly_one_of_spec.rb0000644000004100000410000001561114016744305025727 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::ExactlyOneOfValidator do describe '#validate!' do subject(:validate) { post path, params } module ValidationsSpec module ExactlyOneOfValidatorSpec class API < Grape::API rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end post do end params do optional :beer optional :wine optional :grapefruit optional :other exactly_one_of :beer, :wine, :grapefruit end post 'mixed-params' do end params do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one' end post '/custom-message' do end params do requires :item, type: Hash do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end end post '/nested-hash' do end params do optional :item, type: Hash do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end end post '/nested-optional-hash' do end params do requires :items, type: Array do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end end post '/nested-array' do end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, :grapefruit, type: Boolean exactly_one_of :beer, :wine, :grapefruit end end end post '/deeply-nested-array' do end end end end def app ValidationsSpec::ExactlyOneOfValidatorSpec::API end context 'when all params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end context 'mixed with other params' do let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end end context 'when a subset of params are present' do let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,grapefruit' => ['are mutually exclusive'] ) end end context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true, wine: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['you should choose one'] ) end end context 'when exacly one param is present' do let(:path) { '/' } let(:params) { { beer: true, somethingelse: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of the params are present' do let(:path) { '/' } let(:params) { { somethingelse: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided'] ) end end context 'when params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end context 'when params are nested inside optional hash' do let(:path) { '/nested-optional-hash' } context 'when params are passed' do let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end context 'when params are empty' do let(:params) { { other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end context 'when params are nested inside array' do let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][beer],items[0][wine]' => [ 'are mutually exclusive' ], 'items[1][wine],items[1][grapefruit]' => [ 'are mutually exclusive' ] ) end end context 'when params are deeply nested' do let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => [ 'are mutually exclusive' ] ) end end end end grape-1.5.2/spec/grape/validations/validators/at_least_one_of_spec.rb0000644000004100000410000001436414016744305026056 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::AtLeastOneOfValidator do describe '#validate!' do subject(:validate) { post path, params } module ValidationsSpec module AtLeastOneOfValidatorSpec class API < Grape::API rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit end post do end params do optional :beer, :wine, :grapefruit, :other at_least_one_of :beer, :wine, :grapefruit end post 'mixed-params' do end params do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' end post '/custom-message' do end params do requires :item, type: Hash do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'fail' end end post '/nested-hash' do end params do requires :items, type: Array do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'fail' end end post '/nested-array' do end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'fail' end end end post '/deeply-nested-array' do end end end end def app ValidationsSpec::AtLeastOneOfValidatorSpec::API end context 'when all restricted params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end context 'mixed with other params' do let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end context 'when a subset of restricted params are present' do let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of the restricted params is selected' do let(:path) { '/' } let(:params) { { other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided'] ) end context 'when custom message is specified' do let(:path) { '/custom-message' } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['you should choose something'] ) end end end context 'when exactly one of the restricted params is selected' do let(:path) { '/' } let(:params) { { beer: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when restricted params are nested inside hash' do let(:path) { '/nested-hash' } context 'when at least one of them is present' do let(:params) { { item: { beer: true, wine: true } } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of them are present' do let(:params) { { item: { other: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine],item[grapefruit]' => ['fail'] ) end end end context 'when restricted params are nested inside array' do let(:path) { '/nested-array' } context 'when at least one of them is present' do let(:params) { { items: [{ beer: true, wine: true }, { grapefruit: true }] } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of them are present' do let(:params) { { items: [{ beer: true, other: true }, { other: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[1][beer],items[1][wine],items[1][grapefruit]' => ['fail'] ) end end end context 'when restricted params are deeply nested' do let(:path) { '/deeply-nested-array' } context 'when at least one of them is present' do let(:params) { { items: [{ nested_items: [{ wine: true }] }] } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of them are present' do let(:params) { { items: [{ nested_items: [{ other: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => ['fail'] ) end end end end end grape-1.5.2/spec/grape/validations/validators/coerce_spec.rb0000644000004100000410000011144414016744305024172 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::CoerceValidator do subject do Class.new(Grape::API) end def app subject end describe 'coerce' do class SecureURIOnly def self.parse(value) URI.parse(value) end def self.parsed?(value) value.is_a? URI::HTTPS end end context 'i18n' do after :each do I18n.available_locales = %i[en] I18n.locale = :en I18n.default_locale = :en end it 'i18n error on malformed input' do I18n.available_locales = %i[en zh-CN] 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.available_locales = %i[en pt-BR] I18n.locale = 'pt-BR'.to_sym subject.params do requires :age, type: Integer end subject.get '/single' do 'int works' end get '/single', age: '43a' expect(last_response.status).to eq(400) expect(last_response.body).to eq('age is invalid') end end context 'with a custom validation message' do it 'errors on malformed input' do subject.params do requires :int, type: { value: Integer, message: 'type cast is invalid' } end subject.get '/single' do 'int works' end get '/single', int: '43a' expect(last_response.status).to eq(400) expect(last_response.body).to eq('int type cast is invalid') get '/single', int: '43' expect(last_response.status).to eq(200) expect(last_response.body).to eq('int works') end context 'on custom coercion rules' do before do subject.params do requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val| if val == 'yup' true elsif val == 'false' 0 else val end end) end subject.get '/' do params[:a].class.to_s end end it 'respects :coerce_with' do get '/', a: 'yup' expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') end it 'still validates type' do get '/', a: 'false' expect(last_response.status).to eq(400) expect(last_response.body).to eq('a type cast is invalid') end it 'performs no additional coercion' do get '/', a: 'true' expect(last_response.status).to eq(200) expect(last_response.body).to eq('String') end end end it 'error on malformed input' do subject.params do requires :int, type: Integer end subject.get '/single' do 'int works' end get '/single', int: '43a' expect(last_response.status).to eq(400) expect(last_response.body).to eq('int is invalid') get '/single', int: '43' expect(last_response.status).to eq(200) expect(last_response.body).to eq('int works') end it 'error on malformed input (Array)' do subject.params do requires :ids, type: Array[Integer] end subject.get '/array' do 'array int works' end get 'array', ids: %w[1 2 az] expect(last_response.status).to eq(400) expect(last_response.body).to eq('ids is invalid') get 'array', ids: %w[1 2 890] expect(last_response.status).to eq(200) expect(last_response.body).to eq('array int works') end context 'coerces' do context 'json' do let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } } it 'BigDecimal' do subject.params do requires :bigdecimal, type: BigDecimal end subject.post '/bigdecimal' do "#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}" end post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers expect(last_response.status).to eq(201) expect(last_response.body).to eq('BigDecimal 45.1') end it 'Boolean' do subject.params do requires :boolean, type: Boolean end subject.post '/boolean' do params[:boolean] end post '/boolean', { boolean: 'true' }.to_json, headers expect(last_response.status).to eq(201) expect(last_response.body).to eq('true') end end it 'BigDecimal' do subject.params do requires :bigdecimal, coerce: BigDecimal end subject.get '/bigdecimal' do params[:bigdecimal].class end get '/bigdecimal', bigdecimal: '45' expect(last_response.status).to eq(200) expect(last_response.body).to eq('BigDecimal') end it 'Integer' do subject.params do requires :int, coerce: Integer end subject.get '/int' do params[:int].class end get '/int', int: '45' expect(last_response.status).to eq(200) expect(last_response.body).to eq(integer_class_name) end it 'String' do subject.params do requires :string, coerce: String end subject.get '/string' do params[:string].class end get '/string', string: 45 expect(last_response.status).to eq(200) expect(last_response.body).to eq('String') get '/string', string: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end context 'a custom type' do it 'coerces the given value' do subject.params do requires :uri, coerce: SecureURIOnly end subject.get '/secure_uri' do params[:uri].class end get 'secure_uri', uri: 'https://www.example.com' expect(last_response.status).to eq(200) expect(last_response.body).to eq('URI::HTTPS') get 'secure_uri', uri: 'http://www.example.com' expect(last_response.status).to eq(400) expect(last_response.body).to eq('uri is invalid') end context 'returning the InvalidValue instance when invalid' do let(:custom_type) do Class.new do def self.parse(_val) Grape::Types::InvalidValue.new('must be unique') end end end it 'uses a custom message added to the invalid value' do type = custom_type subject.params do requires :name, type: type end subject.get '/whatever' do params[:name].class end get 'whatever', name: 'Bob' expect(last_response.status).to eq(400) expect(last_response.body).to eq('name must be unique') end end end context 'Array' do it 'Array of Integers' do subject.params do requires :arry, coerce: Array[Integer] end subject.get '/array' do params[:arry][0].class end get '/array', arry: %w[1 2 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq(integer_class_name) end it 'Array of Bools' do subject.params do requires :arry, coerce: Array[Grape::API::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 type implementing parse' do subject.params do requires :uri, type: Array[URI] end subject.get '/uri_array' do params[:uri][0].class end get 'uri_array', uri: ['http://www.google.com'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('URI::HTTP') end it 'Set of type implementing parse' do subject.params do requires :uri, type: Set[URI] end subject.get '/uri_array' do "#{params[:uri].class},#{params[:uri].first.class},#{params[:uri].size}" end get 'uri_array', uri: Array.new(2) { 'http://www.example.com' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('Set,URI::HTTP,1') end it 'Array of a custom type' do subject.params do requires :uri, type: Array[SecureURIOnly] end subject.get '/secure_uris' do params[:uri].first.class end get 'secure_uris', uri: ['https://www.example.com'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('URI::HTTPS') get 'secure_uris', uri: ['https://www.example.com', 'http://www.example.com'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('uri is invalid') end end context 'Set' do it 'Set of Integers' do subject.params do requires :set, coerce: Set[Integer] end subject.get '/set' do params[:set].first.class end get '/set', set: Set.new([1, 2, 3, 4]).to_a expect(last_response.status).to eq(200) expect(last_response.body).to eq(integer_class_name) end it 'Set of Bools' do subject.params do requires :set, coerce: Set[Grape::API::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 'Boolean' do subject.params do requires :boolean, type: Boolean end subject.get '/boolean' do params[:boolean].class end get '/boolean', boolean: 1 expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') end context 'File' do let(:file) { Rack::Test::UploadedFile.new(__FILE__) } let(:filename) { File.basename(__FILE__).to_s } it 'Rack::Multipart::UploadedFile' do subject.params do requires :file, type: Rack::Multipart::UploadedFile end subject.post '/upload' do params[:file][:filename] end post '/upload', file: file expect(last_response.status).to eq(201) expect(last_response.body).to eq(filename) post '/upload', file: 'not a file' expect(last_response.status).to eq(400) expect(last_response.body).to eq('file is invalid') end it 'File' do subject.params do requires :file, coerce: File end subject.post '/upload' do params[:file][:filename] end post '/upload', file: file expect(last_response.status).to eq(201) expect(last_response.body).to eq(filename) post '/upload', file: 'not a file' expect(last_response.status).to eq(400) expect(last_response.body).to eq('file is invalid') post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('file is invalid') end it 'collection' do subject.params do requires :files, type: Array[File] end subject.post '/upload' do params[:files].first[:filename] end post '/upload', files: [file] expect(last_response.status).to eq(201) expect(last_response.body).to eq(filename) end end it 'Nests integers' do subject.params do requires :integers, type: Hash do requires :int, coerce: Integer end end subject.get '/int' do params[:integers][:int].class end get '/int', integers: { int: '45' } expect(last_response.status).to eq(200) expect(last_response.body).to eq(integer_class_name) end context 'nil values' do context 'primitive types' do Grape::Validations::Types::PRIMITIVES.each do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end end context 'structures types' do Grape::Validations::Types::STRUCTURES.each do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end end context 'special types' do Grape::Validations::Types::SPECIAL.each_key do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end context 'variant-member-type collections' do [ Array[Integer, String], [Integer, String, Array[Integer, String]] ].each do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end end end end context 'empty string' do context 'primitive types' do (Grape::Validations::Types::PRIMITIVES - [String]).each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end it 'is not coerced to nil for type String' do subject.params do requires :param, type: String end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response.status).to eq(200) expect(last_response.body).to eq('String') end end context 'structures types' do (Grape::Validations::Types::STRUCTURES - [Hash]).each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end end context 'special types' do (Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end context 'variant-member-type collections' do [ Array[Integer, String], [Integer, String, Array[Integer, String]] ].each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end end end end end end context 'using coerce_with' do it 'parses parameters with Array type' do subject.params do requires :values, type: Array, coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } end subject.get '/ints' do params[:values] end get '/ints', values: '1 2 3 4' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4]) get '/ints', values: 'a b c d' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0]) end it 'parses parameters with Array[String] type' do subject.params do requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) } end subject.get '/strings' do params[:values] end get '/strings', values: '1 2 3 4' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4]) get '/strings', values: 'a b c d' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq(%w[a b c d]) end it 'parses parameters with Array[Array[String]] type and coerce_with' do subject.params do requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(/,/).map(&:strip)] : val } end subject.post '/coerce_nested_strings' do params[:values] end post '/coerce_nested_strings', ::Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(JSON.parse(last_response.body)).to eq([%w[a b c d]]) post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]]) post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(JSON.parse(last_response.body)).to eq([[]]) post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) end it 'parses parameters with Array[Integer] type' do subject.params do requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } end subject.get '/ints' do params[:values] end get '/ints', values: '1 2 3 4' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4]) get '/ints', values: 'a b c d' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0]) end it 'parses parameters even if type is valid' do subject.params do requires :values, type: Array, coerce_with: ->(array) { array.map { |val| val.to_i + 1 } } end subject.get '/ints' do params[:values] end get '/ints', values: [1, 2, 3, 4] expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq([2, 3, 4, 5]) get '/ints', values: %w[a b c d] expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1]) end it 'uses parse where available' do subject.params do requires :ints, type: Array, coerce_with: JSON do requires :i, type: Integer requires :j end end subject.get '/ints' do ints = params[:ints].first 'coercion works' if ints[:i] == 1 && ints[:j] == '2' end get '/ints', ints: [{ i: 1, j: '2' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('ints is invalid') get '/ints', ints: '{"i":1,"j":"2"}' expect(last_response.status).to eq(400) expect(last_response.body).to eq('ints is invalid') get '/ints', ints: '[{"i":"1","j":"2"}]' expect(last_response.status).to eq(200) expect(last_response.body).to eq('coercion works') end it 'accepts any callable' do subject.params do requires :ints, type: Hash, coerce_with: JSON.method(:parse) do requires :int, type: Integer, coerce_with: ->(val) { val == 'three' ? 3 : val } end end subject.get '/ints' do params[:ints][:int] end get '/ints', ints: '{"int":"3"}' expect(last_response.status).to eq(400) expect(last_response.body).to eq('ints[int] is invalid') get '/ints', ints: '{"int":"three"}' expect(last_response.status).to eq(200) expect(last_response.body).to eq('3') get '/ints', ints: '{"int":3}' expect(last_response.status).to eq(200) expect(last_response.body).to eq('3') end context 'Integer type and coerce_with potentially returning nil' do before do subject.params do requires :int, type: Integer, coerce_with: (lambda do |val| if val == '0' nil elsif val.match?(/^-?\d+$/) val.to_i else val end end) end subject.get '/' do params[:int].class.to_s end end it 'accepts value that coerces to nil' do get '/', int: '0' expect(last_response.status).to eq(200) expect(last_response.body).to eq('NilClass') end it 'coerces to Integer' do get '/', int: '1' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Integer') end it 'returns invalid value if coercion returns a wrong type' do get '/', int: 'lol' expect(last_response.status).to eq(400) expect(last_response.body).to eq('int is invalid') end end it 'must be supplied with :type or :coerce' do expect do subject.params do requires :ints, coerce_with: JSON end end.to raise_error(ArgumentError) end end context 'first-class JSON' do it 'parses objects, hashes, and arrays' do subject.params do requires :splines, type: JSON do requires :x, type: Integer, values: [1, 2, 3] optional :ints, type: Array[Integer] optional :obj, type: Hash do optional :y end end end subject.get '/' do if params[:splines].is_a? Hash params[:splines][:obj][:y] elsif params[:splines].any? { |s| s.key? :obj } 'arrays work' end end get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}' expect(last_response.status).to eq(200) expect(last_response.body).to eq('woof') get '/', splines: { x: 1, ints: [1, 2, 3], obj: { y: 'woof' } } expect(last_response.status).to eq(200) expect(last_response.body).to eq('woof') get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]' expect(last_response.status).to eq(200) expect(last_response.body).to eq('arrays work') get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('arrays work') get '/', splines: '{"x":4,"ints":[2]}' expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: { x: 4, ints: [2] } expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]' expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') end it 'works when declared optional' do subject.params do optional :splines, type: JSON do requires :x, type: Integer, values: [1, 2, 3] optional :ints, type: Array[Integer] optional :obj, type: Hash do optional :y end end end subject.get '/' do if params[:splines].is_a? Hash params[:splines][:obj][:y] elsif params[:splines].any? { |s| s.key? :obj } 'arrays work' end end get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}' expect(last_response.status).to eq(200) expect(last_response.body).to eq('woof') get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]' expect(last_response.status).to eq(200) expect(last_response.body).to eq('arrays work') get '/', splines: '{"x":4,"ints":[2]}' expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]' expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') end it 'accepts Array[JSON] shorthand' do subject.params do requires :splines, type: Array[JSON] do requires :x, type: Integer, values: [1, 2, 3] requires :y end end subject.get '/' do params[:splines].first[:y].class.to_s spline = params[:splines].first "#{spline[:x].class}.#{spline[:y].class}" end get '/', splines: '{"x":"1","y":"woof"}' expect(last_response.status).to eq(200) expect(last_response.body).to eq("#{integer_class_name}.String") get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]' expect(last_response.status).to eq(200) expect(last_response.body).to eq("#{integer_class_name}.#{integer_class_name}") get '/', splines: '{"x":"4","y":"woof"}' expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":"4","y":"woof"}]' expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') end it "doesn't make sense using coerce_with" do expect do subject.params do requires :bad, type: JSON, coerce_with: JSON do requires :x end end end.to raise_error(ArgumentError) expect do subject.params do requires :bad, type: Array[JSON], coerce_with: JSON do requires :x end end end.to raise_error(ArgumentError) end end context 'multiple types' do Boolean = Grape::API::Boolean it 'coerces to first possible type' do subject.params do requires :a, types: [Boolean, Integer, String] end subject.get '/' do params[:a].class.to_s end get '/', a: 'true' expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') get '/', a: '5' expect(last_response.status).to eq(200) expect(last_response.body).to eq(integer_class_name) get '/', a: 'anything else' expect(last_response.status).to eq(200) expect(last_response.body).to eq('String') end it 'fails when no coercion is possible' do subject.params do requires :a, types: [Boolean, Integer] end subject.get '/' do params[:a].class.to_s end get '/', a: true expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') get '/', a: 'not good' expect(last_response.status).to eq(400) expect(last_response.body).to eq('a is invalid') end context 'for primitive collections' do before do subject.params do optional :a, types: [String, Array[String]] optional :b, types: [Array[Integer], Array[String]] optional :c, type: Array[Integer, String] optional :d, types: [Integer, String, Set[Integer, String]] end subject.get '/' do ( params[:a] || params[:b] || params[:c] || params[:d] ).inspect end end it 'allows singular form declaration' do get '/', a: 'one way' expect(last_response.status).to eq(200) expect(last_response.body).to eq('"one way"') get '/', a: %w[the other] expect(last_response.status).to eq(200) expect(last_response.body).to eq('["the", "other"]') get '/', a: { a: 1, b: 2 } expect(last_response.status).to eq(400) expect(last_response.body).to eq('a is invalid') get '/', a: [1, 2, 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq('["1", "2", "3"]') end it 'allows multiple collection types' do get '/', b: [1, 2, 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq('[1, 2, 3]') get '/', b: %w[1 2 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq('[1, 2, 3]') get '/', b: [1, true, 'three'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('["1", "true", "three"]') end it 'allows collections with multiple types' do get '/', c: [1, '2', true, 'three'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('[1, 2, "true", "three"]') get '/', d: '1' expect(last_response.status).to eq(200) expect(last_response.body).to eq('1') get '/', d: 'one' expect(last_response.status).to eq(200) expect(last_response.body).to eq('"one"') get '/', d: %w[1 two] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') end end context 'when params is Hashie::Mash' do context 'for primitive collections' do before do subject.params do build_with Grape::Extensions::Hashie::Mash::ParamBuilder optional :a, types: [String, Array[String]] optional :b, types: [Array[Integer], Array[String]] optional :c, type: Array[Integer, String] optional :d, types: [Integer, String, Set[Integer, String]] end subject.get '/' do ( params.a || params.b || params.c || params.d ).inspect end end it 'allows singular form declaration' do get '/', a: 'one way' expect(last_response.status).to eq(200) expect(last_response.body).to eq('"one way"') get '/', a: %w[the other] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') get '/', a: { a: 1, b: 2 } expect(last_response.status).to eq(400) expect(last_response.body).to eq('a is invalid') get '/', a: [1, 2, 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') end it 'allows multiple collection types' do get '/', b: [1, 2, 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') get '/', b: %w[1 2 3] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') get '/', b: [1, true, 'three'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') end it 'allows collections with multiple types' do get '/', c: [1, '2', true, 'three'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') get '/', d: '1' expect(last_response.status).to eq(200) expect(last_response.body).to eq('1') get '/', d: 'one' expect(last_response.status).to eq(200) expect(last_response.body).to eq('"one"') get '/', d: %w[1 two] expect(last_response.status).to eq(200) expect(last_response.body).to eq('#') end end end context 'custom coercion rules' do before do subject.params do requires :a, types: [Boolean, String], coerce_with: (lambda do |val| if val == 'yup' true elsif val == 'false' 0 else val end end) end subject.get '/' do params[:a].class.to_s end end it 'respects :coerce_with' do get '/', a: 'yup' expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') end it 'still validates type' do get '/', a: 'false' expect(last_response.status).to eq(400) expect(last_response.body).to eq('a is invalid') end it 'performs no additional coercion' do get '/', a: 'true' expect(last_response.status).to eq(200) expect(last_response.body).to eq('String') end end it 'may not be supplied together with a single type' do expect do subject.params do requires :a, type: Integer, types: [Integer, String] end end.to raise_exception ArgumentError end end context 'converter' do it 'does not build a coercer multiple times' do subject.params do requires :something, type: Array[String] end subject.get do end expect(Grape::Validations::Types::ArrayCoercer).to( receive(:new).at_most(:once).and_call_original ) 10.times { get '/' } end end end end grape-1.5.2/spec/grape/validations/validators/except_values_spec.rb0000644000004100000410000001764514016744305025611 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::ExceptValuesValidator do module ValidationsSpec class ExceptValuesModel DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze class << self attr_accessor :excepts def excepts @excepts ||= [] [DEFAULT_EXCEPTS + @excepts].flatten.uniq end end end TEST_CASES = { req_except: { requires: { except_values: ExceptValuesModel.excepts }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_hash: { requires: { except_values: { value: ExceptValuesModel.excepts } }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_custom_message: { requires: { except_values: { value: ExceptValuesModel.excepts, message: 'is not allowed' } }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_no_value: { requires: { except_values: { message: 'is not allowed' } }, tests: [ { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } ] }, req_except_empty: { requires: { except_values: [] }, tests: [ { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } ] }, req_except_lambda: { requires: { except_values: -> { ExceptValuesModel.excepts } }, add_excepts: ['invalid-type4'], tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type4', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_lambda_custom_message: { requires: { except_values: { value: -> { ExceptValuesModel.excepts }, message: 'is not allowed' } }, add_excepts: ['invalid-type4'], tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'invalid-type4', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, opt_except_default: { optional: { except_values: ExceptValuesModel.excepts, default: 'valid-type2' }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, { rc: 200, body: { type: 'valid-type2' }.to_json } ] }, opt_except_lambda_default: { optional: { except_values: -> { ExceptValuesModel.excepts }, default: 'valid-type2' }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, { rc: 200, body: { type: 'valid-type2' }.to_json } ] }, req_except_type_coerce: { requires: { type: Integer, except_values: [10, 11] }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: '11', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: '3', rc: 200, body: { type: 3 }.to_json }, { value: 3, rc: 200, body: { type: 3 }.to_json } ] }, opt_except_type_coerce_default: { optional: { type: Integer, except_values: [10, 11], default: 12 }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, { value: 10, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: '3', rc: 200, body: { type: 3 }.to_json }, { value: 3, rc: 200, body: { type: 3 }.to_json }, { rc: 200, body: { type: 12 }.to_json } ] }, opt_except_array_type_coerce_default: { optional: { type: Array[Integer], except_values: [10, 11], default: 12 }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, { value: 10, rc: 400, body: { error: 'type is invalid' }.to_json }, { value: [10], rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: ['3'], rc: 200, body: { type: [3] }.to_json }, { value: [3], rc: 200, body: { type: [3] }.to_json }, { rc: 200, body: { type: 12 }.to_json } ] }, req_except_range: { optional: { type: Integer, except_values: 10..12 }, tests: [ { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 13, rc: 200, body: { type: 13 }.to_json } ] } }.freeze module ExceptValidatorSpec class API < Grape::API default_format :json TEST_CASES.each_with_index do |(k, v), _i| params do requires :type, v[:requires] if v.key? :requires optional :type, v[:optional] if v.key? :optional end get k do { type: params[:type] } end end end end end it 'raises IncompatibleOptionValues on a default value in exclude' do subject = Class.new(Grape::API) expect do subject.params do optional :type, except_values: ValidationsSpec::ExceptValuesModel.excepts, default: ValidationsSpec::ExceptValuesModel.excepts.sample end end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues when a default array has excluded values' do subject = Class.new(Grape::API) expect do subject.params do optional :type, type: Array[Integer], except_values: 10..12, default: [8, 9, 10] end end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues when type is incompatible with values array' do subject = Class.new(Grape::API) expect do subject.params { optional :type, except_values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end def app ValidationsSpec::ExceptValidatorSpec::API end ValidationsSpec::TEST_CASES.each_with_index do |(k, v), i| v[:tests].each do |t| it "#{i}: #{k} - #{t[:value]}" do ValidationsSpec::ExceptValuesModel.excepts = v[:add_excepts] if v.key? :add_excepts body = {} body[:type] = t[:value] if t.key? :value get k.to_s, **body expect(last_response.status).to eq t[:rc] expect(last_response.body).to eq t[:body] ValidationsSpec::ExceptValuesModel.excepts = nil end end end end grape-1.5.2/spec/grape/validations/validators/allow_blank_spec.rb0000644000004100000410000004403214016744305025215 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::AllowBlankValidator do module ValidationsSpec module AllowBlankValidatorSpec class API < Grape::API default_format :json params do requires :name, allow_blank: false end get '/disallow_blank' params do optional :name, type: String, allow_blank: false end get '/opt_disallow_string_blank' params do optional :name, allow_blank: false end get '/disallow_blank_optional_param' params do requires :name, allow_blank: true end get '/allow_blank' params do requires :val, type: DateTime, allow_blank: true end get '/allow_datetime_blank' params do requires :val, type: DateTime, allow_blank: false end get '/disallow_datetime_blank' params do requires :val, type: DateTime end get '/default_allow_datetime_blank' params do requires :val, type: Date, allow_blank: true end get '/allow_date_blank' params do requires :val, type: Integer, allow_blank: true end get '/allow_integer_blank' params do requires :val, type: Float, allow_blank: true end get '/allow_float_blank' params do requires :val, type: Integer, allow_blank: true end get '/allow_integer_blank' params do requires :val, type: Symbol, allow_blank: true end get '/allow_symbol_blank' params do requires :val, type: Boolean, allow_blank: true end get '/allow_boolean_blank' params do requires :val, type: Boolean, allow_blank: false end get '/disallow_boolean_blank' params do optional :user, type: Hash do requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_an_optional_group' params do optional :user, type: Hash do requires :name, type: Date, allow_blank: true end end get '/allow_blank_date_param_in_an_optional_group' params do optional :user, type: Hash do optional :name, allow_blank: false requires :age end end get '/disallow_blank_optional_param_in_an_optional_group' params do requires :user, type: Hash do requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_a_required_group' params do requires :user, type: Hash do requires :name, allow_blank: false end end get '/disallow_string_value_in_a_required_hash_group' params do requires :user, type: Hash do optional :name, allow_blank: false end end get '/disallow_blank_optional_param_in_a_required_group' params do optional :user, type: Hash do optional :name, allow_blank: false end end get '/disallow_string_value_in_an_optional_hash_group' resources :custom_message do params do requires :name, allow_blank: { value: false, message: 'has no value' } end get params do optional :name, allow_blank: { value: false, message: 'has no value' } end get '/disallow_blank_optional_param' params do requires :name, allow_blank: true end get '/allow_blank' params do requires :val, type: DateTime, allow_blank: true end get '/allow_datetime_blank' params do requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } end get '/disallow_datetime_blank' params do requires :val, type: DateTime end get '/default_allow_datetime_blank' params do requires :val, type: Date, allow_blank: true end get '/allow_date_blank' params do requires :val, type: Integer, allow_blank: true end get '/allow_integer_blank' params do requires :val, type: Float, allow_blank: true end get '/allow_float_blank' params do requires :val, type: Integer, allow_blank: true end get '/allow_integer_blank' params do requires :val, type: Symbol, allow_blank: true end get '/allow_symbol_blank' params do requires :val, type: Boolean, allow_blank: true end get '/allow_boolean_blank' params do requires :val, type: Boolean, allow_blank: { value: false, message: 'has no value' } end get '/disallow_boolean_blank' params do optional :user, type: Hash do requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_blank_required_param_in_an_optional_group' params do optional :user, type: Hash do requires :name, type: Date, allow_blank: true end end get '/allow_blank_date_param_in_an_optional_group' params do optional :user, type: Hash do optional :name, allow_blank: { value: false, message: 'has no value' } requires :age end end get '/disallow_blank_optional_param_in_an_optional_group' params do requires :user, type: Hash do requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_blank_required_param_in_a_required_group' params do requires :user, type: Hash do requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_string_value_in_a_required_hash_group' params do requires :user, type: Hash do optional :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_blank_optional_param_in_a_required_group' params do optional :user, type: Hash do optional :name, allow_blank: { value: false, message: 'has no value' } end end get '/disallow_string_value_in_an_optional_hash_group' end end end end def app ValidationsSpec::AllowBlankValidatorSpec::API end context 'invalid input' do it 'refuses empty string' do get '/disallow_blank', name: '' expect(last_response.status).to eq(400) get '/disallow_datetime_blank', val: '' expect(last_response.status).to eq(400) end it 'refuses only whitespaces' do get '/disallow_blank', name: ' ' expect(last_response.status).to eq(400) get '/disallow_blank', name: " \n " expect(last_response.status).to eq(400) get '/disallow_blank', name: "\n" expect(last_response.status).to eq(400) end it 'refuses nil' do get '/disallow_blank', name: nil expect(last_response.status).to eq(400) end it 'refuses missing' do get '/disallow_blank' expect(last_response.status).to eq(400) end end context 'custom validation message' do context 'with invalid input' do it 'refuses empty string' do get '/custom_message', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end it 'refuses empty string for an optional param' do get '/custom_message/disallow_blank_optional_param', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end it 'refuses only whitespaces' do get '/custom_message', name: ' ' expect(last_response.body).to eq('{"error":"name has no value"}') get '/custom_message', name: " \n " expect(last_response.body).to eq('{"error":"name has no value"}') get '/custom_message', name: "\n" expect(last_response.body).to eq('{"error":"name has no value"}') end it 'refuses nil' do get '/custom_message', name: nil expect(last_response.body).to eq('{"error":"name has no value"}') end end context 'with valid input' do it 'accepts valid input' do get '/custom_message', name: 'bob' expect(last_response.status).to eq(200) end it 'accepts empty input when allow_blank is false' do get '/custom_message/allow_blank', name: '' expect(last_response.status).to eq(200) end it 'accepts empty input' do get '/custom_message/default_allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when datetime allow_blank' do get '/custom_message/allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when date allow_blank' do get '/custom_message/allow_date_blank', val: '' expect(last_response.status).to eq(200) end context 'allow_blank when Numeric' do it 'accepts empty when integer allow_blank' do get '/custom_message/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when float allow_blank' do get '/custom_message/allow_float_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when integer allow_blank' do get '/custom_message/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end end it 'accepts empty when symbol allow_blank' do get '/custom_message/allow_symbol_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when boolean allow_blank' do get '/custom_message/allow_boolean_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts false when boolean allow_blank' do get '/custom_message/disallow_boolean_blank', val: false expect(last_response.status).to eq(200) end end context 'in an optional group' do context 'as a required param' do it 'accepts a missing group, even with a disallwed blank param' do get '/custom_message/disallow_blank_required_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing date value' do get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(200) end it 'refuses a blank value in an existing group' do get '/custom_message/disallow_blank_required_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end end context 'as an optional param' do it 'accepts a missing group, even with a disallwed blank param' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing optional value' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end end end context 'in a required group' do context 'as a required param' do it 'refuses a blank value in a required existing group' do get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end it 'refuses a string value in a required hash group' do get '/custom_message/disallow_string_value_in_a_required_hash_group', user: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user is invalid, user[name] is missing"}') end end context 'as an optional param' do it 'accepts a nested missing value' do get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end it 'refuses a string value in an optional hash group' do get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user is invalid"}') end end end end context 'valid input' do it 'allows missing optional strings' do get 'opt_disallow_string_blank' expect(last_response.status).to eq(200) end it 'accepts valid input' do get '/disallow_blank', name: 'bob' expect(last_response.status).to eq(200) end it 'accepts empty input when allow_blank is false' do get '/allow_blank', name: '' expect(last_response.status).to eq(200) end it 'accepts empty input' do get '/default_allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when datetime allow_blank' do get '/allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when date allow_blank' do get '/allow_date_blank', val: '' expect(last_response.status).to eq(200) end context 'allow_blank when Numeric' do it 'accepts empty when integer allow_blank' do get '/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when float allow_blank' do get '/allow_float_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when integer allow_blank' do get '/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end end it 'accepts empty when symbol allow_blank' do get '/allow_symbol_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when boolean allow_blank' do get '/allow_boolean_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts false when boolean allow_blank' do get '/disallow_boolean_blank', val: false expect(last_response.status).to eq(200) end it 'accepts value when time allow_blank' do get '/disallow_datetime_blank', val: Time.now expect(last_response.status).to eq(200) end end context 'in an optional group' do context 'as a required param' do it 'accepts a missing group, even with a disallwed blank param' do get '/disallow_blank_required_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing date value' do get '/allow_blank_date_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(200) end it 'refuses a blank value in an existing group' do get '/disallow_blank_required_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(400) end end context 'as an optional param' do it 'accepts a missing group, even with a disallwed blank param' do get '/disallow_blank_optional_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing optional value' do get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) end end end context 'in a required group' do context 'as a required param' do it 'refuses a blank value in a required existing group' do get '/disallow_blank_required_param_in_a_required_group', user: { name: '' } expect(last_response.status).to eq(400) end it 'refuses a string value in a required hash group' do get '/disallow_string_value_in_a_required_hash_group', user: '' expect(last_response.status).to eq(400) end end context 'as an optional param' do it 'accepts a nested missing value' do get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) end it 'refuses a string value in an optional hash group' do get '/disallow_string_value_in_an_optional_hash_group', user: '' expect(last_response.status).to eq(400) end end end end grape-1.5.2/spec/grape/validations/validators/default_spec.rb0000644000004100000410000003602714016744305024361 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::DefaultValidator do module ValidationsSpec module DefaultValidatorSpec class API < Grape::API default_format :json params do optional :id optional :type, default: 'default-type' end get '/' do { id: params[:id], type: params[:type] } end params do optional :type1, default: 'default-type1' optional :type2, default: 'default-type2' end get '/user' do { type1: params[:type1], type2: params[:type2] } end params do requires :id optional :type1, default: 'default-type1' optional :type2, default: 'default-type2' end get '/message' do { id: params[:id], type1: params[:type1], type2: params[:type2] } end params do optional :random, default: -> { Random.rand } optional :not_random, default: Random.rand end get '/numbers' do { random_number: params[:random], non_random_number: params[:non_random_number] } end params do optional :array, type: Array do requires :name optional :with_default, default: 'default' end end get '/array' do { array: params[:array] } end params do requires :thing1 optional :more_things, type: Array do requires :nested_thing requires :other_thing, default: 1 end end get '/optional_array' do { thing1: params[:thing1] } end params do requires :root, type: Hash do optional :some_things, type: Array do requires :foo optional :options, type: Array do requires :name, type: String requires :value, type: String end end end end get '/nested_optional_array' do { root: params[:root] } end params do requires :root, type: Hash do optional :some_things, type: Array do requires :foo optional :options, type: Array do optional :name, type: String optional :value, type: String end end end end get '/another_nested_optional_array' do { root: params[:root] } end end end end def app ValidationsSpec::DefaultValidatorSpec::API end it 'lets you leave required values nested inside an optional blank' do get '/optional_array', thing1: 'stuff' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ thing1: 'stuff' }.to_json) end it 'allows optional arrays to be omitted' do params = { some_things: [{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, { foo: 'two' }, { foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] } get '/nested_optional_array', root: params expect(last_response.status).to eq(200) expect(last_response.body).to eq({ root: params }.to_json) end it 'does not allows faulty optional arrays' do params = { some_things: [ { foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, { foo: 'two', options: [{ name: 'wat' }] }, { foo: 'three' } ] } error = { error: 'root[some_things][1][options][0][value] is missing' } get '/nested_optional_array', root: params expect(last_response.status).to eq(400) expect(last_response.body).to eq(error.to_json) end it 'allows optional arrays with optional params' do params = { some_things: [ { foo: 'one', options: [{ value: 'nope' }] }, { foo: 'two', options: [{ name: 'wat' }] }, { foo: 'three' } ] } get '/another_nested_optional_array', root: params expect(last_response.status).to eq(200) expect(last_response.body).to eq({ root: params }.to_json) end it 'set default value for optional param' do get('/') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json) end it 'set default values for optional params' do get('/user') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json) end it 'set default values for missing params in the request' do get('/user?type2=value2') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json) end it 'set default values for optional params and allow to use required fields in the same time' do get('/message?id=1') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json) end it 'sets lambda based defaults at the time of call' do get('/numbers') expect(last_response.status).to eq(200) before = JSON.parse(last_response.body) get('/numbers') expect(last_response.status).to eq(200) after = JSON.parse(last_response.body) expect(before['non_random_number']).to eq(after['non_random_number']) expect(before['random_number']).not_to eq(after['random_number']) end it 'sets default values for grouped arrays' do get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json) end context 'optional group with defaults' do subject do Class.new(Grape::API) do default_format :json end end def app subject end context 'optional array without default value includes optional param with default value' do before do subject.params do optional :optional_array, type: Array do optional :foo_in_optional_array, default: 'bar' end end subject.post '/optional_array' do { optional_array: params[:optional_array] } end end it 'returns nil for optional array if param is not provided' do post '/optional_array' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_array: nil }.to_json) end end context 'optional array with default value includes optional param with default value' do before do subject.params do optional :optional_array_with_default, type: Array, default: [] do optional :foo_in_optional_array, default: 'bar' end end subject.post '/optional_array_with_default' do { optional_array_with_default: params[:optional_array_with_default] } end end it 'sets default value for optional array if param is not provided' do post '/optional_array_with_default' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_array_with_default: [] }.to_json) end end context 'optional hash without default value includes optional param with default value' do before do subject.params do optional :optional_hash_without_default, type: Hash do optional :foo_in_optional_hash, default: 'bar' end end subject.post '/optional_hash_without_default' do { optional_hash_without_default: params[:optional_hash_without_default] } end end it 'returns nil for optional hash if param is not provided' do post '/optional_hash_without_default' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_hash_without_default: nil }.to_json) end it 'does not fail even if invalid params is passed to default validator' do expect { post '/optional_hash_without_default', optional_hash_without_default: '5678' }.not_to raise_error expect(last_response.status).to eq(400) expect(last_response.body).to eq({ error: 'optional_hash_without_default is invalid' }.to_json) end end context 'optional hash with default value includes optional param with default value' do before do subject.params do optional :optional_hash_with_default, type: Hash, default: {} do optional :foo_in_optional_hash, default: 'bar' end end subject.post '/optional_hash_with_default_empty_hash' do { optional_hash_with_default: params[:optional_hash_with_default] } end subject.params do optional :optional_hash_with_default, type: Hash, default: { foo_in_optional_hash: 'parent_default' } do optional :some_param optional :foo_in_optional_hash, default: 'own_default' end end subject.post '/optional_hash_with_default_inner_params' do { foo_in_optional_hash: params[:optional_hash_with_default][:foo_in_optional_hash] } end end it 'sets default value for optional hash if param is not provided' do post '/optional_hash_with_default_empty_hash' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_hash_with_default: {} }.to_json) end it 'sets default value from parent defaults for inner param if parent param is not provided' do post '/optional_hash_with_default_inner_params' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ foo_in_optional_hash: 'parent_default' }.to_json) end it 'sets own default value for inner param if parent param is provided' do post '/optional_hash_with_default_inner_params', optional_hash_with_default: { some_param: 'param' } expect(last_response.status).to eq(201) expect(last_response.body).to eq({ foo_in_optional_hash: 'own_default' }.to_json) end end end context 'optional with nil as value' do subject do Class.new(Grape::API) do default_format :json end end def app subject end context 'primitive types' do [ [Integer, 0], [Integer, 42], [Float, 0.0], [Float, 4.2], [BigDecimal, 0.0], [BigDecimal, 4.2], [Numeric, 0], [Numeric, 42], [Date, Date.today], [DateTime, DateTime.now], [Time, Time.now], [Time, Time.at(0)], [Grape::API::Boolean, false], [String, ''], [String, 'non-empty-string'], [Symbol, :symbol], [TrueClass, true], [FalseClass, false] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end context 'structures types' do [ [Hash, {}], [Hash, { test: 'non-empty' }], [Array, []], [Array, ['non-empty']], [Array[Integer], []], [Set, []], [Set, [1]] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end context 'special types' do [ [JSON, ''], [JSON, { test: 'non-empty-string' }.to_json], [Array[JSON], []], [Array[JSON], [{ test: 'non-empty-string' }.to_json]], [::File, ''], [::File, { test: 'non-empty-string' }.to_json], [Rack::Multipart::UploadedFile, ''], [Rack::Multipart::UploadedFile, { test: 'non-empty-string' }.to_json] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end context 'variant-member-type collections' do [ [Array[Integer, String], [0, '']], [Array[Integer, String], [42, 'non-empty-string']], [[Integer, String, Array[Integer, String]], [0, '', [0, '']]], [[Integer, String, Array[Integer, String]], [42, 'non-empty-string', [42, 'non-empty-string']]] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end end context 'array with default values and given conditions' do subject do Class.new(Grape::API) do default_format :json end end def app subject end it 'applies the default values only if the conditions are met' do subject.params do requires :ary, type: Array do requires :has_value, type: Grape::API::Boolean given has_value: ->(has_value) { has_value } do optional :type, type: String, values: %w[str int], default: 'str' given type: ->(type) { type == 'str' } do optional :str, type: String, default: 'a' end given type: ->(type) { type == 'int' } do optional :int, type: Integer, default: 1 end end end end subject.post('/nested_given_and_default') { declared(self.params) } params = { ary: [ { has_value: false }, { has_value: true, type: 'int', int: 123 }, { has_value: true, type: 'str', str: 'b' } ] } expected = { 'ary' => [ { 'has_value' => false, 'type' => nil, 'int' => nil, 'str' => nil }, { 'has_value' => true, 'type' => 'int', 'int' => 123, 'str' => nil }, { 'has_value' => true, 'type' => 'str', 'int' => nil, 'str' => 'b' } ] } post '/nested_given_and_default', params expect(last_response.status).to eq(201) expect(JSON.parse(last_response.body)).to eq(expected) end end end grape-1.5.2/spec/grape/validations/validators/values_spec.rb0000644000004100000410000005355214016744305024236 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::ValuesValidator do module ValidationsSpec class ValuesModel DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'].freeze DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze class << self def values @values ||= [] [DEFAULT_VALUES + @values].flatten.uniq end def add_value(value) @values ||= [] @values << value end def excepts @excepts ||= [] [DEFAULT_EXCEPTS + @excepts].flatten.uniq end def add_except(except) @excepts ||= [] @excepts << except end end end module ValuesValidatorSpec class API < Grape::API default_format :json resources :custom_message do params do requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } end get '/' do { type: params[:type] } end params do optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' end get '/lambda' do { type: params[:type] } end params do requires :type, values: { except: ValuesModel.excepts, except_message: 'value is on exclusions list', message: 'default exclude message' } end get '/exclude/exclude_message' params do requires :type, values: { except: -> { ValuesModel.excepts }, except_message: 'value is on exclusions list' } end get '/exclude/lambda/exclude_message' params do requires :type, values: { except: ValuesModel.excepts, message: 'default exclude message' } end get '/exclude/fallback_message' end params do requires :type, values: ValuesModel.values end get '/' do { type: params[:type] } end params do requires :type, values: [] end get '/empty' params do optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' end get '/default/hash/valid' do { type: params[:type] } end params do optional :type, values: ValuesModel.values, default: 'valid-type2' end get '/default/valid' do { type: params[:type] } end params do optional :type, values: { except: ValuesModel.excepts }, default: 'valid-type2' end get '/default/except' do { type: params[:type] } end params do optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' end get '/lambda' do { type: params[:type] } end params do requires :type, values: ->(v) { ValuesModel.values.include? v } end get '/lambda_val' do { type: params[:type] } end params do requires :number, type: Integer, values: ->(v) { v > 0 } end get '/lambda_int_val' do { number: params[:number] } end params do requires :type, values: -> { [] } end get '/empty_lambda' params do optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } end get '/default_lambda' do { type: params[:type] } end params do optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } end get '/default_and_values_lambda' do { type: params[:type] } end params do optional :type, type: Boolean, desc: 'A boolean', values: [true] end get '/values/optional_boolean' do { type: params[:type] } end params do requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 end get '/values/coercion' do { type: params[:type] } end params do requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 end get '/values/array_coercion' do { type: params[:type] } end params do optional :optional, type: Array do requires :type, values: %w[a b] end end get '/optional_with_required_values' params do requires :type, values: { except: ValuesModel.excepts } end get '/except/exclusive' do { type: params[:type] } end params do requires :type, type: String, values: { except: ValuesModel.excepts } end get '/except/exclusive/type' do { type: params[:type] } end params do requires :type, values: { except: -> { ValuesModel.excepts } } end get '/except/exclusive/lambda' do { type: params[:type] } end params do requires :type, type: String, values: { except: -> { ValuesModel.excepts } } end get '/except/exclusive/lambda/type' do { type: params[:type] } end params do requires :type, type: Integer, values: { except: -> { [3, 4, 5] } } end get '/except/exclusive/lambda/coercion' do { type: params[:type] } end params do requires :type, type: Integer, values: { value: 1..5, except: [3] } end get '/mixed/value/except' do { type: params[:type] } end params do optional :optional, type: Array[String], values: %w[a b c] end put '/optional_with_array_of_string_values' params do requires :type, values: { proc: ->(v) { ValuesModel.values.include? v } } end get '/proc' do { type: params[:type] } end params do requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' } end get '/proc/message' params do optional :name, type: String, values: %w[a b], allow_blank: true end get '/allow_blank' end end end def app ValidationsSpec::ValuesValidatorSpec::API end context 'with a custom validation message' do it 'allows a valid value for a parameter' do get('/custom_message', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not allow an invalid value for a parameter' do get('/custom_message', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) end it 'validates against values in a proc' do ValidationsSpec::ValuesModel.add_value('valid-type4') get('/custom_message/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) end it 'does not allow an invalid value for a parameter using lambda' do get('/custom_message/lambda', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) end end context 'with a custom exclude validation message' do it 'does not allow an invalid value for a parameter' do get('/custom_message/exclude/exclude_message', type: 'invalid-type1') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json) end end context 'with a custom exclude validation message' do it 'does not allow an invalid value for a parameter' do get('/custom_message/exclude/lambda/exclude_message', type: 'invalid-type1') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type value is on exclusions list' }.to_json) end end context 'exclude with a standard custom validation message' do it 'does not allow an invalid value for a parameter' do get('/custom_message/exclude/fallback_message', type: 'invalid-type1') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type default exclude message' }.to_json) end end it 'allows a valid value for a parameter' do get('/', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not allow an invalid value for a parameter' do get('/', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'rejects all values if values is an empty array' do get('/empty', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end context 'nil value for a parameter' do it 'does not allow for root params scope' do get('/', type: nil) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'allows for a required param in child scope' do get('/optional_with_required_values') expect(last_response.status).to eq 200 end it 'accepts for an optional param with a list of values' do put('/optional_with_array_of_string_values', optional: nil) expect(last_response.status).to eq 200 end end it 'allows a valid default value' do get('/default/valid') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end it 'allows a default value with except' do get('/default/except') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end it 'allows a valid default value' do get('/default/hash/valid') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end it 'allows a proc for values' do get('/lambda', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not validate updated values without proc' do ValidationsSpec::ValuesModel.add_value('valid-type4') get('/', type: 'valid-type4') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'validates against values in a proc' do ValidationsSpec::ValuesModel.add_value('valid-type4') get('/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) end it 'does not allow an invalid value for a parameter using lambda' do get('/lambda', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'does not allow non-numeric string value for int value using lambda' do get('/lambda_int_val', number: 'foo') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json) end it 'does not allow nil for int value using lambda' do get('/lambda_int_val', number: nil) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json) end it 'allows numeric string for int value using lambda' do get('/lambda_int_val', number: '3') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ number: 3 }.to_json) end it 'allows value using lambda' do get('/lambda_val', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not allow invalid value using lambda' do get('/lambda_val', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'validates against an empty array in a proc' do get('/empty_lambda', type: 'any') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'validates default value from proc' do get('/default_lambda') expect(last_response.status).to eq 200 end it 'validates default value from proc against values in a proc' do get('/default_and_values_lambda') expect(last_response.status).to eq 200 end it 'raises IncompatibleOptionValues on an invalid default value from proc' do subject = Class.new(Grape::API) expect do subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: ValidationsSpec::ValuesModel.values.sample + '_invalid' } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues on an invalid default value' do subject = Class.new(Grape::API) expect do subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues when type is incompatible with values array' do subject = Class.new(Grape::API) expect do subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end context 'boolean values' do it 'allows a value from the list' 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 'rejects a value which is not in the list' do get('/values/optional_boolean', type: false) expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end it 'allows values to be a kind of the coerced type not just an instance of it' do get('/values/coercion', type: 10) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 10 }.to_json) end it 'allows values to be a kind of the coerced type in an array' do get('/values/array_coercion', type: [10]) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: [10] }.to_json) end it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do subject = Class.new(Grape::API) expect do subject.params { requires :type, values: [10.5, 11], type: Integer } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises IncompatibleOptionValues when except contains a value that is not a kind of the type' do subject = Class.new(Grape::API) expect do subject.params { requires :type, values: { except: [10.5, 11] }, type: Integer } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'allows a blank value when the allow_blank option is true' do get 'allow_blank', name: nil expect(last_response.status).to eq(200) get 'allow_blank', name: '' expect(last_response.status).to eq(200) end context 'with a lambda values' do subject do Class.new(Grape::API) do params do optional :type, type: String, values: -> { [SecureRandom.uuid] }, default: -> { SecureRandom.uuid } end get '/random_values' end end def app subject end before do expect(SecureRandom).to receive(:uuid).and_return('foo').once end it 'only evaluates values dynamically with each request' do get '/random_values', type: 'foo' expect(last_response.status).to eq 200 end it 'chooses default' do get '/random_values' expect(last_response.status).to eq 200 end end context 'with a range of values' do subject(:app) do Class.new(Grape::API) do params do optional :value, type: Float, values: 0.0..10.0 end get '/value' do { value: params[:value] }.to_json end params do optional :values, type: Array[Float], values: 0.0..10.0 end get '/values' do { values: params[:values] }.to_json end end end it 'allows a single value inside of the range' do get('/value', value: 5.2) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ value: 5.2 }.to_json) end it 'allows an array of values inside of the range' do get('/values', values: [8.6, 7.5, 3, 0.9]) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ values: [8.6, 7.5, 3.0, 0.9] }.to_json) end it 'rejects a single value outside the range' do get('/value', value: 'a') expect(last_response.status).to eq 400 expect(last_response.body).to eq('value is invalid, value does not have a valid value') end it 'rejects an array of values if any of them are outside the range' do get('/values', values: [8.6, 75, 3, 0.9]) expect(last_response.status).to eq 400 expect(last_response.body).to eq('values does not have a valid value') end end context 'exclusive excepts' do it 'allows any other value outside excepts' do get '/except/exclusive', type: 'value' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'value' }.to_json) end it 'allows any other value outside excepts when type is included' do get '/except/exclusive/type', type: 'value' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'value' }.to_json) end it 'rejects values that matches except' do get '/except/exclusive', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json) end it 'rejects an array of values if any of them matches except' do get '/except/exclusive', type: %w[valid1 valid2 invalid-type1 valid4] expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json) end end context 'exclusive excepts with lambda' do it 'allows any other value outside excepts when type is included' do get '/except/exclusive/lambda/type', type: 'value' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'value' }.to_json) end it 'allows any other value outside excepts' do get '/except/exclusive/lambda', type: 'value' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'value' }.to_json) end it 'rejects values that matches except' do get '/except/exclusive/lambda', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json) end end context 'exclusive excepts with lambda and coercion' do it 'allows any other value outside excepts' do get '/except/exclusive/lambda/coercion', type: '10010000' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 10_010_000 }.to_json) end it 'rejects values that matches except' do get '/except/exclusive/lambda/coercion', type: '3' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json) end end context 'with mixed values and excepts' do it 'allows value, but not in except' do get '/mixed/value/except', type: 2 expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 2 }.to_json) end it 'rejects except' do get '/mixed/value/except', type: 3 expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json) end it 'rejects outside except and outside value' do get '/mixed/value/except', type: 10 expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end context 'custom validation using proc' do it 'accepts a single valid value' do get '/proc', type: 'valid-type1' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'accepts multiple valid values' do get '/proc', type: ['valid-type1', 'valid-type3'] expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: ['valid-type1', 'valid-type3'] }.to_json) end it 'rejects a single invalid value' do get '/proc', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'rejects an invalid value among valid ones' do get '/proc', type: ['valid-type1', 'invalid-type1', 'valid-type3'] expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'uses supplied message' do get '/proc/message', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type failed check' }.to_json) end end end grape-1.5.2/spec/grape/validations/validators/all_or_none_spec.rb0000644000004100000410000001145414016744305025221 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::AllOrNoneOfValidator do describe '#validate!' do subject(:validate) { post path, params } module ValidationsSpec module AllOrNoneOfValidatorSpec class API < Grape::API rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, type: Boolean all_or_none_of :beer, :wine end post do end params do optional :beer, :wine, :other, type: Boolean all_or_none_of :beer, :wine end post 'mixed-params' do end params do optional :beer, :wine, type: Boolean all_or_none_of :beer, :wine, message: 'choose all or none' end post '/custom-message' do end params do requires :item, type: Hash do optional :beer, :wine, type: Boolean all_or_none_of :beer, :wine end end post '/nested-hash' do end params do requires :items, type: Array do optional :beer, :wine, type: Boolean all_or_none_of :beer, :wine end end post '/nested-array' do end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, type: Boolean all_or_none_of :beer, :wine end end end post '/deeply-nested-array' do end end end end def app ValidationsSpec::AllOrNoneOfValidatorSpec::API end context 'when all restricted params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end context 'mixed with other params' do let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end context 'when a subset of restricted params are present' do let(:path) { '/' } let(:params) { { beer: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['provide all or none of parameters'] ) end end context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['choose all or none'] ) end end context 'when no restricted params are present' do let(:path) { '/' } let(:params) { { somethingelse: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when restricted params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['provide all or none of parameters'] ) end end context 'when mutually exclusive params are nested inside array' do let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[1][beer],items[1][wine]' => ['provide all or none of parameters'] ) end end context 'when mutually exclusive params are deeply nested' do let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => [ 'provide all or none of parameters' ] ) end end end end grape-1.5.2/spec/grape/validations/validators/regexp_spec.rb0000644000004100000410000001153414016744305024223 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::RegexpValidator do module ValidationsSpec module RegexpValidatorSpec class API < Grape::API default_format :json resources :custom_message do params do requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } end get do end params do requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } end get 'regexp_with_array' do end end params do requires :name, regexp: /^[a-z]+$/ end get do end params do requires :names, type: Array[String], regexp: /^[a-z]+$/ end get 'regexp_with_array' do end params do requires :people, type: Hash do requires :names, type: Array[String], regexp: /^[a-z]+$/ end end get 'nested_regexp_with_array' do end end end end def app ValidationsSpec::RegexpValidatorSpec::API end context 'custom validation message' do context 'with invalid input' do it 'refuses inapppopriate' do get '/custom_message', name: 'invalid name' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name format is invalid"}') end it 'refuses empty' do get '/custom_message', name: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name format is invalid"}') end end it 'accepts nil' do get '/custom_message', name: nil expect(last_response.status).to eq(200) end it 'accepts valid input' do get '/custom_message', name: 'bob' expect(last_response.status).to eq(200) end context 'regexp with array' do it 'refuses inapppopriate items' do get '/custom_message/regexp_with_array', names: ['invalid name', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names format is invalid"}') end it 'refuses empty items' do get '/custom_message/regexp_with_array', names: ['', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names format is invalid"}') end it 'refuses nil items' do get '/custom_message/regexp_with_array', names: [nil, 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names can\'t be nil"}') end it 'accepts valid items' do get '/custom_message/regexp_with_array', names: ['bob'] expect(last_response.status).to eq(200) end it 'accepts nil instead of array' do get '/custom_message/regexp_with_array', names: nil expect(last_response.status).to eq(200) end end end context 'invalid input' do it 'refuses inapppopriate' do get '/', name: 'invalid name' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is invalid"}') end it 'refuses empty' do get '/', name: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is invalid"}') end end it 'accepts nil' do get '/', name: nil expect(last_response.status).to eq(200) end it 'accepts valid input' do get '/', name: 'bob' expect(last_response.status).to eq(200) end context 'regexp with array' do it 'refuses inapppopriate items' do get '/regexp_with_array', names: ['invalid name', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names is invalid"}') end it 'refuses empty items' do get '/regexp_with_array', names: ['', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names is invalid"}') end it 'refuses nil items' do get '/regexp_with_array', names: [nil, 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names is invalid"}') end it 'accepts valid items' do get '/regexp_with_array', names: ['bob'] expect(last_response.status).to eq(200) end it 'accepts nil instead of array' do get '/regexp_with_array', names: nil expect(last_response.status).to eq(200) end end context 'nested regexp with array' do it 'refuses inapppopriate' do get '/nested_regexp_with_array', people: 'invalid name' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}') end end end grape-1.5.2/spec/grape/validations/validators/zh-CN.yml0000644000004100000410000000033614016744305023032 0ustar www-datawww-datazh-CN: grape: errors: format: ! '%{attributes}%{message}' attributes: age: 年龄 messages: coerce: '格式不正确' presence: '请填写' regexp: '格式不正确' grape-1.5.2/spec/grape/validations/types_spec.rb0000644000004100000410000000451614016744305021727 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::Types do module TypesSpec class FooType def self.parse(_); end end class BarType def self.parse; end end end describe '::primitive?' do [ Integer, Float, Numeric, BigDecimal, Grape::API::Boolean, String, Symbol, Date, DateTime, Time ].each do |type| it "recognizes #{type} as a primitive" do expect(described_class.primitive?(type)).to be_truthy end end it 'identifies unknown types' do expect(described_class.primitive?(Object)).to be_falsy expect(described_class.primitive?(TypesSpec::FooType)).to be_falsy end end describe '::structure?' do [ Hash, Array, Set ].each do |type| it "recognizes #{type} as a structure" do expect(described_class.structure?(type)).to be_truthy end end end describe '::special?' do [ JSON, Array[JSON], File, Rack::Multipart::UploadedFile ].each do |type| it "provides special handling for #{type.inspect}" do expect(described_class.special?(type)).to be_truthy end end end describe '::custom?' do it 'returns false if the type does not respond to :parse' do expect(described_class.custom?(Object)).to be_falsy end it 'returns true if the type responds to :parse with one argument' do expect(described_class.custom?(TypesSpec::FooType)).to be_truthy end it 'returns false if the type\'s #parse method takes other than one argument' do expect(described_class.custom?(TypesSpec::BarType)).to be_falsy end end describe '::build_coercer' do it 'has internal cache variables' do expect(described_class.instance_variable_get(:@__cache)).to be_a(Hash) expect(described_class.instance_variable_get(:@__cache_write_lock)).to be_a(Mutex) end it 'caches the result of the build_coercer method' do original_cache = described_class.instance_variable_get(:@__cache) described_class.instance_variable_set(:@__cache, {}) a_coercer = described_class.build_coercer(Array[String]) b_coercer = described_class.build_coercer(Array[String]) expect(a_coercer.object_id).to eq(b_coercer.object_id) described_class.instance_variable_set(:@__cache, original_cache) end end end grape-1.5.2/spec/grape/validations/attributes_iterator_spec.rb0000644000004100000410000000015514016744305024655 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::AttributesIterator do end grape-1.5.2/spec/grape/validations/multiple_attributes_iterator_spec.rb0000644000004100000410000000264514016744305026576 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations::MultipleAttributesIterator do describe '#each' do subject(:iterator) { described_class.new(validator, scope, params) } let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) } let(:validator) { double(attrs: %i[first second third]) } context 'when params is a hash' do let(:params) do { first: 'string', second: 'string' } end it 'yields the whole params hash and the skipped flag without the list of attrs' do expect { |b| iterator.each(&b) }.to yield_with_args(params, false) end end context 'when params is an array' do let(:params) do [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }] end it 'yields each element of the array without the list of attrs' do expect { |b| iterator.each(&b) }.to yield_successive_args([params[0], false], [params[1], false]) end end context 'when params is empty optional placeholder' do let(:params) do [Grape::DSL::Parameters::EmptyOptionalValue, { first: 'string2', second: 'string2' }] end it 'yields each element of the array without the list of attrs' do expect { |b| iterator.each(&b) }.to yield_successive_args([Grape::DSL::Parameters::EmptyOptionalValue, true], [params[1], false]) end end end end grape-1.5.2/spec/grape/validations/instance_behaivour_spec.rb0000644000004100000410000000233014016744305024423 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe 'Validator with instance variables' do let(:validator_type) do Class.new(Grape::Validations::Base) do def validate_param!(_attr_name, _params) if instance_variable_defined?(:@instance_variable) && @instance_variable raise Grape::Exceptions::Validation.new(params: ['params'], message: 'This should never happen') end @instance_variable = true end end end before do Grape::Validations.register_validator('instance_validator', validator_type) end after do Grape::Validations.deregister_validator('instance_validator') end let(:app) do Class.new(Grape::API) do params do optional :param_to_validate, instance_validator: true optional :another_param_to_validate, instance_validator: true end get do 'noop' end end end it 'passes validation every time' do expect(validator_type).to receive(:new).exactly(4).times.and_call_original 2.times do get '/', param_to_validate: 'value', another_param_to_validate: 'value' expect(last_response.status).to eq 200 end end end grape-1.5.2/spec/grape/extensions/0000755000004100000410000000000014016744305017100 5ustar www-datawww-datagrape-1.5.2/spec/grape/extensions/param_builders/0000755000004100000410000000000014016744305022071 5ustar www-datawww-datagrape-1.5.2/spec/grape/extensions/param_builders/hashie/0000755000004100000410000000000014016744305023332 5ustar www-datawww-datagrape-1.5.2/spec/grape/extensions/param_builders/hashie/mash_spec.rb0000644000004100000410000000337114016744305025625 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Extensions::Hashie::Mash::ParamBuilder do subject { Class.new(Grape::API) } def app subject end describe 'in an endpoint' do context '#params' do before do subject.params do build_with Grape::Extensions::Hashie::Mash::ParamBuilder end subject.get do params.class end end it 'should be of type Hashie::Mash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hashie::Mash') end end end describe 'in an api' do before do subject.send(:include, Grape::Extensions::Hashie::Mash::ParamBuilder) end context '#params' do before do subject.get do params.class end end it 'should be Hashie::Mash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hashie::Mash') end end context 'in a nested namespace api' do before do subject.namespace :foo do get do params.class end end end it 'should be Hashie::Mash' do get '/foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hashie::Mash') end end it 'is indifferent to key or symbol access' do subject.params do build_with Grape::Extensions::Hashie::Mash::ParamBuilder requires :a, type: String end subject.get '/foo' do [params[:a], params['a']] end get '/foo', a: 'bar' expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", "bar"]') end end end grape-1.5.2/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb0000644000004100000410000000546214016744305031313 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do subject { Class.new(Grape::API) } def app subject end describe 'in an endpoint' do context '#params' do before do subject.params do build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder end subject.get do params.class end end it 'should be of type Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') end end end describe 'in an api' do before do subject.send(:include, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) end context '#params' do before do subject.get do params.class end end it 'is a Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') end it 'parses sub hash params' do subject.params do build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String end optional :d, type: Array end end subject.get '/foo' do [params[:a]['b'][:c], params['a'][:d]] end get '/foo', a: { b: { c: 'bar' }, d: ['foo'] } expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", ["foo"]]') end it 'params are indifferent to symbol or string keys' do subject.params do build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String end optional :d, type: Array end end subject.get '/foo' do [params[:a]['b'][:c], params['a'][:d]] end get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", ["foo"]]') end it 'responds to string keys' do subject.params do build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder requires :a, type: String end subject.get '/foo' do [params[:a], params['a']] end get '/foo', a: 'bar' expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", "bar"]') end end end end grape-1.5.2/spec/grape/extensions/param_builders/hash_spec.rb0000644000004100000410000000347314016744305024362 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Extensions::Hash::ParamBuilder do subject { Class.new(Grape::API) } def app subject end describe 'in an endpoint' do context '#params' do before do subject.params do build_with Grape::Extensions::Hash::ParamBuilder end subject.get do params.class end end it 'should be of type Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hash') end end end describe 'in an api' do before do subject.send(:include, Grape::Extensions::Hash::ParamBuilder) end context '#params' do before do subject.get do params.class end end it 'should be Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hash') end end it 'symbolizes params keys' do subject.params do optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String end optional :d, type: Array end end subject.get '/foo' do [params[:a][:b][:c], params[:a][:d]] end get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", ["foo"]]') end it 'symbolizes the params' do subject.params do build_with Grape::Extensions::Hash::ParamBuilder requires :a, type: String end subject.get '/foo' do [params[:a], params['a']] end get '/foo', a: 'bar' expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", nil]') end end end grape-1.5.2/spec/grape/api/0000755000004100000410000000000014016744305015452 5ustar www-datawww-datagrape-1.5.2/spec/grape/api/invalid_format_spec.rb0000644000004100000410000000200114016744305022000 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace do format :json content_type :json, 'application/json' params do requires :id, desc: 'Identifier.' end get ':id' do { id: params[:id], format: params[:format] } end end end context 'get' do it 'no format' do get '/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: nil)) end it 'json format' do get '/foo.json' expect(last_response.status).to eq 200 expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'json')) end it 'invalid format' do get '/foo.invalid' expect(last_response.status).to eq 200 expect(last_response.body).to eq(::Grape::Json.dump(id: 'foo', format: 'invalid')) end end end grape-1.5.2/spec/grape/api/required_parameters_with_invalid_method_spec.rb0000644000004100000410000000064414016744305027161 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace do params do requires :id, desc: 'Identifier.' end get ':id' do end end end context 'post' do it '405' do post '/something' expect(last_response.status).to eq 405 end end end grape-1.5.2/spec/grape/api/instance_spec.rb0000644000004100000410000000470414016744305020622 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' require 'shared/versioning_examples' describe Grape::API::Instance do subject(:an_instance) do Class.new(Grape::API::Instance) do namespace :some_namespace do get 'some_endpoint' do 'success' end end end end let(:root_api) do to_mount = an_instance Class.new(Grape::API) do mount to_mount end end def app root_api end context 'when an instance is mounted on the root' do it 'can call the instance endpoint' do get '/some_namespace/some_endpoint' expect(last_response.body).to eq 'success' end end context 'when an instance is the root' do let(:root_api) do to_mount = an_instance Class.new(Grape::API::Instance) do mount to_mount end end it 'can call the instance endpoint' do get '/some_namespace/some_endpoint' expect(last_response.body).to eq 'success' end end context 'top level setting' do it 'does not inherit settings from the superclass (Grape::API::Instance)' do expect(an_instance.top_level_setting.parent).to be_nil end end context 'with multiple moutes' do let(:first) do Class.new(Grape::API::Instance) do namespace(:some_namespace) do route :any, '*path' do error!('Not found! (1)', 404) end end end end let(:second) do Class.new(Grape::API::Instance) do namespace(:another_namespace) do route :any, '*path' do error!('Not found! (2)', 404) end end end end let(:root_api) do first_instance = first second_instance = second Class.new(Grape::API) do mount first_instance mount first_instance mount second_instance end end it 'does not raise a FrozenError on first instance' do expect { patch '/some_namespace/anything' }.not_to \ raise_error end it 'responds the correct body at the first instance' do patch '/some_namespace/anything' expect(last_response.body).to eq 'Not found! (1)' end it 'does not raise a FrozenError on second instance' do expect { get '/another_namespace/other' }.not_to \ raise_error end it 'responds the correct body at the second instance' do get '/another_namespace/foobar' expect(last_response.body).to eq 'Not found! (2)' end end end grape-1.5.2/spec/grape/api/shared_helpers_exactly_one_of_spec.rb0000644000004100000410000000217214016744305025061 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API::Helpers do subject do shared_params = Module.new do extend Grape::API::Helpers params :drink do optional :beer optional :wine exactly_one_of :beer, :wine end end Class.new(Grape::API) do helpers shared_params format :json params do requires :orderType, type: String, values: %w[food drink] given orderType: ->(val) { val == 'food' } do optional :pasta optional :pizza exactly_one_of :pasta, :pizza end given orderType: ->(val) { val == 'drink' } do use :drink end end get do declared(params, include_missing: true) end end end def app subject end it 'defines parameters' do get '/', orderType: 'food', pizza: 'mista' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ orderType: 'food', pasta: nil, pizza: 'mista', beer: nil, wine: nil }.to_json) end end grape-1.5.2/spec/grape/api/parameters_modification_spec.rb0000644000004100000410000000162614016744305023706 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :test do params do optional :foo, default: +'-abcdef' end get do params[:foo].slice!(0) params[:foo] end end end context 'when route modifies param value' do it 'param default should not change' do get '/test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'abcdef' get '/test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'abcdef' get '/test?foo=-123456' expect(last_response.status).to eq 200 expect(last_response.body).to eq '123456' get '/test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'abcdef' end end end grape-1.5.2/spec/grape/api/routes_with_requirements_spec.rb0000644000004100000410000000407714016744305024200 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end context 'get' do it 'routes to a namespace param with dots' do subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do get '/' do params[:ns_with_dots] end end get '/test.id.with.dots' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id.with.dots' end it 'routes to a path with multiple params with dots' do subject.get ':id_with_dots/:another_id_with_dots', requirements: { id_with_dots: %r{[^\/]+}, another_id_with_dots: %r{[^\/]+} } do "#{params[:id_with_dots]}/#{params[:another_id_with_dots]}" end get '/test.id/test2.id' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id/test2.id' end it 'routes to namespace and path params with dots, with overridden requirements' do subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do get ':another_id_with_dots', requirements: { ns_with_dots: %r{[^\/]+}, another_id_with_dots: %r{[^\/]+} } do "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}" end end get '/test.id/test2.id' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id/test2.id' end it 'routes to namespace and path params with dots, with merged requirements' do subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^\/]+} } do get ':another_id_with_dots', requirements: { another_id_with_dots: %r{[^\/]+} } do "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}" end end get '/test.id/test2.id' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id/test2.id' end end end grape-1.5.2/spec/grape/api/namespace_parameters_in_route_spec.rb0000644000004100000410000000126514016744305025100 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :me do namespace :pending do get '/' do 'banana' end end put ':id' do params[:id] end end end context 'get' do it 'responds without ext' do get '/me/pending' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'banana' end end context 'put' do it 'responds' do put '/me/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end end grape-1.5.2/spec/grape/api/patch_method_helpers_spec.rb0000644000004100000410000000334314016744305023175 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API::Helpers do module PatchHelpersSpec class PatchPublic < Grape::API format :json version 'public-v1', using: :header, vendor: 'grape' get do { ok: 'public' } end end module AuthMethods def authenticate!; end end class PatchPrivate < Grape::API format :json version 'private-v1', using: :header, vendor: 'grape' helpers AuthMethods before do authenticate! end get do { ok: 'private' } end end class Main < Grape::API mount PatchPublic mount PatchPrivate end end def app PatchHelpersSpec::Main end context 'patch' do it 'public' do patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 405 end it 'private' do patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 405 end it 'default' do patch '/' expect(last_response.status).to eq 405 end end context 'default' do it 'public' do get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'public' }.to_json) end it 'private' do get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'private' }.to_json) end it 'default' do get '/' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'public' }.to_json) end end end grape-1.5.2/spec/grape/api/shared_helpers_spec.rb0000644000004100000410000000127114016744305022002 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API::Helpers do subject do shared_params = Module.new do extend Grape::API::Helpers params :pagination do optional :page, type: Integer optional :size, type: Integer end end Class.new(Grape::API) do helpers shared_params format :json params do use :pagination end get do declared(params, include_missing: true) end end end def app subject end it 'defines parameters' do get '/' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ page: nil, size: nil }.to_json) end end grape-1.5.2/spec/grape/api/nested_helpers_spec.rb0000644000004100000410000000172214016744305022017 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API::Helpers do module NestedHelpersSpec module HelperMethods extend Grape::API::Helpers def current_user @current_user ||= params[:current_user] end end class Nested < Grape::API resource :level1 do helpers HelperMethods get do current_user end resource :level2 do get do current_user end end end end class Main < Grape::API mount Nested end end subject do NestedHelpersSpec::Main end def app subject end it 'can access helpers from a mounted resource' do get '/level1', current_user: 'hello' expect(last_response.body).to eq('hello') end it 'can access helpers from a mounted resource in a nested resource' do get '/level1/level2', current_user: 'world' expect(last_response.body).to eq('world') end end grape-1.5.2/spec/grape/api/recognize_path_spec.rb0000644000004100000410000000110614016744305022010 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API do describe '.recognize_path' do subject { Class.new(Grape::API) } it 'fetches endpoint by given path' do subject.get('/foo/:id') {} subject.get('/bar/:id') {} subject.get('/baz/:id') {} actual = subject.recognize_path('/bar/1234').routes[0].origin expect(actual).to eq('/bar/:id') end it 'returns nil if given path does not match with registered routes' do subject.get {} expect(subject.recognize_path('/bar/1234')).to be_nil end end end grape-1.5.2/spec/grape/api/defines_boolean_in_params_spec.rb0000644000004100000410000000171314016744305024160 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API::Instance do describe 'boolean constant' do module DefinesBooleanInstanceSpec class API < Grape::API params do requires :message, type: Boolean end post :echo do { class: params[:message].class.name, value: params[:message] } end end end def app DefinesBooleanInstanceSpec::API end let(:expected_body) do { class: 'TrueClass', value: true }.to_s end it 'sets Boolean as a type' do post '/echo?message=true' expect(last_response.status).to eq(201) expect(last_response.body).to eq expected_body end context 'Params endpoint type' do subject { DefinesBooleanInstanceSpec::API.new.router.map['POST'].first.options[:params]['message'][:type] } it 'params type is a boolean' do is_expected.to eq 'Grape::API::Boolean' end end end end grape-1.5.2/spec/grape/api/required_parameters_in_route_spec.rb0000644000004100000410000000126514016744305024764 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :api do get ':id' do [params[:id], params[:ext]].compact.join('/') end put ':something_id' do params[:something_id] end end end context 'get' do it 'responds' do get '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end context 'put' do it 'responds' do put '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end end grape-1.5.2/spec/grape/api/optional_parameters_in_route_spec.rb0000644000004100000410000000152214016744305024765 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :api do get ':id(/:ext)' do [params[:id], params[:ext]].compact.join('/') end put ':id' do params[:id] end end end context 'get' do it 'responds without ext' do get '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end it 'responds with ext' do get '/api/foo/bar' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo/bar' end end context 'put' do it 'responds' do put '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end end grape-1.5.2/spec/grape/api/deeply_included_options_spec.rb0000644000004100000410000000222314016744305023714 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' module DeeplyIncludedOptionsSpec module Defaults extend ActiveSupport::Concern included do format :json end end module Admin module Defaults extend ActiveSupport::Concern include DeeplyIncludedOptionsSpec::Defaults end class Users < Grape::API include DeeplyIncludedOptionsSpec::Admin::Defaults resource :users do get do status 200 end end end end class Main < Grape::API mount DeeplyIncludedOptionsSpec::Admin::Users end end describe Grape::API do subject { DeeplyIncludedOptionsSpec::Main } def app subject end it 'works for unspecified format' do get '/users' expect(last_response.status).to eql 200 expect(last_response.content_type).to eql 'application/json' end it 'works for specified format' do get '/users.json' expect(last_response.status).to eql 200 expect(last_response.content_type).to eql 'application/json' end it "doesn't work for format different than specified" do get '/users.txt' expect(last_response.status).to eql 404 end end grape-1.5.2/spec/grape/api/inherited_helpers_spec.rb0000644000004100000410000000474614016744305022521 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::API::Helpers do let(:user) { 'Miguel Caneo' } let(:id) { '42' } module InheritedHelpersSpec class SuperClass < Grape::API helpers do params(:superclass_params) { requires :id, type: String } def current_user params[:user] end end end class OverriddenSubClass < SuperClass params { use :superclass_params } helpers do def current_user "#{params[:user]} with id" end end get 'resource' do "#{current_user}: #{params['id']}" end end class SubClass < SuperClass params { use :superclass_params } get 'resource' do "#{current_user}: #{params['id']}" end end class Example < SubClass params { use :superclass_params } get 'resource' do "#{current_user}: #{params['id']}" end end end context 'non overriding subclass' do subject { InheritedHelpersSpec::SubClass } def app subject end context 'given expected params' do it 'inherits helpers from a superclass' do get '/resource', id: id, user: user expect(last_response.body).to eq("#{user}: #{id}") end end context 'with lack of expected params' do it 'returns missing error' do get '/resource' expect(last_response.body).to eq('id is missing') end end end context 'overriding subclass' do subject { InheritedHelpersSpec::OverriddenSubClass } def app subject end context 'given expected params' do it 'overrides helpers from a superclass' do get '/resource', id: id, user: user expect(last_response.body).to eq("#{user} with id: #{id}") end end context 'with lack of expected params' do it 'returns missing error' do get '/resource' expect(last_response.body).to eq('id is missing') end end end context 'example subclass' do subject { InheritedHelpersSpec::Example } def app subject end context 'given expected params' do it 'inherits helpers from a superclass' do get '/resource', id: id, user: user expect(last_response.body).to eq("#{user}: #{id}") end end context 'with lack of expected params' do it 'returns missing error' do get '/resource' expect(last_response.body).to eq('id is missing') end end end end grape-1.5.2/spec/grape/api/custom_validations_spec.rb0000644000004100000410000001257614016744305022733 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Validations do context 'using a custom length validator' do before do module CustomValidationsSpec class DefaultLength < Grape::Validations::Base def validate_param!(attr_name, params) @option = params[:max].to_i if params.key?(:max) return if params[attr_name].length <= @option raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long") end end end end subject do Class.new(Grape::API) do params do requires :text, default_length: 140 end get do 'bacon' end end end def app subject end it 'under 140 characters' do get '/', text: 'abc' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'over 140 characters' do get '/', text: 'a' * 141 expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text must be at the most 140 characters long' end it 'specified in the query string' do get '/', text: 'a' * 141, max: 141 expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end end context 'using a custom body-only validator' do before do module CustomValidationsSpec class InBody < Grape::Validations::PresenceValidator def validate(request) validate!(request.env['api.request.body']) end end end end subject do Class.new(Grape::API) do params do requires :text, in_body: true end get do 'bacon' end end end def app subject end it 'allows field in body' do get '/', text: 'abc' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'ignores field in query' do get '/', nil, text: 'abc' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text is missing' end end context 'using a custom validator with message_key' do before do module CustomValidationsSpec class WithMessageKey < Grape::Validations::PresenceValidator def validate_param!(attr_name, _params) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: :presence) end end end end subject do Class.new(Grape::API) do params do requires :text, with_message_key: true end get do 'bacon' end end end def app subject end it 'fails with message' do get '/', text: 'foobar' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text is missing' end end context 'using a custom request/param validator' do before do module CustomValidationsSpec class Admin < Grape::Validations::Base def validate(request) # return if the param we are checking was not in request # @attrs is a list containing the attribute we are currently validating return unless request.params.key? @attrs.first # check if admin flag is set to true return unless @option # check if user is admin or not # as an example get a token from request and check if it's admin or not raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers['X-Access-Token'] == 'admin' end end end end subject do Class.new(Grape::API) do params do optional :admin_field, type: String, admin: true optional :non_admin_field, type: String optional :admin_false_field, type: String, admin: false end get do 'bacon' end end end def app subject end it 'fail when non-admin user sets an admin field' do get '/', admin_field: 'tester', non_admin_field: 'toaster' expect(last_response.status).to eq 400 expect(last_response.body).to include 'Can not set Admin only field.' end it 'does not fail when we send non-admin fields only' do get '/', non_admin_field: 'toaster' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'does not fail when we send non-admin and admin=false fields only' do get '/', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'does not fail when we send admin fields and we are admin' do header 'X-Access-Token', 'admin' get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'fails when we send admin fields and we are not admin' do header 'X-Access-Token', 'user' get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 400 expect(last_response.body).to include 'Can not set Admin only field.' end end end grape-1.5.2/spec/shared/0000755000004100000410000000000014016744305015051 5ustar www-datawww-datagrape-1.5.2/spec/shared/versioning_examples.rb0000644000004100000410000001374214016744305021466 0ustar www-datawww-data# frozen_string_literal: true shared_examples_for 'versioning' do it 'sets the API version' do subject.format :txt subject.version 'v1', macro_options subject.get :hello do "Version: #{request.env['api.version']}" end versioned_get '/hello', 'v1', **macro_options expect(last_response.body).to eql 'Version: v1' end it 'adds the prefix before the API version' do subject.format :txt subject.prefix 'api' subject.version 'v1', macro_options subject.get :hello do "Version: #{request.env['api.version']}" end versioned_get '/hello', 'v1', **macro_options.merge(prefix: 'api') expect(last_response.body).to eql 'Version: v1' end it 'is able to specify version as a nesting' do subject.version 'v2', macro_options subject.get '/awesome' do 'Radical' end subject.version 'v1', macro_options do get '/legacy' do 'Totally' end end versioned_get '/awesome', 'v1', **macro_options expect(last_response.status).to eql 404 versioned_get '/awesome', 'v2', **macro_options expect(last_response.status).to eql 200 versioned_get '/legacy', 'v1', **macro_options expect(last_response.status).to eql 200 versioned_get '/legacy', 'v2', **macro_options expect(last_response.status).to eql 404 end it 'is able to specify multiple versions' do subject.version 'v1', 'v2', macro_options subject.get 'awesome' do 'I exist' end versioned_get '/awesome', 'v1', **macro_options expect(last_response.status).to eql 200 versioned_get '/awesome', 'v2', **macro_options expect(last_response.status).to eql 200 versioned_get '/awesome', 'v3', **macro_options expect(last_response.status).to eql 404 end context 'with different versions for the same endpoint' do context 'without a prefix' do it 'allows the same endpoint to be implemented' do subject.format :txt subject.version 'v2', macro_options subject.get 'version' do request.env['api.version'] end subject.version 'v1', macro_options do get 'version' do 'version ' + request.env['api.version'] end end versioned_get '/version', 'v2', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') versioned_get '/version', 'v1', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('version v1') end end context 'with a prefix' do it 'allows the same endpoint to be implemented' do subject.format :txt subject.prefix 'api' subject.version 'v2', macro_options subject.get 'version' do request.env['api.version'] end subject.version 'v1', macro_options do get 'version' do 'version ' + request.env['api.version'] end end versioned_get '/version', 'v1', **macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('version v1') versioned_get '/version', 'v2', **macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') end end end context 'with before block defined within a version block' do it 'calls before block that is defined within the version block' do subject.format :txt subject.prefix 'api' subject.version 'v2', macro_options do before do @output ||= 'v2-' end get 'version' do @output += 'version' end end subject.version 'v1', macro_options do before do @output ||= 'v1-' end get 'version' do @output += 'version' end end versioned_get '/version', 'v1', **macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('v1-version') versioned_get '/version', 'v2', **macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2-version') end end it 'does not overwrite version parameter with API version' do subject.format :txt subject.version 'v1', macro_options subject.params { requires :version } subject.get :api_version_with_version_param do params[:version] end versioned_get '/api_version_with_version_param?version=1', 'v1', **macro_options expect(last_response.body).to eql '1' end context 'with catch-all' do let(:options) { macro_options } let(:v1) do klass = Class.new(Grape::API) klass.version 'v1', options klass.get 'version' do 'v1' end klass end let(:v2) do klass = Class.new(Grape::API) klass.version 'v2', options klass.get 'version' do 'v2' end klass end before do subject.format :txt subject.mount v1 subject.mount v2 subject.route :any, '*path' do params[:path] end end context 'v1' do it 'finds endpoint' do versioned_get '/version', 'v1', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('v1') end it 'finds catch all' do versioned_get '/whatever', 'v1', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to end_with 'whatever' end end context 'v2' do it 'finds endpoint' do versioned_get '/version', 'v2', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') end it 'finds catch all' do versioned_get '/whatever', 'v2', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to end_with 'whatever' end end end end grape-1.5.2/spec/spec_helper.rb0000644000004100000410000000175514016744305016431 0ustar www-datawww-data# frozen_string_literal: true $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support')) require 'grape' require 'rubygems' require 'bundler' Bundler.require :default, :test Dir["#{File.dirname(__FILE__)}/support/*.rb"].each do |file| require file end eager_load! # The default value for this setting is true in a standard Rails app, # so it should be set to true here as well to reflect that. I18n.enforce_available_locales = true RSpec.configure do |config| config.include Rack::Test::Methods config.include Spec::Support::Helpers config.raise_errors_for_deprecations! config.filter_run_when_matching :focus config.warnings = true config.before(:each) { Grape::Util::InheritableSetting.reset_global! } # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' end require 'coveralls' Coveralls.wear! grape-1.5.2/spec/support/0000755000004100000410000000000014016744305015317 5ustar www-datawww-datagrape-1.5.2/spec/support/content_type_helpers.rb0000644000004100000410000000060514016744305022102 0ustar www-datawww-data# frozen_string_literal: true module Spec module Support module Helpers %w[put patch post delete].each do |method| define_method :"#{method}_with_json" do |uri, params = {}, env = {}, &block| params = params.to_json env['CONTENT_TYPE'] ||= 'application/json' send(method, uri, params, env, &block) end end end end end grape-1.5.2/spec/support/basic_auth_encode_helpers.rb0000644000004100000410000000033114016744305023002 0ustar www-datawww-data# frozen_string_literal: true module Spec module Support module Helpers def encode_basic_auth(username, password) 'Basic ' + Base64.encode64("#{username}:#{password}") end end end end grape-1.5.2/spec/support/eager_load.rb0000644000004100000410000000116514016744305017731 0ustar www-datawww-data# frozen_string_literal: true # Grape uses autoload https://api.rubyonrails.org/classes/ActiveSupport/Autoload.html. # When a class/module get added to the list, ActiveSupport doesn't check whether it really exists. # This method loads all classes/modules defined via autoload to be sure only existing # classes/modules were listed. def eager_load!(scope = Grape) # get modules scope.constants.each do |const_name| const = scope.const_get(const_name) next unless const.respond_to?(:eager_load!) const.eager_load! # check its modules, they might need to be loaded as well. eager_load!(const) end end grape-1.5.2/spec/support/endpoint_faker.rb0000644000004100000410000000074514016744305020642 0ustar www-datawww-data# frozen_string_literal: true module Spec module Support class EndpointFaker class FakerAPI < Grape::API get '/' do end end def initialize(app, endpoint = FakerAPI.endpoints.first) @app = app @endpoint = endpoint end def call(env) @endpoint.instance_exec do @request = Grape::Request.new(env.dup) end @app.call(env.merge('api.endpoint' => @endpoint)) end end end end grape-1.5.2/spec/support/file_streamer.rb0000644000004100000410000000032714016744305020467 0ustar www-datawww-data# frozen_string_literal: true class FileStreamer def initialize(file_path) @file_path = file_path end def each(&blk) File.open(@file_path, 'rb') do |file| file.each(10, &blk) end end end grape-1.5.2/spec/support/integer_helpers.rb0000644000004100000410000000033014016744305021017 0ustar www-datawww-data# frozen_string_literal: true module Spec module Support module Helpers INTEGER_CLASS_NAME = 0.to_i.class.to_s.freeze def integer_class_name INTEGER_CLASS_NAME end end end end grape-1.5.2/spec/support/versioned_helpers.rb0000644000004100000410000000341414016744305021366 0ustar www-datawww-data# frozen_string_literal: true # Versioning module Spec module Support module Helpers # Returns the path with options[:version] prefixed if options[:using] is :path. # Returns normal path otherwise. def versioned_path(**options) case options[:using] when :path File.join('/', options[:prefix] || '', options[:version], options[:path]) when :param File.join('/', options[:prefix] || '', options[:path]) when :header File.join('/', options[:prefix] || '', options[:path]) when :accept_version_header File.join('/', options[:prefix] || '', options[:path]) else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") end end def versioned_headers(**options) case options[:using] when :path {} # no-op when :param {} # no-op when :header { 'HTTP_ACCEPT' => [ "application/vnd.#{options[:vendor]}-#{options[:version]}", options[:format] ].compact.join('+') } when :accept_version_header { 'HTTP_ACCEPT_VERSION' => options[:version].to_s } else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") end end def versioned_get(path, version_name, **version_options) path = versioned_path(**version_options.merge(version: version_name, path: path)) headers = versioned_headers(**version_options.merge(version: version_name)) params = {} params = { version_options[:parameter] => version_name } if version_options[:using] == :param get path, params, headers end end end end grape-1.5.2/spec/support/chunks.rb0000644000004100000410000000031614016744305017137 0ustar www-datawww-data# frozen_string_literal: true module Chunks def read_chunks(body) buffer = [] body.each { |chunk| buffer << chunk } buffer end end RSpec.configure do |config| config.include Chunks end grape-1.5.2/spec/integration/0000755000004100000410000000000014016744305016126 5ustar www-datawww-datagrape-1.5.2/spec/integration/multi_xml/0000755000004100000410000000000014016744305020140 5ustar www-datawww-datagrape-1.5.2/spec/integration/multi_xml/xml_spec.rb0000644000004100000410000000023114016744305022273 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Xml do it 'uses multi_xml' do expect(Grape::Xml).to eq(::MultiXml) end end grape-1.5.2/spec/integration/eager_load/0000755000004100000410000000000014016744305020210 5ustar www-datawww-datagrape-1.5.2/spec/integration/eager_load/eager_load_spec.rb0000644000004100000410000000053114016744305023630 0ustar www-datawww-data# frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) require 'grape' describe Grape do it 'eager_load!' do require 'grape/eager_load' expect { Grape.eager_load! }.to_not raise_error end it 'compile!' do expect { Class.new(Grape::API).compile! }.to_not raise_error end end grape-1.5.2/spec/integration/multi_json/0000755000004100000410000000000014016744305020311 5ustar www-datawww-datagrape-1.5.2/spec/integration/multi_json/json_spec.rb0000644000004100000410000000023514016744305022621 0ustar www-datawww-data# frozen_string_literal: true require 'spec_helper' describe Grape::Json do it 'uses multi_json' do expect(Grape::Json).to eq(::MultiJson) end end grape-1.5.2/CHANGELOG.md0000644000004100000410000025374114016744305014476 0ustar www-datawww-data### 1.5.2 (2021/02/06) #### Features * [#2157](https://github.com/ruby-grape/grape/pull/2157): Custom types can set a message to be used in the response when invalid - [@dnesteryuk](https://github.com/dnesteryuk). * [#2145](https://github.com/ruby-grape/grape/pull/2145): Ruby 3.0 compatibility - [@ericproulx](https://github.com/ericproulx). * [#2143](https://github.com/ruby-grape/grape/pull/2143): Enable GitHub Actions with updated RuboCop and Danger - [@anakinj](https://github.com/anakinj). #### Fixes * [#2144](https://github.com/ruby-grape/grape/pull/2144): Fix compatibility issue with activesupport 6.1 and XML serialization of arrays - [@anakinj](https://github.com/anakinj). * [#2137](https://github.com/ruby-grape/grape/pull/2137): Fix typos - [@johnny-miyake](https://github.com/johnny-miyake). * [#2131](https://github.com/ruby-grape/grape/pull/2131): Fix Ruby 2.7 keyword deprecation warning in validators/coerce - [@K0H205](https://github.com/K0H205). * [#2132](https://github.com/ruby-grape/grape/pull/2132): Use #ruby2_keywords for correct delegation on Ruby <= 2.6, 2.7 and 3 - [@eregon](https://github.com/eregon). * [#2152](https://github.com/ruby-grape/grape/pull/2152): Fix configuration method inside namespaced params - [@fsainz](https://github.com/fsainz). ### 1.5.1 (2020/11/15) #### Fixes * [#2129](https://github.com/ruby-grape/grape/pull/2129): Fix validation error when Required Array nested inside an optional array, for Multiparam validators - [@dwhenry](https://github.com/dwhenry). * [#2128](https://github.com/ruby-grape/grape/pull/2128): Fix validation error when Required Array nested inside an optional array - [@dwhenry](https://github.com/dwhenry). * [#2127](https://github.com/ruby-grape/grape/pull/2127): Fix a performance issue with dependent params - [@dnesteryuk](https://github.com/dnesteryuk). * [#2126](https://github.com/ruby-grape/grape/pull/2126): Fix warnings about redefined attribute accessors in `AttributeTranslator` - [@samsonjs](https://github.com/samsonjs). * [#2121](https://github.com/ruby-grape/grape/pull/2121): Fix 2.7 deprecation warning in validator_factory - [@Legogris](https://github.com/Legogris). * [#2115](https://github.com/ruby-grape/grape/pull/2115): Fix declared_params regression with multiple allowed types - [@stanhu](https://github.com/stanhu). * [#2123](https://github.com/ruby-grape/grape/pull/2123): Fix 2.7 deprecation warning in middleware/stack - [@Legogris](https://github.com/Legogris). ### 1.5.0 (2020/10/05) #### Fixes * [#2104](https://github.com/ruby-grape/grape/pull/2104): Fix Ruby 2.7 keyword deprecation warning - [@stanhu](https://github.com/stanhu). * [#2103](https://github.com/ruby-grape/grape/pull/2103): Ensure complete declared params structure is present - [@tlconnor](https://github.com/tlconnor). * [#2099](https://github.com/ruby-grape/grape/pull/2099): Added truffleruby to Travis-CI - [@gogainda](https://github.com/gogainda). * [#2089](https://github.com/ruby-grape/grape/pull/2089): Specify order of mounting Grape with Rack::Cascade in README - [@jonmchan](https://github.com/jonmchan). * [#2088](https://github.com/ruby-grape/grape/pull/2088): Set `Cache-Control` header only for streamed responses - [@stanhu](https://github.com/stanhu). * [#2092](https://github.com/ruby-grape/grape/pull/2092): Correct an example params in Include Missing doc - [@huyvohcmc](https://github.com/huyvohcmc). * [#2091](https://github.com/ruby-grape/grape/pull/2091): Fix ruby 2.7 keyword deprecations - [@dim](https://github.com/dim). * [#2097](https://github.com/ruby-grape/grape/pull/2097): Skip to set default value unless `meets_dependency?` - [@wanabe](https://github.com/wanabe). * [#2096](https://github.com/ruby-grape/grape/pull/2096): Fix redundant dependency check - [@braktar](https://github.com/braktar). * [#2096](https://github.com/ruby-grape/grape/pull/2098): Fix nested coercion - [@braktar](https://github.com/braktar). * [#2102](https://github.com/ruby-grape/grape/pull/2102): Fix retaining setup blocks when remounting APIs - [@jylamont](https://github.com/jylamont). ### 1.4.0 (2020/07/10) #### Features * [#1520](https://github.com/ruby-grape/grape/pull/1520): Un-deprecate stream-like objects - [@urkle](https://github.com/urkle). * [#2060](https://github.com/ruby-grape/grape/pull/2060): Drop support for Ruby 2.4 - [@dblock](https://github.com/dblock). * [#2060](https://github.com/ruby-grape/grape/pull/2060): Upgraded Rubocop to 0.84.0 - [@dblock](https://github.com/dblock). * [#2077](https://github.com/ruby-grape/grape/pull/2077): Simplify logic for defining declared params - [@dnesteryuk](https://github.com/dnesteryuk). * [#2076](https://github.com/ruby-grape/grape/pull/2076): Make route information available for hooks when the automatically generated endpoints are invoked - [@anakinj](https://github.com/anakinj). #### Fixes * [#2067](https://github.com/ruby-grape/grape/pull/2067): Coerce empty String to `nil` for all primitive types except `String` - [@petekinnecom](https://github.com/petekinnecom). * [#2064](https://github.com/ruby-grape/grape/pull/2064): Fix Ruby 2.7 deprecation warning in `Grape::Middleware::Base#initialize` - [@skarger](https://github.com/skarger). * [#2072](https://github.com/ruby-grape/grape/pull/2072): Fix `Grape.eager_load!` and `compile!` - [@stanhu](https://github.com/stanhu). * [#2084](https://github.com/ruby-grape/grape/pull/2084): Fix memory leak in path normalization - [@fcheung](https://github.com/fcheung). ### 1.3.3 (2020/05/23) #### Features * [#2048](https://github.com/ruby-grape/grape/issues/2034): Grape Enterprise support is now available [via TideLift](https://tidelift.com/subscription/request-a-demo?utm_source=rubygems-grape&utm_medium=referral&utm_campaign=enterprise) - [@dblock](https://github.com/dblock). * [#2039](https://github.com/ruby-grape/grape/pull/2039): Travis - update rails versions - [@ericproulx](https://github.com/ericproulx). * [#2038](https://github.com/ruby-grape/grape/pull/2038): Travis - update ruby versions - [@ericproulx](https://github.com/ericproulx). * [#2050](https://github.com/ruby-grape/grape/pull/2050): Refactor route public_send to AttributeTranslator - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2049](https://github.com/ruby-grape/grape/pull/2049): Coerce an empty string to nil in case of the bool type - [@dnesteryuk](https://github.com/dnesteryuk). * [#2043](https://github.com/ruby-grape/grape/pull/2043): Modify declared for nested array and hash - [@kadotami](https://github.com/kadotami). * [#2040](https://github.com/ruby-grape/grape/pull/2040): Fix a regression with Array of type nil - [@ericproulx](https://github.com/ericproulx). * [#2054](https://github.com/ruby-grape/grape/pull/2054): Coercing of nested arrays - [@dnesteryuk](https://github.com/dnesteryuk). * [#2050](https://github.com/ruby-grape/grape/pull/2053): Fix broken multiple mounts - [@Jack12816](https://github.com/Jack12816). ### 1.3.2 (2020/04/12) #### Features * [#2020](https://github.com/ruby-grape/grape/pull/2020): Reduce array allocation - [@ericproulx](https://github.com/ericproulx). * [#2015](https://github.com/ruby-grape/grape/pull/2014): Reduce MatchData allocation - [@ericproulx](https://github.com/ericproulx). * [#2014](https://github.com/ruby-grape/grape/pull/2014): Reduce total allocated arrays - [@ericproulx](https://github.com/ericproulx). * [#2011](https://github.com/ruby-grape/grape/pull/2011): Reduce total retained regexes - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2033](https://github.com/ruby-grape/grape/pull/2033): Ensure `Float` params are correctly coerced to `BigDecimal` - [@tlconnor](https://github.com/tlconnor). * [#2031](https://github.com/ruby-grape/grape/pull/2031): Fix a regression with an array of a custom type - [@dnesteryuk](https://github.com/dnesteryuk). * [#2026](https://github.com/ruby-grape/grape/pull/2026): Fix a regression in `coerce_with` when coercion returns `nil` - [@misdoro](https://github.com/misdoro). * [#2025](https://github.com/ruby-grape/grape/pull/2025): Fix Decimal type category - [@kdoya](https://github.com/kdoya). * [#2019](https://github.com/ruby-grape/grape/pull/2019): Avoid coercing parameter with multiple types to an empty Array - [@stanhu](https://github.com/stanhu). ### 1.3.1 (2020/03/11) #### Features * [#2005](https://github.com/ruby-grape/grape/pull/2005): Content types registrable - [@ericproulx](https://github.com/ericproulx). * [#2003](https://github.com/ruby-grape/grape/pull/2003): Upgraded Rubocop to 0.80.1 - [@ericproulx](https://github.com/ericproulx). * [#2002](https://github.com/ruby-grape/grape/pull/2002): Objects allocation optimization (lazy_lookup) - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2006](https://github.com/ruby-grape/grape/pull/2006): Fix explicit rescue StandardError - [@ericproulx](https://github.com/ericproulx). * [#2004](https://github.com/ruby-grape/grape/pull/2004): Rubocop fixes - [@ericproulx](https://github.com/ericproulx). * [#1995](https://github.com/ruby-grape/grape/pull/1995): Fix: "undefined instance variables" and "method redefined" warnings - [@nbeyer](https://github.com/nbeyer). * [#1994](https://github.com/ruby-grape/grape/pull/1993): Fix typos in README - [@bellmyer](https://github.com/bellmyer). * [#1993](https://github.com/ruby-grape/grape/pull/1993): Lazy join allow header - [@ericproulx](https://github.com/ericproulx). * [#1987](https://github.com/ruby-grape/grape/pull/1987): Re-add exactly_one_of mutually exclusive error message - [@ZeroInputCtrl](https://github.com/ZeroInputCtrl). * [#1977](https://github.com/ruby-grape/grape/pull/1977): Skip validation for a file if it is optional and nil - [@dnesteryuk](https://github.com/dnesteryuk). * [#1976](https://github.com/ruby-grape/grape/pull/1976): Ensure classes/modules listed for autoload really exist - [@dnesteryuk](https://github.com/dnesteryuk). * [#1971](https://github.com/ruby-grape/grape/pull/1971): Fix BigDecimal coercion - [@FlickStuart](https://github.com/FlickStuart). * [#1968](https://github.com/ruby-grape/grape/pull/1968): Fix args forwarding in Grape::Middleware::Stack#merge_with for ruby 2.7.0 - [@dm1try](https://github.com/dm1try). * [#1988](https://github.com/ruby-grape/grape/pull/1988): Refactor the full_messages method and stop overriding full_message - [@hosseintoussi](https://github.com/hosseintoussi). * [#1956](https://github.com/ruby-grape/grape/pull/1956): Comply with Rack spec, fix `undefined method [] for nil:NilClass` error when upgrading Rack - [@ioquatix](https://github.com/ioquatix). ### 1.3.0 (2020/01/11) #### Features * [#1949](https://github.com/ruby-grape/grape/pull/1949): Add support for Ruby 2.7 - [@nbulaj](https://github.com/nbulaj). * [#1948](https://github.com/ruby-grape/grape/pull/1948): Relax `dry-types` dependency version - [@nbulaj](https://github.com/nbulaj). * [#1944](https://github.com/ruby-grape/grape/pull/1944): Reduces `attribute_translator` string allocations - [@ericproulx](https://github.com/ericproulx). * [#1943](https://github.com/ruby-grape/grape/pull/1943): Reduces number of regex string allocations - [@ericproulx](https://github.com/ericproulx). * [#1942](https://github.com/ruby-grape/grape/pull/1942): Optimizes retained memory methods - [@ericproulx](https://github.com/ericproulx). * [#1941](https://github.com/ruby-grape/grape/pull/1941): Adds frozen string literal - [@ericproulx](https://github.com/ericproulx). * [#1940](https://github.com/ruby-grape/grape/pull/1940): Gets rid of a needless step in `HashWithIndifferentAccess` - [@dnesteryuk](https://github.com/dnesteryuk). * [#1938](https://github.com/ruby-grape/grape/pull/1938): Adds project metadata to the gemspec - [@orien](https://github.com/orien). * [#1920](https://github.com/ruby-grape/grape/pull/1920): Replaces Virtus with dry-types - [@dnesteryuk](https://github.com/dnesteryuk). * [#1930](https://github.com/ruby-grape/grape/pull/1930): Moves block call to separate method so it can be spied on - [@estolfo](https://github.com/estolfo). #### Fixes * [#1965](https://github.com/ruby-grape/grape/pull/1965): Fix typos in README - [@davidalee](https://github.com/davidalee). * [#1963](https://github.com/ruby-grape/grape/pull/1963): The values validator must properly work with booleans - [@dnesteryuk](https://github.com/dnesteryuk). * [#1950](https://github.com/ruby-grape/grape/pull/1950): Consider the allow_blank option in the values validator - [@dnesteryuk](https://github.com/dnesteryuk). * [#1947](https://github.com/ruby-grape/grape/pull/1947): Careful check for empty params - [@dnesteryuk](https://github.com/dnesteryuk). * [#1931](https://github.com/ruby-grape/grape/pull/1946): Fixes issue when using namespaces in `Grape::API::Instance` mounted directly - [@myxoh](https://github.com/myxoh). ### 1.2.5 (2019/12/01) #### Features * [#1931](https://github.com/ruby-grape/grape/pull/1931): Introduces LazyBlock to generate expressions that will executed at mount time - [@myxoh](https://github.com/myxoh). * [#1918](https://github.com/ruby-grape/grape/pull/1918): Helper methods to access controller context from middleware - [@NikolayRys](https://github.com/NikolayRys). * [#1915](https://github.com/ruby-grape/grape/pull/1915): Micro optimizations in allocating hashes and arrays - [@dnesteryuk](https://github.com/dnesteryuk). * [#1904](https://github.com/ruby-grape/grape/pull/1904): Allows Grape to load files on startup rather than on the first call - [@myxoh](https://github.com/myxoh). * [#1907](https://github.com/ruby-grape/grape/pull/1907): Adds outside configuration to Grape with `configure` - [@unleashy](https://github.com/unleashy). * [#1914](https://github.com/ruby-grape/grape/pull/1914): Run specs in random order - [@splattael](https://github.com/splattael). #### Fixes * [#1917](https://github.com/ruby-grape/grape/pull/1917): Update access to rack constant - [@NikolayRys](https://github.com/NikolayRys). * [#1916](https://github.com/ruby-grape/grape/pull/1916): Drop old appraisals - [@NikolayRys](https://github.com/NikolayRys). * [#1911](https://github.com/ruby-grape/grape/pull/1911): Make sure `Grape::Valiations::AtLeastOneOfValidator` properly treats nested params in errors - [@dnesteryuk](https://github.com/dnesteryuk). * [#1893](https://github.com/ruby-grape/grape/pull/1893): Allows `Grape::API` to behave like a Rack::app in some instances where it was misbehaving - [@myxoh](https://github.com/myxoh). * [#1898](https://github.com/ruby-grape/grape/pull/1898): Refactor `ValidatorFactory` to improve memory allocation - [@Bhacaz](https://github.com/Bhacaz). * [#1900](https://github.com/ruby-grape/grape/pull/1900): Define boolean for `Grape::Api::Instance` - [@Bhacaz](https://github.com/Bhacaz). * [#1903](https://github.com/ruby-grape/grape/pull/1903): Allow nested params renaming (Hash/Array) - [@bikolya](https://github.com/bikolya). * [#1913](https://github.com/ruby-grape/grape/pull/1913): Fix multiple params validators to return correct messages for nested params - [@bikolya](https://github.com/bikolya). * [#1926](https://github.com/ruby-grape/grape/pull/1926): Fixes configuration within given or mounted blocks - [@myxoh](https://github.com/myxoh). * [#1937](https://github.com/ruby-grape/grape/pull/1937): Fix bloat in released gem - [@dblock](https://github.com/dblock). ### 1.2.4 (2019/06/13) #### Features * [#1888](https://github.com/ruby-grape/grape/pull/1888): Makes the `configuration` hash widely available - [@myxoh](https://github.com/myxoh). * [#1864](https://github.com/ruby-grape/grape/pull/1864): Adds `finally` on the API - [@myxoh](https://github.com/myxoh). * [#1869](https://github.com/ruby-grape/grape/pull/1869): Fix issue with empty headers after `error!` method call - [@anaumov](https://github.com/anaumov). #### Fixes * [#1868](https://github.com/ruby-grape/grape/pull/1868): Fix NoMethodError with none hash params - [@ksss](https://github.com/ksss). * [#1876](https://github.com/ruby-grape/grape/pull/1876): Fix const errors being hidden by bug in `const_missing` - [@dandehavilland](https://github.com/dandehavilland). ### 1.2.3 (2019/01/16) #### Features * [#1850](https://github.com/ruby-grape/grape/pull/1850): Adds `same_as` validator - [@glaucocustodio](https://github.com/glaucocustodio). * [#1833](https://github.com/ruby-grape/grape/pull/1833): Allows to set the `ParamBuilder` globally - [@myxoh](https://github.com/myxoh). #### Fixes * [#1852](https://github.com/ruby-grape/grape/pull/1852): `allow_blank` called after `as` when the original param is not blank - [@glaucocustodio](https://github.com/glaucocustodio). * [#1844](https://github.com/ruby-grape/grape/pull/1844): Enforce `:tempfile` to be a `Tempfile` object in `File` validator - [@Nyangawa](https://github.com/Nyangawa). ### 1.2.2 (2018/12/07) #### Features * [#1832](https://github.com/ruby-grape/grape/pull/1832): Support `body_name` in `desc` block - [@fotos](https://github.com/fotos). * [#1831](https://github.com/ruby-grape/grape/pull/1831): Support `security` in `desc` block - [@fotos](https://github.com/fotos). #### Fixes * [#1836](https://github.com/ruby-grape/grape/pull/1836): Fix: memory leak not releasing `call` method calls from setup - [@myxoh](https://github.com/myxoh). * [#1830](https://github.com/ruby-grape/grape/pull/1830), [#1829](https://github.com/ruby-grape/grape/issues/1829): Restores `self` sanity - [@myxoh](https://github.com/myxoh). ### 1.2.1 (2018/11/28) #### Fixes * [#1825](https://github.com/ruby-grape/grape/pull/1825): `to_s` on a mounted class now responses with the API name - [@myxoh](https://github.com/myxoh). ### 1.2.0 (2018/11/26) #### Features * [#1813](https://github.com/ruby-grape/grape/pull/1813): Add ruby 2.5 support, drop 2.2. Update rails version in travis - [@darren987469](https://github.com/darren987469). * [#1803](https://github.com/ruby-grape/grape/pull/1803): Adds the ability to re-mount all endpoints in any location - [@myxoh](https://github.com/myxoh). * [#1795](https://github.com/ruby-grape/grape/pull/1795): Fix vendor/subtype parsing of an invalid Accept header - [@bschmeck](https://github.com/bschmeck). * [#1791](https://github.com/ruby-grape/grape/pull/1791): Support `summary`, `hidden`, `deprecated`, `is_array`, `nickname`, `produces`, `consumes`, `tags` options in `desc` block - [@darren987469](https://github.com/darren987469). #### Fixes * [#1796](https://github.com/ruby-grape/grape/pull/1796): Fix crash when available locales are enforced but fallback locale unavailable - [@Morred](https://github.com/Morred). * [#1776](https://github.com/ruby-grape/grape/pull/1776): Validate response returned by the exception handler - [@darren987469](https://github.com/darren987469). * [#1787](https://github.com/ruby-grape/grape/pull/1787): Add documented but not implemented ability to `.insert` a middleware in the stack - [@michaellennox](https://github.com/michaellennox). * [#1788](https://github.com/ruby-grape/grape/pull/1788): Fix route requirements bug - [@darren987469](https://github.com/darren987469), [@darrellnash](https://github.com/darrellnash). * [#1810](https://github.com/ruby-grape/grape/pull/1810): Fix support in `given` for aliased params - [@darren987469](https://github.com/darren987469). * [#1811](https://github.com/ruby-grape/grape/pull/1811): Support nested dependent parameters - [@darren987469](https://github.com/darren987469), [@andreacfm](https://github.com/andreacfm). * [#1822](https://github.com/ruby-grape/grape/pull/1822): Raise validation error when optional hash type parameter is received string type value and exactly_one_of be used - [@woshidan](https://github.com/woshidan). ### 1.1.0 (2018/8/4) #### Features * [#1759](https://github.com/ruby-grape/grape/pull/1759): Instrument serialization as `'format_response.grape'` - [@zvkemp](https://github.com/zvkemp). #### Fixes * [#1762](https://github.com/ruby-grape/grape/pull/1763): Fix unsafe HTML rendering on errors - [@ctennis](https://github.com/ctennis). * [#1759](https://github.com/ruby-grape/grape/pull/1759): Update appraisal for rails_edge - [@zvkemp](https://github.com/zvkemp). * [#1758](https://github.com/ruby-grape/grape/pull/1758): Fix expanding load_path in gemspec - [@2maz](https://github.com/2maz). * [#1765](https://github.com/ruby-grape/grape/pull/1765): Use 415 when request body is of an unsupported media type - [@jdmurphy](https://github.com/jdmurphy). * [#1771](https://github.com/ruby-grape/grape/pull/1771): Fix param aliases with 'given' blocks - [@jereynolds](https://github.com/jereynolds). ### 1.0.3 (2018/4/23) #### Fixes * [#1755](https://github.com/ruby-grape/grape/pull/1755): Fix shared params with exactly_one_of - [@milgner](https://github.com/milgner). * [#1740](https://github.com/ruby-grape/grape/pull/1740): Fix dependent parameter validation using `given` when parameter is a `Hash` - [@jvortmann](https://github.com/jvortmann). * [#1737](https://github.com/ruby-grape/grape/pull/1737): Fix translating error when passing symbols as params in custom validations - [@mlzhuyi](https://github.com/mlzhuyi). * [#1749](https://github.com/ruby-grape/grape/pull/1749): Allow rescue from non-`StandardError` exceptions - [@dm1try](https://github.com/dm1try). * [#1750](https://github.com/ruby-grape/grape/pull/1750): Fix a circular dependency warning due to router being loaded by API - [@salasrod](https://github.com/salasrod). * [#1752](https://github.com/ruby-grape/grape/pull/1752): Fix `include_missing` behavior for aliased parameters - [@jonasoberschweiber](https://github.com/jonasoberschweiber). * [#1754](https://github.com/ruby-grape/grape/pull/1754): Allow rescue from non-`StandardError` exceptions to use default error handling - [@jelkster](https://github.com/jelkster). * [#1756](https://github.com/ruby-grape/grape/pull/1756): Allow custom Grape exception handlers when the built-in exception handling is enabled - [@soylent](https://github.com/soylent). ### 1.0.2 (2018/1/10) #### Features * [#1686](https://github.com/ruby-grape/grape/pull/1686): Avoid coercion of a value if it is valid - [@timothysu](https://github.com/timothysu). * [#1688](https://github.com/ruby-grape/grape/pull/1688): Removes yard docs - [@ramkumar-kr](https://github.com/ramkumar-kr). * [#1702](https://github.com/ruby-grape/grape/pull/1702): Added danger-toc, verify correct TOC in README - [@dblock](https://github.com/dblock). * [#1711](https://github.com/ruby-grape/grape/pull/1711): Automatically coerce arrays and sets of types that implement a `parse` method - [@dslh](https://github.com/dslh). #### Fixes * [#1710](https://github.com/ruby-grape/grape/pull/1710): Fix wrong transformation of empty Array in declared params - [@pablonahuelgomez](https://github.com/pablonahuelgomez). * [#1722](https://github.com/ruby-grape/grape/pull/1722): Fix catch-all hiding multiple versions of an endpoint after the first definition - [@zherr](https://github.com/zherr). * [#1724](https://github.com/ruby-grape/grape/pull/1724): Optional nested array validation - [@ericproulx](https://github.com/ericproulx). * [#1725](https://github.com/ruby-grape/grape/pull/1725): Fix `rescue_from :all` documentation - [@Jelkster](https://github.com/Jelkster). * [#1726](https://github.com/ruby-grape/grape/pull/1726): Improved startup performance during API method generation - [@jkowens](https://github.com/jkowens). * [#1727](https://github.com/ruby-grape/grape/pull/1727): Fix infinite loop when mounting endpoint with same superclass - [@jkowens](https://github.com/jkowens). ### 1.0.1 (2017/9/8) #### Features * [#1652](https://github.com/ruby-grape/grape/pull/1652): Add the original exception to the error_formatter the original exception - [@dcsg](https://github.com/dcsg). * [#1665](https://github.com/ruby-grape/grape/pull/1665): Make helpers available in subclasses - [@pablonahuelgomez](https://github.com/pablonahuelgomez). * [#1674](https://github.com/ruby-grape/grape/pull/1674): Add parameter alias (`as`) - [@glaucocustodio](https://github.com/glaucocustodio). #### Fixes * [#1652](https://github.com/ruby-grape/grape/pull/1652): Fix missing backtrace that was not being bubbled up to the `error_formatter` - [@dcsg](https://github.com/dcsg). * [#1661](https://github.com/ruby-grape/grape/pull/1661): Handle deeply-nested dependencies correctly - [@rnubel](https://github.com/rnubel), [@jnardone](https://github.com/jnardone). * [#1679](https://github.com/ruby-grape/grape/pull/1679): Treat StandardError from explicit values validator proc as false - [@jlfaber](https://github.com/jlfaber). ### 1.0.0 (2017/7/3) #### Features * [#1594](https://github.com/ruby-grape/grape/pull/1594): Replace `Hashie::Mash` parameters with `ActiveSupport::HashWithIndifferentAccess` - [@james2m](https://github.com/james2m), [@dblock](https://github.com/dblock). * [#1622](https://github.com/ruby-grape/grape/pull/1622): Add `except_values` validator to replace `except` option of `values` validator - [@jlfaber](https://github.com/jlfaber). * [#1635](https://github.com/ruby-grape/grape/pull/1635): Instrument validators with ActiveSupport::Notifications - [@ktimothy](https://github.com/ktimothy). * [#1646](https://github.com/ruby-grape/grape/pull/1646): Add ability to include an array of modules as helpers - [@pablonahuelgomez](https://github.com/pablonahuelgomez). * [#1623](https://github.com/ruby-grape/grape/pull/1623): Removed `multi_json` and `multi_xml` dependencies - [@dblock](https://github.com/dblock). * [#1650](https://github.com/ruby-grape/grape/pull/1650): Add extra specs for Boolean type field - [@tiarly](https://github.com/tiarly). #### Fixes * [#1648](https://github.com/ruby-grape/grape/pull/1631): Declared now returns declared options using the class that params is set to use - [@thogg4](https://github.com/thogg4). * [#1632](https://github.com/ruby-grape/grape/pull/1632): Silence warnings - [@thogg4](https://github.com/thogg4). * [#1615](https://github.com/ruby-grape/grape/pull/1615): Fix default and type validator when values is a Hash with no value attribute - [@jlfaber](https://github.com/jlfaber). * [#1625](https://github.com/ruby-grape/grape/pull/1625): Handle `given` correctly when nested in Array params - [@rnubel](https://github.com/rnubel), [@avellable](https://github.com/avellable). * [#1649](https://github.com/ruby-grape/grape/pull/1649): Don't share validator instances between requests - [@anakinj](https://github.com/anakinj). ### 0.19.2 (2017/4/12) #### Features * [#1555](https://github.com/ruby-grape/grape/pull/1555): Added code coverage w/Coveralls - [@dblock](https://github.com/dblock). * [#1568](https://github.com/ruby-grape/grape/pull/1568): Add `proc` option to `values` validator to allow custom checks - [@jlfaber](https://github.com/jlfaber). * [#1575](https://github.com/ruby-grape/grape/pull/1575): Include nil values for missing nested params in declared - [@thogg4](https://github.com/thogg4). * [#1585](https://github.com/ruby-grape/grape/pull/1585): Bugs in declared method - make sure correct options var is used and respect include missing for non children params - [@thogg4](https://github.com/thogg4). #### Fixes * [#1570](https://github.com/ruby-grape/grape/pull/1570): Make versioner consider the mount destination path - [@namusyaka](https://github.com/namusyaka). * [#1579](https://github.com/ruby-grape/grape/pull/1579): Fix delete status with a return value - [@eproulx-petalmd](https://github.com/eproulx-petalmd). * [#1559](https://github.com/ruby-grape/grape/pull/1559): You can once again pass `nil` to optional attributes with `values` validation set - [@ghiculescu](https://github.com/ghiculescu). * [#1562](https://github.com/ruby-grape/grape/pull/1562): Fix rainbow gem installation failure above ruby 2.3.3 on travis-ci - [@brucehsu](https://github.com/brucehsu). * [#1561](https://github.com/ruby-grape/grape/pull/1561): Fix performance issue introduced by duplicated calls in StackableValue#[] - [@brucehsu](https://github.com/brucehsu). * [#1564](https://github.com/ruby-grape/grape/pull/1564): Fix declared params bug with nested namespaces - [@bmarini](https://github.com/bmarini). * [#1567](https://github.com/ruby-grape/grape/pull/1567): Fix values validator when value is empty array and apply except to input array - [@jlfaber](https://github.com/jlfaber). * [#1569](https://github.com/ruby-grape/grape/pull/1569), [#1511](https://github.com/ruby-grape/grape/issues/1511): Upgrade mustermann-grape to 1.0.0 - [@namusyaka](https://github.com/namusyaka). * [#1589](https://github.com/ruby-grape/grape/pull/1589): [#726](https://github.com/ruby-grape/grape/issues/726): Use default_format when Content-type is missing and respond with 406 when Content-type is invalid - [@inclooder](https://github.com/inclooder). ### 0.19.1 (2017/1/9) #### Features * [#1536](https://github.com/ruby-grape/grape/pull/1536): Updated `invalid_versioner_option` translation - [@Lavode](https://github.com/Lavode). * [#1543](https://github.com/ruby-grape/grape/pull/1543): Added support for ruby 2.4 - [@LeFnord](https://github.com/LeFnord), [@namusyaka](https://github.com/namusyaka). #### Fixes * [#1548](https://github.com/ruby-grape/grape/pull/1548): Fix: avoid failing even if given path does not match with prefix - [@thomas-peyric](https://github.com/thomas-peyric), [@namusyaka](https://github.com/namusyaka). * [#1550](https://github.com/ruby-grape/grape/pull/1550): Fix: return 200 as default status for DELETE - [@jthornec](https://github.com/jthornec). ### 0.19.0 (2016/12/18) #### Features * [#1503](https://github.com/ruby-grape/grape/pull/1503): Allowed use of regexp validator with arrays - [@akoltun](https://github.com/akoltun). * [#1507](https://github.com/ruby-grape/grape/pull/1507): Added group attributes for parameter definitions - [@304](https://github.com/304). * [#1532](https://github.com/ruby-grape/grape/pull/1532): Set 204 as default status for DELETE - [@LeFnord](https://github.com/LeFnord). #### Fixes * [#1505](https://github.com/ruby-grape/grape/pull/1505): Run `before` and `after` callbacks, but skip the rest when handling OPTIONS - [@jlfaber](https://github.com/jlfaber). * [#1517](https://github.com/ruby-grape/grape/pull/1517), [#1089](https://github.com/ruby-grape/grape/pull/1089): Fix: priority of ANY routes - [@namusyaka](https://github.com/namusyaka), [@wagenet](https://github.com/wagenet). * [#1512](https://github.com/ruby-grape/grape/pull/1512): Fix: deeply nested parameters are included within `#declared(params)` - [@krbs](https://github.com/krbs). * [#1510](https://github.com/ruby-grape/grape/pull/1510): Fix: inconsistent validation for multiple parameters - [@dgasper](https://github.com/dgasper). * [#1526](https://github.com/ruby-grape/grape/pull/1526): Reduced warnings caused by instance variables not initialized - [@cpetschnig](https://github.com/cpetschnig). ### 0.18.0 (2016/10/7) #### Features * [#1480](https://github.com/ruby-grape/grape/pull/1480): Used the ruby-grape-danger gem for PR linting - [@dblock](https://github.com/dblock). * [#1486](https://github.com/ruby-grape/grape/pull/1486): Implemented except in values validator - [@jonmchan](https://github.com/jonmchan). * [#1470](https://github.com/ruby-grape/grape/pull/1470): Dropped support for Ruby 2.0 - [@namusyaka](https://github.com/namusyaka). * [#1490](https://github.com/ruby-grape/grape/pull/1490): Switched to Ruby-2.x+ syntax - [@namusyaka](https://github.com/namusyaka). * [#1499](https://github.com/ruby-grape/grape/pull/1499): Support `fail_fast` param validation option - [@dgasper](https://github.com/dgasper). #### Fixes * [#1498](https://github.com/ruby-grape/grape/pull/1498): Fix: skip validations in inactive given blocks - [@jlfaber](https://github.com/jlfaber). * [#1479](https://github.com/ruby-grape/grape/pull/1479): Fix: support inserting middleware before/after anonymous classes in the middleware stack - [@rosa](https://github.com/rosa). * [#1488](https://github.com/ruby-grape/grape/pull/1488): Fix: ensure calling before filters when receiving OPTIONS request - [@namusyaka](https://github.com/namusyaka), [@jlfaber](https://github.com/jlfaber). * [#1493](https://github.com/ruby-grape/grape/pull/1493): Fix: coercion and lambda fails params validation - [@jonmchan](https://github.com/jonmchan). ### 0.17.0 (2016/7/29) #### Features * [#1393](https://github.com/ruby-grape/grape/pull/1393): Middleware can be inserted before or after default Grape middleware - [@ridiculous](https://github.com/ridiculous). * [#1390](https://github.com/ruby-grape/grape/pull/1390): Allowed inserting middleware at arbitrary points in the middleware stack - [@rosa](https://github.com/rosa). * [#1366](https://github.com/ruby-grape/grape/pull/1366): Stored `message_key` on `Grape::Exceptions::Validation` - [@mkou](https://github.com/mkou). * [#1398](https://github.com/ruby-grape/grape/pull/1398): Added `rescue_from :grape_exceptions` - allow Grape to use the built-in `Grape::Exception` handing and use `rescue :all` behavior for everything else - [@mmclead](https://github.com/mmclead). * [#1443](https://github.com/ruby-grape/grape/pull/1443): Extended `given` to receive a `Proc` - [@glaucocustodio](https://github.com/glaucocustodio). * [#1455](https://github.com/ruby-grape/grape/pull/1455): Added an automated PR linter - [@orta](https://github.com/orta). #### Fixes * [#1463](https://github.com/ruby-grape/grape/pull/1463): Fix array indicies in error messages - [@ffloyd](https://github.com/ffloyd). * [#1465](https://github.com/ruby-grape/grape/pull/1465): Fix 'before' being called twice when using not allowed method - [@jsteinberg](https://github.com/jsteinberg). * [#1446](https://github.com/ruby-grape/grape/pull/1446): Fix for `env` inside `before` when using not allowed method - [@leifg](https://github.com/leifg). * [#1438](https://github.com/ruby-grape/grape/pull/1439): Try to dup non-frozen default params with each use - [@jlfaber](https://github.com/jlfaber). * [#1430](https://github.com/ruby-grape/grape/pull/1430): Fix for `declared(params)` inside `route_param` - [@Arkanain](https://github.com/Arkanain). * [#1405](https://github.com/ruby-grape/grape/pull/1405): Fix priority of `rescue_from` clauses applying - [@hedgesky](https://github.com/hedgesky). * [#1365](https://github.com/ruby-grape/grape/pull/1365): Fix finding exception handler in error middleware - [@ktimothy](https://github.com/ktimothy). * [#1380](https://github.com/ruby-grape/grape/pull/1380): Fix `allow_blank: false` for `Time` attributes with valid values causes `NoMethodError` - [@ipkes](https://github.com/ipkes). * [#1384](https://github.com/ruby-grape/grape/pull/1384): Fix parameter validation with an empty optional nested `Array` - [@ipkes](https://github.com/ipkes). * [#1414](https://github.com/ruby-grape/grape/pull/1414): Fix multiple version definitions for path versioning - [@304](https://github.com/304). * [#1415](https://github.com/ruby-grape/grape/pull/1415): Fix `declared(params, include_parent_namespaces: false)` - [@304](https://github.com/304). * [#1421](https://github.com/ruby-grape/grape/pull/1421): Avoid polluting `Grape::Middleware::Error` - [@namusyaka](https://github.com/namusyaka). * [#1422](https://github.com/ruby-grape/grape/pull/1422): Concat parent declared params with current one - [@plukevdh](https://github.com/plukevdh), [@rnubel](https://github.com/rnubel), [@namusyaka](https://github.com/namusyaka). ### 0.16.2 (2016/4/12) #### Features * [#1348](https://github.com/ruby-grape/grape/pull/1348): Fix global functions polluting Grape::API scope - [@dblock](https://github.com/dblock). * [#1357](https://github.com/ruby-grape/grape/pull/1357): Expose Route#options - [@namusyaka](https://github.com/namusyaka). #### Fixes * [#1357](https://github.com/ruby-grape/grape/pull/1357): Don't include fixed named captures as route params - [@namusyaka](https://github.com/namusyaka). * [#1359](https://github.com/ruby-grape/grape/pull/1359): Avoid evaluating the same route twice - [@namusyaka](https://github.com/namusyaka), [@dblock](https://github.com/dblock). * [#1361](https://github.com/ruby-grape/grape/pull/1361): Return 405 correctly even if version is using as header and wrong request method - [@namusyaka](https://github.com/namusyaka), [@dblock](https://github.com/dblock). ### 0.16.1 (2016/4/3) #### Features * [#1276](https://github.com/ruby-grape/grape/pull/1276): Replace rack-mount with new router - [@namusyaka](https://github.com/namusyaka). * [#1321](https://github.com/ruby-grape/grape/pull/1321): Serve files without using FileStreamer-like object - [@lfidnl](https://github.com/lfidnl). * [#1339](https://github.com/ruby-grape/grape/pull/1339): Implement Grape::API.recognize_path - [@namusyaka](https://github.com/namusyaka). #### Fixes * [#1325](https://github.com/ruby-grape/grape/pull/1325): Params: Fix coerce_with helper with Array types - [@ngonzalez](https://github.com/ngonzalez). * [#1326](https://github.com/ruby-grape/grape/pull/1326): Fix wrong behavior for OPTIONS and HEAD requests with catch-all - [@ekampp](https://github.com/ekampp), [@namusyaka](https://github.com/namusyaka). * [#1330](https://github.com/ruby-grape/grape/pull/1330): Add `register` keyword for adding customized parsers and formatters - [@namusyaka](https://github.com/namusyaka). * [#1336](https://github.com/ruby-grape/grape/pull/1336): Do not modify Hash argument to `error!` - [@tjwp](https://github.com/tjwp). ### 0.15.0 (2016/3/8) #### Features * [#1227](https://github.com/ruby-grape/grape/pull/1227): Store `message_key` on `Grape::Exceptions::Validation` - [@stjhimy](https://github.com/sthimy). * [#1232](https://github.com/ruby-grape/grape/pull/1232): Helpers are now available inside `rescue_from` - [@namusyaka](https://github.com/namusyaka). * [#1237](https://github.com/ruby-grape/grape/pull/1237): Allow multiple parameters in `given`, which behaves as if the scopes were nested in the inputted order - [@ochagata](https://github.com/ochagata). * [#1238](https://github.com/ruby-grape/grape/pull/1238): Call `after` of middleware on error - [@namusyaka](https://github.com/namusyaka). * [#1243](https://github.com/ruby-grape/grape/pull/1243): Add `header` support for middleware - [@namusyaka](https://github.com/namusyaka). * [#1252](https://github.com/ruby-grape/grape/pull/1252): Allow default to be a subset or equal to allowed values without raising IncompatibleOptionValues - [@jeradphelps](https://github.com/jeradphelps). * [#1255](https://github.com/ruby-grape/grape/pull/1255): Allow param type definition in `route_param` - [@namusyaka](https://github.com/namusyaka). * [#1257](https://github.com/ruby-grape/grape/pull/1257): Allow Proc, Symbol or String in `rescue_from with: ...` - [@namusyaka](https://github.com/namusyaka). * [#1280](https://github.com/ruby-grape/grape/pull/1280): Support `Rack::Sendfile` middleware - [@lfidnl](https://github.com/lfidnl). * [#1285](https://github.com/ruby-grape/grape/pull/1285): Add a warning for errors appearing in `after` callbacks - [@gregormelhorn](https://github.com/gregormelhorn). * [#1295](https://github.com/ruby-grape/grape/pull/1295): Add custom validation messages for parameter exceptions - [@railsmith](https://github.com/railsmith). #### Fixes * [#1216](https://github.com/ruby-grape/grape/pull/1142): Fix JSON error response when calling `error!` with non-Strings - [@jrforrest](https://github.com/jrforrest). * [#1225](https://github.com/ruby-grape/grape/pull/1225): Fix `given` with nested params not returning correct declared params - [@JanStevens](https://github.com/JanStevens). * [#1249](https://github.com/ruby-grape/grape/pull/1249): Don't fail even if invalid type value is passed to default validator - [@namusyaka](https://github.com/namusyaka). * [#1266](https://github.com/ruby-grape/grape/pull/1266): Fix `Allow` header including `OPTIONS` when `do_not_route_options!` is active - [@arempe93](https://github.com/arempe93). * [#1270](https://github.com/ruby-grape/grape/pull/1270): Fix `param` versioning with a custom parameter - [@wshatch](https://github.com/wshatch). * [#1282](https://github.com/ruby-grape/grape/pull/1282): Fix specs circular dependency - [@304](https://github.com/304). * [#1283](https://github.com/ruby-grape/grape/pull/1283): Fix 500 error for xml format when method is not allowed - [@304](https://github.com/304). * [#1197](https://github.com/ruby-grape/grape/pull/1290): Fix using JSON and Array[JSON] as groups when parameter is optional - [@lukeivers](https://github.com/lukeivers). ### 0.14.0 (2015/12/07) #### Features * [#1218](https://github.com/ruby-grape/grape/pull/1218): Provide array index context in errors - [@towanda](https://github.com/towanda). * [#1196](https://github.com/ruby-grape/grape/pull/1196): Allow multiple `before_each` blocks - [@huynhquancam](https://github.com/huynhquancam). * [#1190](https://github.com/ruby-grape/grape/pull/1190): Bypass formatting for statuses with no entity-body - [@tylerdooling](https://github.com/tylerdooling). * [#1188](https://github.com/ruby-grape/grape/pull/1188): Allow parameters with more than one type - [@dslh](https://github.com/dslh). * [#1179](https://github.com/ruby-grape/grape/pull/1179): Allow all RFC6838 valid characters in header vendor - [@suan](https://github.com/suan). * [#1170](https://github.com/ruby-grape/grape/pull/1170): Allow dashes and periods in header vendor - [@suan](https://github.com/suan). * [#1167](https://github.com/ruby-grape/grape/pull/1167): Convenience wrapper `type: File` for validating multipart file parameters - [@dslh](https://github.com/dslh). * [#1167](https://github.com/ruby-grape/grape/pull/1167): Refactor and extend coercion and type validation system - [@dslh](https://github.com/dslh). * [#1163](https://github.com/ruby-grape/grape/pull/1163): First-class `JSON` parameter type - [@dslh](https://github.com/dslh). * [#1161](https://github.com/ruby-grape/grape/pull/1161): Custom parameter coercion using `coerce_with` - [@dslh](https://github.com/dslh). #### Fixes * [#1194](https://github.com/ruby-grape/grape/pull/1194): Redirect as plain text with message - [@tylerdooling](https://github.com/tylerdooling). * [#1185](https://github.com/ruby-grape/grape/pull/1185): Use formatters for custom vendored content types - [@tylerdooling](https://github.com/tylerdooling). * [#1156](https://github.com/ruby-grape/grape/pull/1156): Fixed `no implicit conversion of Symbol into Integer` with nested `values` validation - [@quickpay](https://github.com/quickpay). * [#1153](https://github.com/ruby-grape/grape/pull/1153): Fixes boolean declaration in an external file - [@towanda](https://github.com/towanda). * [#1142](https://github.com/ruby-grape/grape/pull/1142): Makes #declared unavailable to before filters - [@jrforrest](https://github.com/jrforrest). * [#1114](https://github.com/ruby-grape/grape/pull/1114): Fix regression which broke identical endpoints with different versions - [@suan](https://github.com/suan). * [#1109](https://github.com/ruby-grape/grape/pull/1109): Memoize Virtus attribute and fix memory leak - [@marshall-lee](https://github.com/marshall-lee). * [#1101](https://github.com/ruby-grape/grape/pull/1101): Fix: Incorrect media-type `Accept` header now correctly returns 406 with `strict: true` - [@elliotlarson](https://github.com/elliotlarson). * [#1108](https://github.com/ruby-grape/grape/pull/1039): Raise a warning when `desc` is called with options hash and block - [@rngtng](https://github.com/rngtng). ### 0.13.0 (2015/8/10) #### 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 (2015/6/18) #### 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://github.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 (2015/2/23) * [#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 (2014/12/28) * [#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 (2014/12/19) * [#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 (2014/8/27) #### 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 (2014/7/10) #### 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 (2014/4/2) #### 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 explicitly 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 (2013/10/19) #### 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 (2013/9/16) #### Features * Grape is no longer tested against Ruby 1.8.7 - [@dblock](https://github.com/dblock). * [#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 (2013/6/14) #### 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 (2013/4/1) * [#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 (2013/3/17) * [#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 (2013/2/28) * [#355](https://github.com/ruby-grape/grape/issues/355): Relax dependency constraint on Hashie - [@reset](https://github.com/reset). ### 0.3.1 (2013/2/25) * [#351](https://github.com/ruby-grape/grape/issues/351): Compatibility with Ruby 2.0 - [@mbleigh](https://github.com/mbleigh). ### 0.3.0 (2013/02/21) * [#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](https://github.com/dblock). * [#131](https://github.com/ruby-grape/grape/issues/131): Added instructions for Grape API reloading in Rails - [@jyn](https://github.com/jyn), [@dblock](https://github.com/dblock). * [#317](https://github.com/ruby-grape/grape/issues/317): Added `headers` that returns a hash of parsed HTTP request headers - [@dblock](https://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](https://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](https://github.com/dblock). * Added `do_not_route_options!`, which disables the automatic creation of the `OPTIONS` route - [@dblock](https://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](https://github.com/dblock). * A formatter that raises an exception will cause the API to return a 500 error - [@dblock](https://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](https://github.com/dblock). * [#60](https://github.com/ruby-grape/grape/issues/60): Fix: mounting of a Grape API onto a path - [@dblock](https://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](https://github.com/FreakenK). ### 0.2.6 (2013/01/11) * Fix: support content-type with character set when parsing POST and PUT input - [@dblock](https://github.com/dblock). * Fix: CVE-2013-0175, multi_xml parse vulnerability, require multi_xml 0.5.2 - [@dblock](https://github.com/dblock). ### 0.2.5 (2013/01/10) * Added support for custom parsers via `parser`, in addition to built-in multipart, JSON and XML parsers - [@dblock](https://github.com/dblock). * Removed `body_params`, data sent via a POST or PUT with a supported content-type is merged into `params` - [@dblock](https://github.com/dblock). * Setting `format` will automatically remove other content-types by calling `content_type` - [@dblock](https://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](https://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 (2013/01/06) * [#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 (2012/12/24) * [#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 (2012/12/10) #### 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 (2012/7/11) * [#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 (2012/3/28) * 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 (2011/6/14) * 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 (2011/4/8) * 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 (2011/1/10) * 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 (2010/11/14) * Endpoints properly reset between each request - [@mbleigh](https://github.com/mbleigh). ### 0.1.0 (2010/11/13) * Initial public release - [@mbleigh](https://github.com/mbleigh). grape-1.5.2/LICENSE0000644000004100000410000000211014016744305013650 0ustar www-datawww-dataCopyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors. 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-1.5.2/lib/0000755000004100000410000000000014016744305013417 5ustar www-datawww-datagrape-1.5.2/lib/grape/0000755000004100000410000000000014016744305014515 5ustar www-datawww-datagrape-1.5.2/lib/grape/parser/0000755000004100000410000000000014016744305016011 5ustar www-datawww-datagrape-1.5.2/lib/grape/parser/xml.rb0000644000004100000410000000062314016744305017137 0ustar www-datawww-data# frozen_string_literal: true module Grape module Parser module Xml class << self def call(object, _env) ::Grape::Xml.parse(object) rescue ::Grape::Xml::ParseError # handle XML parsing errors via the rescue handlers or provide error message raise Grape::Exceptions::InvalidMessageBody, 'application/xml' end end end end end grape-1.5.2/lib/grape/parser/json.rb0000644000004100000410000000062714016744305017314 0ustar www-datawww-data# frozen_string_literal: true module Grape module Parser module Json class << self def call(object, _env) ::Grape::Json.load(object) rescue ::Grape::Json::ParseError # handle JSON parsing errors via the rescue handlers or provide error message raise Grape::Exceptions::InvalidMessageBody, 'application/json' end end end end end grape-1.5.2/lib/grape/formatter.rb0000644000004100000410000000146514016744305017053 0ustar www-datawww-data# frozen_string_literal: true module Grape module Formatter extend Util::Registrable class << self def builtin_formatters @builtin_formatters ||= { json: Grape::Formatter::Json, jsonapi: Grape::Formatter::Json, serializable_hash: Grape::Formatter::SerializableHash, txt: Grape::Formatter::Txt, xml: Grape::Formatter::Xml } end def formatters(**options) builtin_formatters.merge(default_elements).merge!(options[:formatters] || {}) end def formatter_for(api_format, **options) spec = formatters(**options)[api_format] case spec when nil ->(obj, _env) { obj } when Symbol method(spec) else spec end end end end end grape-1.5.2/lib/grape/version.rb0000644000004100000410000000014614016744305016530 0ustar www-datawww-data# frozen_string_literal: true module Grape # The current version of Grape. VERSION = '1.5.2' end grape-1.5.2/lib/grape/locale/0000755000004100000410000000000014016744305015754 5ustar www-datawww-datagrape-1.5.2/lib/grape/locale/en.yml0000644000004100000410000000505214016744305017103 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' except_values: 'has a value not allowed' same_as: 'is not the same as %{parameter}' 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, :accept_version_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, Hash, JSON or Array[JSON]' 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}' invalid_version_header: problem: 'Invalid version header' resolution: '%{message}' invalid_response: 'Invalid response' grape-1.5.2/lib/grape/namespace.rb0000644000004100000410000000267514016744305017010 0ustar www-datawww-data# frozen_string_literal: true require 'grape/util/cache' module Grape # A container for endpoints or other namespaces, which allows for both # logical grouping of endpoints as well as sharing common configuration. # May also be referred to as group, segment, or resource. class Namespace attr_reader :space, :options # @param space [String] the name of this namespace # @param options [Hash] options hash # @option options :requirements [Hash] param-regex pairs, all of which must # be met by a request's params for all endpoints in this namespace, or # validation will fail and return a 422. def initialize(space, **options) @space = space.to_s @options = options end # Retrieves the requirements from the options hash, if given. # @return [Hash] def requirements options[:requirements] || {} end # (see ::joined_space_path) def self.joined_space(settings) settings&.map(&:space) end # Join the namespaces from a list of settings to create a path prefix. # @param settings [Array] list of Grape::Util::InheritableSettings. def self.joined_space_path(settings) Grape::Router.normalize_path(JoinedSpaceCache[joined_space(settings)]) end class JoinedSpaceCache < Grape::Util::Cache def initialize @cache = Hash.new do |h, joined_space| h[joined_space] = -joined_space.join('/') end end end end end grape-1.5.2/lib/grape/dsl/0000755000004100000410000000000014016744305015277 5ustar www-datawww-datagrape-1.5.2/lib/grape/dsl/settings.rb0000644000004100000410000001324414016744305017470 0ustar www-datawww-data# frozen_string_literal: true require 'active_support/concern' module Grape module DSL # Keeps track of settings (implemented 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_writer :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 ||= build_top_level_setting 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, 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 def namespace_reverse_stackable(key, value = nil) get_or_set :namespace_reverse_stackable, key, value end def namespace_stackable_with_hash(key) settings = get_or_set :namespace_stackable, key, nil return if settings.blank? settings.each_with_object({}) { |value, result| result.deep_merge!(value) } end def namespace_reverse_stackable_with_hash(key) settings = get_or_set :namespace_reverse_stackable, key, nil return if settings.blank? result = {} settings.each do |setting| setting.each do |field, value| result[field] ||= value end end result 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, 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 private # Builds the current class :inheritable_setting. If available, it inherits from # the superclass's :inheritable_setting. def build_top_level_setting Grape::Util::InheritableSetting.new.tap do |setting| # Doesn't try to inherit settings from +Grape::API::Instance+ which also responds to # +inheritable_setting+, however, it doesn't contain any user-defined settings. # Otherwise, it would lead to an extra instance of +Grape::Util::InheritableSetting+ # in the chain for every endpoint. if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance setting.inherit_from superclass.inheritable_setting end end end end end end grape-1.5.2/lib/grape/dsl/helpers.rb0000644000004100000410000000563014016744305017272 0ustar www-datawww-data# frozen_string_literal: true require '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 [Array] new_modules optional array of modules 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 # # @example Include many modules # # class ExampleAPI < Grape::API # helpers Authentication, Mailer, OtherModule # end # def helpers(*new_modules, &block) include_new_modules(new_modules) if new_modules.any? include_block(block) if block_given? include_all_in_scope if !block_given? && new_modules.empty? end protected def include_new_modules(modules) modules.each { |mod| make_inclusion(mod) } end def include_block(block) Module.new.tap do |mod| make_inclusion(mod) { mod.class_eval(&block) } end end def make_inclusion(mod, &block) define_boolean_in_mod(mod) inject_api_helpers_to_mod(mod, &block) namespace_stackable(:helpers, mod) end def include_all_in_scope Module.new.tap do |mod| namespace_stackable(:helpers).each { |mod_to_include| mod.send :include, mod_to_include } change! end end def define_boolean_in_mod(mod) return if defined? mod::Boolean mod.const_set('Boolean', Grape::API::Boolean) end def inject_api_helpers_to_mod(mod, &_block) mod.extend(BaseHelper) unless mod.is_a?(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 return unless instance_variable_defined?(:@named_params) && @named_params && @named_params.any? api.namespace_stackable(:named_params, @named_params) end end end end end grape-1.5.2/lib/grape/dsl/routing.rb0000644000004100000410000001655414016744305017326 0ustar www-datawww-data# frozen_string_literal: true require 'active_support/concern' module Grape module DSL module Routing extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods attr_reader :endpoints # 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.extract_options! options = options.reverse_merge(using: :path) requested_versions = args.flatten raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor) @versions = versions | requested_versions if block_given? within_namespace do namespace_inheritable(:version, requested_versions) namespace_inheritable(:version_options, options) instance_eval(&block) end else namespace_inheritable(:version, requested_versions) namespace_inheritable(:version_options, options) end end @versions.last if instance_variable_defined?(:@versions) && @versions end # Define a root URL prefix for your entire API. def prefix(prefix = nil) namespace_inheritable(:root_prefix, prefix) 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 # 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, *opts) mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) mounts.each_pair do |app, path| if app.respond_to?(:mount_instance) opts_with = opts.any? ? opts.shift[:with] : {} mount({ app.mount_instance(configuration: opts_with) => path }) next end in_setting = inheritable_setting if app.respond_to?(:inheritable_setting, true) mount_path = Grape::Router.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.change! change! end endpoints << Grape::Endpoint.new( in_setting, method: :any, path: path, app: app, route_options: { anchor: false }, forward_match: !app.respond_to?(:inheritable_setting), 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) methods = '*' if methods == :any endpoint_options = { method: methods, path: paths, for: self, route_options: { params: namespace_stackable_with_hash(: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 Grape::Http::Headers::SUPPORTED_METHODS.each do |supported_method| define_method supported_method.downcase do |*args, &block| options = args.extract_options! paths = args.first || ['/'] route(supported_method, paths, options, &block) end end # Declare a "namespace", which prefixes all subordinate routes with its # name. Any endpoints within a namespace, group, resource or 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) @namespace_description = nil unless instance_variable_defined?(:@namespace_description) && @namespace_description if space || block_given? within_namespace do previous_namespace_description = @namespace_description @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {}) nest(block) do namespace_stackable(:namespace, Namespace.new(space, **options)) if space end @namespace_description = previous_namespace_description end else Namespace.joined_space_path(namespace_stackable(:namespace)) end end alias group namespace alias resource namespace alias resources namespace alias segment namespace # An array of API routes. def routes @routes ||= prepare_routes end # Remove all defined routes. def reset_routes! endpoints.each(&:reset_routes!) @routes = nil end def reset_endpoints! @endpoints = [] end # This 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) Grape::Validations::ParamsScope.new(api: self) do requires param, type: options[:type] end if options.key?(:type) namespace(":#{param}", options, &block) end # @return array of defined versions def versions @versions ||= [] end end end end end grape-1.5.2/lib/grape/dsl/inside_route.rb0000644000004100000410000003762414016744305020331 0ustar www-datawww-data# frozen_string_literal: true require 'active_support/concern' require 'grape/dsl/headers' module Grape module DSL module InsideRoute extend ActiveSupport::Concern include Grape::DSL::Settings include Grape::DSL::Headers # Denotes a situation where a DSL method has been invoked in a # filter which it should not yet be available in class MethodNotYetAvailable < StandardError; end # @param type [Symbol] The type of filter for which evaluation has been # completed # @return [Module] A module containing method overrides suitable for the # position in the filter evaluation sequence denoted by +type+. This # defaults to an empty module if no overrides are defined for the given # filter +type+. def self.post_filter_methods(type) @post_filter_modules ||= { before: PostBeforeFilter } @post_filter_modules[type] end # Methods which should not be available in filters until the before filter # has completed module PostBeforeFilter def declared(passed_params, options = {}, declared_params = nil, params_nested_path = []) options = options.reverse_merge(include_missing: true, include_parent_namespaces: true) declared_params ||= optioned_declared_params(**options) if passed_params.is_a?(Array) declared_array(passed_params, options, declared_params, params_nested_path) else declared_hash(passed_params, options, declared_params, params_nested_path) end end private def declared_array(passed_params, options, declared_params, params_nested_path) passed_params.map do |passed_param| declared(passed_param || {}, options, declared_params, params_nested_path) end end def declared_hash(passed_params, options, declared_params, params_nested_path) declared_params.each_with_object(passed_params.class.new) do |declared_param, memo| if declared_param.is_a?(Hash) declared_param.each_pair do |declared_parent_param, declared_children_params| params_nested_path_dup = params_nested_path.dup params_nested_path_dup << declared_parent_param.to_s next unless options[:include_missing] || passed_params.key?(declared_parent_param) passed_children_params = passed_params[declared_parent_param] || passed_params.class.new memo_key = optioned_param_key(declared_parent_param, options) memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do declared(passed_children_params, options, declared_children_params, params_nested_path_dup) end end else # If it is not a Hash then it does not have children. # Find its value or set it to nil. has_renaming = route_setting(:renamed_params) && route_setting(:renamed_params).find { |current| current[declared_param] } param_renaming = has_renaming[declared_param] if has_renaming next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming)) memo_key = optioned_param_key(param_renaming || declared_param, options) passed_param = passed_params[param_renaming || declared_param] params_nested_path_dup = params_nested_path.dup params_nested_path_dup << declared_param.to_s memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do passed_param end end end end def handle_passed_param(params_nested_path, has_passed_children = false, &_block) return yield if has_passed_children key = params_nested_path[0] key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1 route_options_params = options[:route_options][:params] || {} type = route_options_params.dig(key, :type) has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) } if type == 'Hash' && !has_children {} elsif type == 'Array' || type&.start_with?('[') && !type&.include?(',') [] elsif type == 'Set' || type&.start_with?('# "Body" def body(value = nil) if value @body = value elsif value == false @body = '' status 204 else instance_variable_defined?(:@body) ? @body : nil end end # Allows you to explicitly return no content. # # @example # delete :id do # return_no_content # "not returned" # end # # DELETE /12 # => 204 No Content, "" def return_no_content status 204 body false end # Deprecated method to send files to the client. Use `sendfile` or `stream` def file(value = nil) if value.is_a?(String) warn '[DEPRECATION] Use sendfile or stream to send files.' sendfile(value) elsif !value.is_a?(NilClass) warn '[DEPRECATION] Use stream to use a Stream object.' stream(value) else warn '[DEPRECATION] Use sendfile or stream to send files.' sendfile end end # Allows you to send a file to the client via sendfile. # # @example # get '/file' do # sendfile FileStreamer.new(...) # end # # GET /file # => "contents of file" def sendfile(value = nil) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @stream = Grape::ServeStream::StreamResponse.new(file_body) elsif !value.is_a?(NilClass) raise ArgumentError, 'Argument must be a file path' else stream 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) return if value.nil? && @stream.nil? header 'Content-Length', nil header 'Transfer-Encoding', nil header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @stream = Grape::ServeStream::StreamResponse.new(file_body) elsif value.respond_to?(:each) @stream = Grape::ServeStream::StreamResponse.new(value) elsif !value.is_a?(NilClass) raise ArgumentError, 'Stream object must respond to :each.' else @stream end 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 raise 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.description # end def route env[Grape::Env::GRAPE_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 explicitly 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 ||= (namespace_stackable_with_hash(: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[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION] entity_class.represent(object, **embeds.merge(options)) end end end end grape-1.5.2/lib/grape/dsl/configuration.rb0000644000004100000410000000047014016744305020474 0ustar www-datawww-data# frozen_string_literal: true require 'active_support/concern' module Grape module DSL module Configuration extend ActiveSupport::Concern module ClassMethods include Grape::DSL::Settings include Grape::DSL::Logger include Grape::DSL::Desc end end end end grape-1.5.2/lib/grape/dsl/api.rb0000644000004100000410000000072214016744305016376 0ustar www-datawww-data# frozen_string_literal: true require '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-1.5.2/lib/grape/dsl/parameters.rb0000644000004100000410000002413014016744305017767 0ustar www-datawww-data# frozen_string_literal: true require '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 # Set the module used to build the request.params. # # @param build_with the ParamBuilder module to use when building request.params # Available builders are: # # * Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder (default) # * Grape::Extensions::Hash::ParamBuilder # * Grape::Extensions::Hashie::Mash::ParamBuilder # # @example # # require 'grape/extenstions/hashie_mash' # class API < Grape::API # desc "Get collection" # params do # build_with Grape::Extensions::Hashie::Mash::ParamBuilder # requires :user_id, type: Integer # end # get do # params['user_id'] # end # end def build_with(build_with = nil) @api.namespace_inheritable(:build_params_with, build_with) end # 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 = @api.namespace_stackable_with_hash(:named_params) || {} options = names.extract_options! names.each do |name| params_block = named_params.fetch(name) do raise "Params :#{name} not found!" end instance_exec(options, ¶ms_block) end end alias use_scope use alias includes use # Require one or more parameters for the current endpoint. # # @param attrs list of parameters 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::Validations::Types} for a list of # types that are supported automatically. Custom classes may be used # where they define a class-level `::parse` method, or in conjunction # with the `:coerce_with` parameter. `JSON` may be supplied to denote # `JSON`-formatted objects or arrays of objects. `Array[JSON]` accepts # the same values as `JSON` but will wrap single objects in an `Array`. # @option attrs :types [Array] may be supplied in place of +:type+ # to declare an attribute that has multiple allowed types. See # {Validations::Types::MultipleTypeCoercer} for more details on coercion # and validation rules for variant-type parameters. # @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 # @option attrs :coerce_with [#parse, #call] method to be used when coercing # the parameter to the type named by `attrs[:type]`. Any class or object # that defines `::parse` or `::call` may be used. # # @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.extract_options!.clone opts[:presence] = { value: true, message: opts[:message] } opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group 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, **opts.slice(:as)) 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.extract_options!.clone type = opts[:type] opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group # check type for optional parameter group if attrs && block_given? raise Grape::Exceptions::MissingGroupTypeError.new if type.nil? raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(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, **opts.slice(:as)) end end # Define common settings for one or more parameters # @param (see #requires) # @option (see #requires) def with(*attrs, &block) new_group_scope(attrs.clone, &block) 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: { value: true, message: extract_message_option(attrs) }) 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: { value: true, message: extract_message_option(attrs) }) 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: { value: true, message: extract_message_option(attrs) }) 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: { value: true, message: extract_message_option(attrs) }) 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 # @raise Grape::Exceptions::UnknownParameter if `attr` has not been # defined in this scope yet # @yield a parameter definition DSL def given(*attrs, &block) attrs.each do |attr| proxy_attr = first_hash_key_or_param(attr) raise Grape::Exceptions::UnknownParameter.new(proxy_attr) unless declared_param?(proxy_attr) end new_lateral_scope(dependent_on: attrs, &block) end # Test for whether a certain parameter has been defined in this params # block yet. # @return [Boolean] whether the parameter has been defined def declared_param?(param) if lateral? # Elements of @declared_params of lateral scope are pushed in @parent. So check them in @parent. @parent.declared_param?(param) else # @declared_params also includes hashes of options and such, but those # won't be flattened out. @declared_params.flatten.any? do |declared_param| first_hash_key_or_param(declared_param) == param end end end alias group requires class EmptyOptionalValue; end def map_params(params, element, is_array = false) if params.is_a?(Array) params.map do |el| map_params(el, element, true) end elsif params.is_a?(Hash) params[element] || (@optional && is_array ? EmptyOptionalValue : {}) elsif params == EmptyOptionalValue EmptyOptionalValue else {} end end # @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 instance_variable_defined?(:@parent) && @parent params = map_params(params, @element) if instance_variable_defined?(:@element) && @element params end private def first_hash_key_or_param(parameter) parameter.is_a?(Hash) ? parameter.keys.first : parameter end end end end grape-1.5.2/lib/grape/dsl/validations.rb0000644000004100000410000000336214016744305020145 0ustar www-datawww-data# frozen_string_literal: true require 'active_support/concern' module Grape module DSL module Validations extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods # Clears all defined parameters and validations. The main purpose of it is to clean up # settings, so next endpoint won't interfere with previous one. # # params do # # params for the endpoint below this block # end # post '/current' do # # whatever # end # # # somewhere between them the reset_validations! method gets called # # params do # # params for the endpoint below this block # end # post '/next' do # # whatever # end 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| full_name = name[:full_name].to_s setting[full_name] ||= {} setting[full_name].merge!(opts) namespace_stackable(:params, full_name => opts) end end end end end end grape-1.5.2/lib/grape/dsl/desc.rb0000644000004100000410000001200314016744305016536 0ustar www-datawww-data# frozen_string_literal: true module Grape module DSL module Desc include Grape::DSL::Settings # 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 :summary [String] summary for 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 :body_name [String] override the autogenerated body name param # @option options :headers [Hash] HTTP headers this method can accept # @option options :hidden [Boolean] hide the endpoint or not # @option options :deprecated [Boolean] deprecate the endpoint or not # @option options :is_array [Boolean] response entity is array or not # @option options :nickname [String] nickname of the endpoint # @option options :produces [Array[String]] a list of MIME types the endpoint produce # @option options :consumes [Array[String]] a list of MIME types the endpoint consume # @option options :security [Array[Hash]] a list of security schemes # @option options :tags [Array[String]] a list of tags # @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? endpoint_configuration = if defined?(configuration) # When the instance is mounted - the configuration is executed on mount time if configuration.respond_to?(:evaluate) configuration.evaluate # Within `given` or `mounted blocks` the configuration is already evaluated elsif configuration.is_a?(Hash) configuration end end endpoint_configuration ||= {} config_class = desc_container(endpoint_configuration) config_class.configure do description description end config_class.configure(&config_block) unless options.empty? warn '[DEPRECATION] Passing a options hash and a block to `desc` is deprecated. Move all hash options to block.' end 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) description = route_setting(:description) if value description ||= route_setting(:description, {}) description[field] = value elsif description description[field] end end def unset_description_field(field) description = route_setting(:description) description.delete(field) if description end # Returns an object which configures itself via an instance-context DSL. def desc_container(endpoint_configuration) Module.new do include Grape::Util::StrictHashConfiguration.module( :summary, :description, :detail, :params, :entity, :http_codes, :named, :body_name, :headers, :hidden, :deprecated, :is_array, :nickname, :produces, :consumes, :security, :tags ) config_context.define_singleton_method(:configuration) do endpoint_configuration end def config_context.success(*args) entity(*args) end def config_context.failure(*args) http_codes(*args) end end end end end end grape-1.5.2/lib/grape/dsl/logger.rb0000644000004100000410000000104714016744305017105 0ustar www-datawww-data# frozen_string_literal: true module Grape module DSL module Logger include Grape::DSL::Settings attr_writer :logger # 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 end end end grape-1.5.2/lib/grape/dsl/headers.rb0000644000004100000410000000060114016744305017234 0ustar www-datawww-data# frozen_string_literal: true module Grape module DSL module Headers # 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 alias headers header end end end grape-1.5.2/lib/grape/dsl/request_response.rb0000644000004100000410000001525214016744305021237 0ustar www-datawww-data# frozen_string_literal: true require '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.formatter_for(new_format, **{})) # define a single mime type mime_type = content_types[new_format.to_sym] raise 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.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) formatter = if options.is_a?(Hash) && options.key?(:with) options[:with] else 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 = namespace_stackable_with_hash(: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.extract_options! if block_given? && options.key?(:with) raise ArgumentError, 'both :with option and block cannot be passed' end handler ||= extract_with(options) if args.include?(:all) namespace_inheritable(:rescue_all, true) namespace_inheritable :all_rescue_handler, handler elsif args.include?(:grape_exceptions) namespace_inheritable(:rescue_all, true) namespace_inheritable(:rescue_grape_exceptions, true) else handler_type = case options[:rescue_subclasses] when nil, true :rescue_handlers else :base_only_rescue_handlers end namespace_reverse_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) raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with] && options[:with].is_a?(Class) namespace_stackable(:representations, model_class => options[:with]) end private def extract_with(options) return unless options.key?(:with) with_option = options.delete(:with) return with_option if with_option.instance_of?(Proc) return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String) raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc" end end end end end grape-1.5.2/lib/grape/dsl/callbacks.rb0000644000004100000410000000412114016744305017541 0ustar www-datawww-data# frozen_string_literal: true require '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 # Allows you to specify a something that will always be executed after a call # API call. Unlike the `after` block, this code will run even on # unsuccesful requests. # @example # class ExampleAPI < Grape::API # before do # ApiLogger.start # end # finally do # ApiLogger.close # end # end # # This will make sure that the ApiLogger is opened and closed around every # request # @param ensured_block [Proc] The block to be executed after every api_call def finally(&block) namespace_stackable(:finallies, block) end end end end end grape-1.5.2/lib/grape/dsl/middleware.rb0000644000004100000410000000264214016744305017745 0ustar www-datawww-data# frozen_string_literal: true require '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 = [:use, middleware_class, *args] arr << block if block_given? namespace_stackable(:middleware, arr) end def insert(*args, &block) arr = [:insert, *args] arr << block if block_given? namespace_stackable(:middleware, arr) end def insert_before(*args, &block) arr = [:insert_before, *args] arr << block if block_given? namespace_stackable(:middleware, arr) end def insert_after(*args, &block) arr = [:insert_after, *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-1.5.2/lib/grape/formatter/0000755000004100000410000000000014016744305016520 5ustar www-datawww-datagrape-1.5.2/lib/grape/formatter/txt.rb0000644000004100000410000000035514016744305017667 0ustar www-datawww-data# frozen_string_literal: true module Grape module Formatter module Txt class << self def call(object, _env) object.respond_to?(:to_txt) ? object.to_txt : object.to_s end end end end end grape-1.5.2/lib/grape/formatter/xml.rb0000644000004100000410000000046414016744305017651 0ustar www-datawww-data# frozen_string_literal: true module Grape module Formatter module Xml class << self def call(object, _env) return object.to_xml if object.respond_to?(:to_xml) raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') end end end end end grape-1.5.2/lib/grape/formatter/serializable_hash.rb0000644000004100000410000000211114016744305022511 0ustar www-datawww-data# frozen_string_literal: true module Grape module Formatter module SerializableHash class << self def call(object, _env) return object if object.is_a?(String) return ::Grape::Json.dump(serialize(object)) if serializable?(object) return object.to_json if object.respond_to?(:to_json) ::Grape::Json.dump(object) end private def serializable?(object) object.respond_to?(:serializable_hash) || object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } || object.is_a?(Hash) end def serialize(object) if object.respond_to? :serializable_hash object.serializable_hash elsif object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } object.map(&:serializable_hash) elsif object.is_a?(Hash) h = {} object.each_pair do |k, v| h[k] = serialize(v) end h else object end end end end end end grape-1.5.2/lib/grape/formatter/json.rb0000644000004100000410000000041714016744305020020 0ustar www-datawww-data# frozen_string_literal: true module Grape module Formatter module Json class << self def call(object, _env) return object.to_json if object.respond_to?(:to_json) ::Grape::Json.dump(object) end end end end end grape-1.5.2/lib/grape/router/0000755000004100000410000000000014016744305016035 5ustar www-datawww-datagrape-1.5.2/lib/grape/router/route.rb0000644000004100000410000000535714016744305017532 0ustar www-datawww-data# frozen_string_literal: true require 'grape/router/pattern' require 'grape/router/attribute_translator' require 'forwardable' require 'pathname' module Grape class Router class Route ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze FIXED_NAMED_CAPTURES = %w[format version].freeze attr_accessor :pattern, :translator, :app, :index, :options alias attributes translator extend Forwardable def_delegators :pattern, :path, :origin delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes def method_missing(method_id, *arguments) match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s) if match method_name = match.captures.last.to_sym warn_route_methods(method_name, caller(1).shift) @options[method_name] else super end end def respond_to_missing?(method_id, _) ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s) end def route_method warn_route_methods(:method, caller(1).shift, :request_method) request_method end def route_path warn_route_methods(:path, caller(1).shift) pattern.path end def initialize(method, pattern, **options) method_s = method.to_s method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase @options = options.merge(method: method_upcase) @pattern = Pattern.new(pattern, **options) @translator = AttributeTranslator.new(**options, request_method: method_upcase) end def exec(env) @app.call(env) end def apply(app) @app = app self end def match?(input) translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input) end def params(input = nil) if input.nil? pattern.named_captures.keys.each_with_object(translator.params) do |(key), defaults| defaults[key] ||= '' unless FIXED_NAMED_CAPTURES.include?(key) || defaults.key?(key) end else parsed = pattern.params(input) parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {} end end private def warn_route_methods(name, location, expected = nil) path, line = *location.scan(SOURCE_LOCATION_REGEXP).first path = File.realpath(path) if Pathname.new(path).relative? expected ||= name warn <<-WARNING #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}. WARNING end end end end grape-1.5.2/lib/grape/router/attribute_translator.rb0000644000004100000410000000243014016744305022635 0ustar www-datawww-data# frozen_string_literal: true module Grape class Router # this could be an OpenStruct, but doesn't work in Ruby 2.3.0, see https://bugs.ruby-lang.org/issues/12251 class AttributeTranslator attr_reader :attributes ROUTE_ATTRIBUTES = %i[ prefix version settings format description http_codes headers entity details requirements request_method namespace ].freeze ROUTER_ATTRIBUTES = %i[pattern index].freeze def initialize(**attributes) @attributes = attributes end (ROUTER_ATTRIBUTES + ROUTE_ATTRIBUTES).each do |attr| define_method attr do attributes[attr] end end def to_h attributes end def method_missing(method_name, *args) if setter?(method_name[-1]) attributes[method_name[0..-1]] = *args else attributes[method_name] end end def respond_to_missing?(method_name, _include_private = false) if setter?(method_name[-1]) true else @attributes.key?(method_name) end end private def setter?(method_name) method_name[-1] == '=' end end end end grape-1.5.2/lib/grape/router/pattern.rb0000644000004100000410000000364114016744305020043 0ustar www-datawww-data# frozen_string_literal: true require 'forwardable' require 'mustermann/grape' require 'grape/util/cache' module Grape class Router class Pattern DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze attr_reader :origin, :path, :pattern, :to_regexp extend Forwardable def_delegators :pattern, :named_captures, :params def_delegators :to_regexp, :=== alias match? === def initialize(pattern, **options) @origin = pattern @path = build_path(pattern, **options) @pattern = Mustermann::Grape.new(@path, **pattern_options(options)) @to_regexp = @pattern.to_regexp end private def pattern_options(options) capture = extract_capture(**options) options = DEFAULT_PATTERN_OPTIONS.dup options[:capture] = capture if capture.present? options end def build_path(pattern, anchor: false, suffix: nil, **_options) unless anchor || pattern.end_with?('*path') pattern = +pattern pattern << '/' unless pattern.end_with?('/') pattern << '*path' end pattern = -pattern.split('/').tap do |parts| parts[parts.length - 1] = '?' + parts.last end.join('/') if pattern.end_with?('*path') PatternCache[[pattern, suffix]] end def extract_capture(requirements: {}, **options) requirements = {}.merge(requirements) DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture| option = Array(options[field]) capture[field] = option.map(&:to_s) if option.present? end end class PatternCache < Grape::Util::Cache def initialize @cache = Hash.new do |h, (pattern, suffix)| h[[pattern, suffix]] = -"#{pattern}#{suffix}" end end end end end end grape-1.5.2/lib/grape/request.rb0000644000004100000410000000207714016744305016540 0ustar www-datawww-data# frozen_string_literal: true require 'grape/util/lazy_object' module Grape class Request < Rack::Request HTTP_PREFIX = 'HTTP_' alias rack_params params def initialize(env, **options) extend options[:build_params_with] || Grape.config.param_builder super(env) end def params @params ||= build_params end def headers @headers ||= build_headers end private def grape_routing_args args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup # preserve version from query string parameters args.delete(:version) args.delete(:route_info) args end def build_headers Grape::Util::LazyObject.new do env.each_pair.with_object({}) do |(k, v), headers| next unless k.to_s.start_with? HTTP_PREFIX transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k) headers[transformed_header] = v end end end def transform_header(header) -header[5..-1].split('_').each(&:capitalize!).join('-') end end end grape-1.5.2/lib/grape/util/0000755000004100000410000000000014016744305015472 5ustar www-datawww-datagrape-1.5.2/lib/grape/util/lazy_value.rb0000644000004100000410000000434414016744305020177 0ustar www-datawww-data# frozen_string_literal: true module Grape module Util class LazyValue attr_reader :access_keys def initialize(value, access_keys = []) @value = value @access_keys = access_keys end def evaluate_from(configuration) matching_lazy_value = configuration.fetch(@access_keys) matching_lazy_value.evaluate end def evaluate @value end def lazy? true end def reached_by(parent_access_keys, access_key) @access_keys = parent_access_keys + [access_key] self end def to_s evaluate.to_s end end class LazyValueEnumerable < LazyValue def [](key) if @value_hash[key].nil? LazyValue.new(nil).reached_by(access_keys, key) else @value_hash[key].reached_by(access_keys, key) end end def fetch(access_keys) fetched_keys = access_keys.dup value = self[fetched_keys.shift] fetched_keys.any? ? value.fetch(fetched_keys) : value end def []=(key, value) @value_hash[key] = if value.is_a?(Hash) LazyValueHash.new(value) elsif value.is_a?(Array) LazyValueArray.new(value) else LazyValue.new(value) end end end class LazyValueArray < LazyValueEnumerable def initialize(array) super @value_hash = [] array.each_with_index do |value, index| self[index] = value end end def evaluate evaluated = [] @value_hash.each_with_index do |value, index| evaluated[index] = value.evaluate end evaluated end end class LazyValueHash < LazyValueEnumerable def initialize(hash) super @value_hash = {}.with_indifferent_access hash.each do |key, value| self[key] = value end end def evaluate evaluated = {}.with_indifferent_access @value_hash.each do |key, value| evaluated[key] = value.evaluate end evaluated end end end end grape-1.5.2/lib/grape/util/cache.rb0000644000004100000410000000047714016744305017072 0ustar www-datawww-data# frozen_String_literal: true require 'singleton' require 'forwardable' module Grape module Util class Cache include Singleton attr_reader :cache class << self extend Forwardable def_delegators :cache, :[] def_delegators :instance, :cache end end end end grape-1.5.2/lib/grape/util/lazy_block.rb0000644000004100000410000000061114016744305020146 0ustar www-datawww-data# frozen_string_literal: true module Grape module Util class LazyBlock def initialize(&new_block) @block = new_block end def evaluate_from(configuration) @block.call(configuration) end def evaluate @block.call({}) end def lazy? true end def to_s evaluate.to_s end end end end grape-1.5.2/lib/grape/util/xml.rb0000644000004100000410000000027614016744305016624 0ustar www-datawww-data# frozen_string_literal: true module Grape if Object.const_defined? :MultiXml Xml = ::MultiXml else Xml = ::ActiveSupport::XmlMini Xml::ParseError = StandardError end end grape-1.5.2/lib/grape/util/registrable.rb0000644000004100000410000000044314016744305020323 0ustar www-datawww-data# frozen_string_literal: true module Grape module Util module Registrable def default_elements @default_elements ||= {} end def register(format, element) default_elements[format] = element unless default_elements[format] end end end end grape-1.5.2/lib/grape/util/inheritable_setting.rb0000644000004100000410000000675014016744305022052 0ustar www-datawww-data# frozen_string_literal: true module 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 attr_accessor :namespace_inheritable, :namespace_stackable, :namespace_reverse_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.namespace_reverse_stackable = ReverseStackableValues.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 namespace_reverse_stackable.inherited_values = parent.namespace_reverse_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.namespace_reverse_stackable = namespace_reverse_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, namespace_reverse_stackable: namespace_reverse_stackable.to_hash } end end end end grape-1.5.2/lib/grape/util/base_inheritable.rb0000644000004100000410000000207014016744305021276 0ustar www-datawww-data# frozen_string_literal: true module Grape module Util # Base for classes which need to operate with own values kept # in the hash and inherited values kept in a Hash-like object. class BaseInheritable attr_accessor :inherited_values, :new_values # @param inherited_values [Object] An object implementing an interface # of the Hash class. def initialize(inherited_values = nil) @inherited_values = inherited_values || {} @new_values = {} end def delete(key) new_values.delete key end def initialize_copy(other) super self.inherited_values = other.inherited_values self.new_values = other.new_values.dup end def keys if new_values.any? combined = inherited_values.keys combined.concat(new_values.keys) combined.uniq! combined else inherited_values.keys end end def key?(name) inherited_values.key?(name) || new_values.key?(name) end end end end grape-1.5.2/lib/grape/util/json.rb0000644000004100000410000000026514016744305016773 0ustar www-datawww-data# frozen_string_literal: true module Grape if Object.const_defined? :MultiJson Json = ::MultiJson else Json = ::JSON Json::ParseError = Json::ParserError end end grape-1.5.2/lib/grape/util/strict_hash_configuration.rb0000644000004100000410000000522514016744305023265 0ustar www-datawww-data# frozen_string_literal: true module 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.each_key { |k| merge_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_class ||= new_config_class end end end end end end grape-1.5.2/lib/grape/util/endpoint_configuration.rb0000644000004100000410000000017414016744305022570 0ustar www-datawww-data# frozen_string_literal: true module Grape module Util class EndpointConfiguration < LazyValueHash end end end grape-1.5.2/lib/grape/util/env.rb0000644000004100000410000000140314016744305016605 0ustar www-datawww-data# frozen_string_literal: true module Grape module Env API_VERSION = 'api.version' API_ENDPOINT = 'api.endpoint' API_REQUEST_INPUT = 'api.request.input' API_REQUEST_BODY = 'api.request.body' API_TYPE = 'api.type' API_SUBTYPE = 'api.subtype' API_VENDOR = 'api.vendor' API_FORMAT = 'api.format' RACK_INPUT = 'rack.input' RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash' RACK_REQUEST_FORM_HASH = 'rack.request.form_hash' RACK_REQUEST_FORM_INPUT = 'rack.request.form_input' GRAPE_REQUEST = 'grape.request' GRAPE_REQUEST_HEADERS = 'grape.request.headers' GRAPE_REQUEST_PARAMS = 'grape.request.params' GRAPE_ROUTING_ARGS = 'grape.routing_args' GRAPE_ALLOWED_METHODS = 'grape.allowed_methods' end end grape-1.5.2/lib/grape/util/reverse_stackable_values.rb0000644000004100000410000000062414016744305023064 0ustar www-datawww-data# frozen_string_literal: true require_relative 'stackable_values' module Grape module Util class ReverseStackableValues < StackableValues protected def concat_values(inherited_value, new_value) return inherited_value unless new_value [].tap do |value| value.concat(new_value) value.concat(inherited_value) end end end end end grape-1.5.2/lib/grape/util/lazy_object.rb0000644000004100000410000000160014016744305020321 0ustar www-datawww-data# frozen_string_literal: true # Based on https://github.com/HornsAndHooves/lazy_object module Grape module Util class LazyObject < BasicObject attr_reader :callable def initialize(&callable) @callable = callable end def __target_object__ @__target_object__ ||= callable.call end def ==(other) __target_object__ == other end def !=(other) __target_object__ != other end def ! !__target_object__ end def method_missing(method_name, *args, &block) if __target_object__.respond_to?(method_name) __target_object__.send(method_name, *args, &block) else super end end def respond_to_missing?(method_name, include_priv = false) __target_object__.respond_to?(method_name, include_priv) end end end end grape-1.5.2/lib/grape/util/stackable_values.rb0000644000004100000410000000162714016744305021335 0ustar www-datawww-data# frozen_string_literal: true require_relative 'base_inheritable' module Grape module Util class StackableValues < BaseInheritable # Even if there is no value, an empty array will be returned. def [](name) inherited_value = inherited_values[name] new_value = new_values[name] return new_value || [] unless inherited_value concat_values(inherited_value, new_value) end def []=(name, value) new_values[name] ||= [] new_values[name].push value end def to_hash keys.each_with_object({}) do |key, result| result[key] = self[key] end end protected def concat_values(inherited_value, new_value) return inherited_value unless new_value [].tap do |value| value.concat(inherited_value) value.concat(new_value) end end end end end grape-1.5.2/lib/grape/util/inheritable_values.rb0000644000004100000410000000074314016744305021670 0ustar www-datawww-data# frozen_string_literal: true require_relative 'base_inheritable' module Grape module Util class InheritableValues < BaseInheritable def [](name) values[name] end def []=(name, value) new_values[name] = value end def merge(new_hash) values.merge!(new_hash) end def to_hash values end protected def values @inherited_values.merge(@new_values) end end end end grape-1.5.2/lib/grape/http/0000755000004100000410000000000014016744305015474 5ustar www-datawww-datagrape-1.5.2/lib/grape/http/headers.rb0000644000004100000410000000336314016744305017441 0ustar www-datawww-data# frozen_string_literal: true require 'grape/util/lazy_object' module Grape module Http module Headers # https://github.com/rack/rack/blob/master/lib/rack.rb HTTP_VERSION = 'HTTP_VERSION' PATH_INFO = 'PATH_INFO' REQUEST_METHOD = 'REQUEST_METHOD' QUERY_STRING = 'QUERY_STRING' CONTENT_TYPE = 'Content-Type' GET = 'GET' POST = 'POST' PUT = 'PUT' PATCH = 'PATCH' DELETE = 'DELETE' HEAD = 'HEAD' OPTIONS = 'OPTIONS' SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze } HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' X_CASCADE = 'X-Cascade' HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING' HTTP_ACCEPT = 'HTTP_ACCEPT' FORMAT = 'format' HTTP_HEADERS = Grape::Util::LazyObject.new do common_http_headers = %w[ Version Host Connection Cache-Control Dnt Upgrade-Insecure-Requests User-Agent Sec-Fetch-Dest Accept Sec-Fetch-Site Sec-Fetch-Mode Sec-Fetch-User Accept-Encoding Accept-Language Cookie ].freeze common_http_headers.each_with_object({}) do |header, response| response["HTTP_#{header.upcase.tr('-', '_')}"] = header end.freeze end def self.find_supported_method(route_method) Grape::Http::Headers::SUPPORTED_METHODS.detect { |supported_method| supported_method.casecmp(route_method).zero? } end end end end grape-1.5.2/lib/grape/eager_load.rb0000644000004100000410000000110514016744305017121 0ustar www-datawww-data# frozen_string_literal: true Grape.eager_load! Grape::Http.eager_load! Grape::Exceptions.eager_load! Grape::Extensions.eager_load! Grape::Extensions::ActiveSupport.eager_load! Grape::Extensions::Hashie.eager_load! Grape::Middleware.eager_load! Grape::Middleware::Auth.eager_load! Grape::Middleware::Versioner.eager_load! Grape::Util.eager_load! Grape::ErrorFormatter.eager_load! Grape::Formatter.eager_load! Grape::Parser.eager_load! Grape::DSL.eager_load! Grape::API.eager_load! Grape::Presenters.eager_load! Grape::ServeStream.eager_load! Rack::Head # AutoLoads the Rack::Head grape-1.5.2/lib/grape/error_formatter.rb0000644000004100000410000000156714016744305020267 0ustar www-datawww-data# frozen_string_literal: true module Grape module ErrorFormatter extend Util::Registrable class << self def builtin_formatters @builtin_formatters ||= { serializable_hash: Grape::ErrorFormatter::Json, json: Grape::ErrorFormatter::Json, jsonapi: Grape::ErrorFormatter::Json, txt: Grape::ErrorFormatter::Txt, xml: Grape::ErrorFormatter::Xml } end def formatters(**options) builtin_formatters.merge(default_elements).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 end end grape-1.5.2/lib/grape/serve_stream/0000755000004100000410000000000014016744305017214 5ustar www-datawww-datagrape-1.5.2/lib/grape/serve_stream/file_body.rb0000644000004100000410000000115314016744305021475 0ustar www-datawww-data# frozen_string_literal: true module Grape module ServeStream CHUNK_SIZE = 16_384 # Class helps send file through API class FileBody attr_reader :path # @param path [String] def initialize(path) @path = path end # Need for Rack::Sendfile middleware # # @return [String] def to_path path end def each File.open(path, 'rb') do |file| while (chunk = file.read(CHUNK_SIZE)) yield chunk end end end def ==(other) path == other.path end end end end grape-1.5.2/lib/grape/serve_stream/stream_response.rb0000644000004100000410000000101114016744305022743 0ustar www-datawww-data# frozen_string_literal: true module Grape module ServeStream # A simple class used to identify responses which represent streams (or files) and do not # need to be formatted or pre-read by Rack::Response class StreamResponse attr_reader :stream # @param stream [Object] def initialize(stream) @stream = stream end # Equality provided mostly for tests. # # @return [Boolean] def ==(other) stream == other.stream end end end end grape-1.5.2/lib/grape/serve_stream/sendfile_response.rb0000644000004100000410000000071514016744305023253 0ustar www-datawww-data# frozen_string_literal: true module Grape module ServeStream # Response should respond to to_path method # for using Rack::SendFile middleware class SendfileResponse < Rack::Response def respond_to?(method_name, include_all = false) if method_name == :to_path @body.respond_to?(:to_path, include_all) else super end end def to_path @body.to_path end end end end grape-1.5.2/lib/grape/api.rb0000644000004100000410000001556414016744305015626 0ustar www-datawww-data# frozen_string_literal: true require 'grape/router' require 'grape/api/instance' module Grape # The API class is the primary entry point for creating Grape APIs. Users # should subclass this class in order to build an API. class API # Class methods that we want to call on the API rather than on the API object NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile! inherited]).freeze class << self attr_accessor :base_instance, :instances # Rather than initializing an object of type Grape::API, create an object of type Instance def new(*args, &block) base_instance.new(*args, &block) end # When inherited, will create a list of all instances (times the API was mounted) # It will listen to the setup required to mount that endpoint, and replicate it on any new instance def inherited(api, base_instance_parent = Grape::API::Instance) api.initial_setup(base_instance_parent) api.override_all_methods! make_inheritable(api) end # Initialize the instance variables on the remountable class, and the base_instance # an instance that will be used to create the set up but will not be mounted def initial_setup(base_instance_parent) @instances = [] @setup = Set.new @base_parent = base_instance_parent @base_instance = mount_instance end # Redefines all methods so that are forwarded to add_setup and be recorded def override_all_methods! (base_instance.methods - NON_OVERRIDABLE).each do |method_override| define_singleton_method(method_override) do |*args, &block| add_setup(method_override, *args, &block) end end end # Configure an API from the outside. If a block is given, it'll pass a # configuration hash to the block which you can use to configure your # API. If no block is given, returns the configuration hash. # The configuration set here is accessible from inside an API with # `configuration` as normal. def configure config = @base_instance.configuration if block_given? yield config self else config end 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. # NOTE: This will only be called on an API directly mounted on RACK def call(*args, &block) instance_for_rack.call(*args, &block) end # Allows an API to itself be inheritable: def make_inheritable(api) # When a child API inherits from a parent API. def api.inherited(child_api) # The instances of the child API inherit from the instances of the parent API Grape::API.inherited(child_api, base_instance) end end # Alleviates problems with autoloading by tring to search for the constant def const_missing(*args) if base_instance.const_defined?(*args) base_instance.const_get(*args) else super end end # The remountable class can have a configuration hash to provide some dynamic class-level variables. # For instance, a description could be done using: `desc configuration[:description]` if it may vary # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration # too much, you may actually want to provide a new API rather than remount it. def mount_instance(**opts) instance = Class.new(@base_parent) instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {}) instance.base = self replay_setup_on(instance) instance end # Replays the set up to produce an API as defined in this class, can be called # on classes that inherit from Grape::API def replay_setup_on(instance) @setup.each do |setup_step| replay_step_on(instance, setup_step) end end def respond_to?(method, include_private = false) super(method, include_private) || base_instance.respond_to?(method, include_private) end def respond_to_missing?(method, include_private = false) base_instance.respond_to?(method, include_private) end def method_missing(method, *args, &block) # If there's a missing method, it may be defined on the base_instance instead. if respond_to_missing?(method) base_instance.send(method, *args, &block) else super end end def compile! require 'grape/eager_load' instance_for_rack.compile! # See API::Instance.compile! end private def instance_for_rack if never_mounted? base_instance else mounted_instances.first end end # Adds a new stage to the set up require to get a Grape::API up and running def add_setup(method, *args, &block) setup_step = { method: method, args: args, block: block } @setup << setup_step last_response = nil @instances.each do |instance| last_response = replay_step_on(instance, setup_step) end last_response end def replay_step_on(instance, setup_step) return if skip_immediate_run?(instance, setup_step[:args]) args = evaluate_arguments(instance.configuration, *setup_step[:args]) response = instance.send(setup_step[:method], *args, &setup_step[:block]) if skip_immediate_run?(instance, [response]) response else evaluate_arguments(instance.configuration, response).first end end # Skips steps that contain arguments to be lazily executed (on re-mount time) def skip_immediate_run?(instance, args) instance.base_instance? && (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) }) end def any_lazy?(args) args.any? { |argument| argument.respond_to?(:lazy?) && argument.lazy? } end def evaluate_arguments(configuration, *args) args.map do |argument| if argument.respond_to?(:lazy?) && argument.lazy? argument.evaluate_from(configuration) elsif argument.is_a?(Hash) argument.transform_values { |value| evaluate_arguments(configuration, value).first } elsif argument.is_a?(Array) evaluate_arguments(configuration, *argument) else argument end end end def never_mounted? mounted_instances.empty? end def mounted_instances instances - [base_instance] end end end end grape-1.5.2/lib/grape/content_types.rb0000644000004100000410000000144714016744305017746 0ustar www-datawww-data# frozen_string_literal: true require 'grape/util/registrable' module Grape module ContentTypes extend Util::Registrable # Content types are listed in order of preference. CONTENT_TYPES = { xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', binary: 'application/octet-stream', txt: 'text/plain' }.freeze class << self def content_types_for_settings(settings) return if settings.blank? settings.each_with_object({}) { |value, result| result.merge!(value) } end def content_types_for(from_settings) if from_settings.present? from_settings else Grape::ContentTypes::CONTENT_TYPES.merge(default_elements) end end end end end grape-1.5.2/lib/grape/router.rb0000644000004100000410000001146314016744305016367 0ustar www-datawww-data# frozen_string_literal: true require 'grape/router/route' require 'grape/util/cache' module Grape class Router attr_reader :map, :compiled def self.normalize_path(path) path = +"/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') path = '/' if path == '' path end def self.supported_methods @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*'] end def initialize @neutral_map = [] @neutral_regexes = [] @map = Hash.new { |hash, key| hash[key] = [] } @optimized_map = Hash.new { |hash, key| hash[key] = // } end def compile! return if compiled @union = Regexp.union(@neutral_regexes) @neutral_regexes = nil self.class.supported_methods.each do |method| routes = map[method] @optimized_map[method] = routes.map.with_index do |route, index| route.index = index Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})") end @optimized_map[method] = Regexp.union(@optimized_map[method]) end @compiled = true end def append(route) map[route.request_method] << route end def associate_routes(pattern, **options) @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}") @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length) end def call(env) with_optimization do response, route = identity(env) response || rotation(env, route) end end def recognize_path(input) any = with_optimization { greedy_match?(input) } return if any == default_response any.endpoint end private def identity(env) route = nil response = transaction(env) do |input, method| route = match?(input, method) process_route(route, env) if route end [response, route] end def rotation(env, exact_route = nil) response = nil input, method = *extract_input_and_method(env) map[method].each do |route| next if exact_route == route next unless route.match?(input) response = process_route(route, env) break unless cascade?(response) end response end def transaction(env) input, method = *extract_input_and_method(env) response = yield(input, method) return response if response && !(cascade = cascade?(response)) last_neighbor_route = greedy_match?(input) # If last_neighbor_route exists and request method is OPTIONS, # return response by using #call_with_allow_headers. return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade route = match?(input, '*') return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route if route response = process_route(route, env) return response if response && !(cascade = cascade?(response)) end return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route nil end def process_route(route, env) prepare_env_from_route(env, route) route.exec(env) end def make_routing_args(default_args, route, input) args = default_args || { route_info: route } args.merge(route.params(input) || {}) end def extract_input_and_method(env) input = string_for(env[Grape::Http::Headers::PATH_INFO]) method = env[Grape::Http::Headers::REQUEST_METHOD] [input, method] end def with_optimization compile! unless compiled yield || default_response end def default_response [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']] end def match?(input, method) current_regexp = @optimized_map[method] return unless current_regexp.match(input) last_match = Regexp.last_match @map[method].detect { |route| last_match["_#{route.index}"] } end def greedy_match?(input) return unless @union.match(input) last_match = Regexp.last_match @neutral_map.detect { |route| last_match["_#{route.index}"] } end def call_with_allow_headers(env, route) prepare_env_from_route(env, route) env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze route.endpoint.call(env) end def prepare_env_from_route(env, route) input, = *extract_input_and_method(env) env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input) end def cascade?(response) response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass' end def string_for(input) self.class.normalize_path(input) end end end grape-1.5.2/lib/grape/validations.rb0000644000004100000410000000116214016744305017357 0ustar www-datawww-data# frozen_string_literal: true module 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 def self.deregister_validator(short_name) validators.delete(short_name) end end end grape-1.5.2/lib/grape/presenters/0000755000004100000410000000000014016744305016707 5ustar www-datawww-datagrape-1.5.2/lib/grape/presenters/presenter.rb0000644000004100000410000000025414016744305021244 0ustar www-datawww-data# frozen_string_literal: true module Grape module Presenters class Presenter def self.represent(object, **_options) object end end end end grape-1.5.2/lib/grape/error_formatter/0000755000004100000410000000000014016744305017731 5ustar www-datawww-datagrape-1.5.2/lib/grape/error_formatter/txt.rb0000644000004100000410000000145614016744305021103 0ustar www-datawww-data# frozen_string_literal: true module Grape module ErrorFormatter module Txt extend Base class << self def call(message, backtrace, options = {}, env = nil, original_exception = nil) message = present(message, env) result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message rescue_options = options[:rescue_options] || {} if rescue_options[:backtrace] && backtrace && !backtrace.empty? result += "\r\n backtrace:" result += backtrace.join("\r\n ") end if rescue_options[:original_exception] && original_exception result += "\r\n original exception:" result += "\r\n #{original_exception.inspect}" end result end end end end end grape-1.5.2/lib/grape/error_formatter/xml.rb0000644000004100000410000000146014016744305021057 0ustar www-datawww-data# frozen_string_literal: true module Grape module ErrorFormatter module Xml extend Base class << self def call(message, backtrace, options = {}, env = nil, original_exception = nil) message = present(message, env) result = message.is_a?(Hash) ? message : { message: message } rescue_options = options[:rescue_options] || {} if rescue_options[:backtrace] && backtrace && !backtrace.empty? result = result.merge(backtrace: backtrace) end if rescue_options[:original_exception] && original_exception result = result.merge(original_exception: original_exception.inspect) end result.respond_to?(:to_xml) ? result.to_xml(root: :error) : result.to_s end end end end end grape-1.5.2/lib/grape/error_formatter/base.rb0000644000004100000410000000253314016744305021173 0ustar www-datawww-data# frozen_string_literal: true module Grape module ErrorFormatter module Base def present(message, env) present_options = {} presented_message = message if presented_message.is_a?(Hash) presented_message = presented_message.dup present_options[:with] = presented_message.delete(:with) end presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options) unless presenter || env[Grape::Env::GRAPE_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[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || [] found_code = http_codes.find do |http_code| (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent) end if env[Grape::Env::API_ENDPOINT].request presenter = found_code[2] if found_code end if presenter embeds = { env: env } embeds[:version] = env[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION] presented_message = presenter.represent(presented_message, embeds).serializable_hash end presented_message end end end end grape-1.5.2/lib/grape/error_formatter/json.rb0000644000004100000410000000165114016744305021232 0ustar www-datawww-data# frozen_string_literal: true module Grape module ErrorFormatter module Json extend Base class << self def call(message, backtrace, options = {}, env = nil, original_exception = nil) result = wrap_message(present(message, env)) rescue_options = options[:rescue_options] || {} if rescue_options[:backtrace] && backtrace && !backtrace.empty? result = result.merge(backtrace: backtrace) end if rescue_options[:original_exception] && original_exception result = result.merge(original_exception: original_exception.inspect) end ::Grape::Json.dump(result) end private def wrap_message(message) if message.is_a?(Exceptions::ValidationErrors) || message.is_a?(Hash) message else { error: message } end end end end end end grape-1.5.2/lib/grape/parser.rb0000644000004100000410000000123314016744305016335 0ustar www-datawww-data# frozen_string_literal: true module Grape module Parser extend Util::Registrable class << self def builtin_parsers @builtin_parsers ||= { json: Grape::Parser::Json, jsonapi: Grape::Parser::Json, xml: Grape::Parser::Xml } end def parsers(**options) builtin_parsers.merge(default_elements).merge!(options[:parsers] || {}) end def parser_for(api_format, **options) spec = parsers(**options)[api_format] case spec when nil nil when Symbol method(spec) else spec end end end end end grape-1.5.2/lib/grape/middleware/0000755000004100000410000000000014016744305016632 5ustar www-datawww-datagrape-1.5.2/lib/grape/middleware/formatter.rb0000644000004100000410000001363614016744305021173 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' module Grape module Middleware class Formatter < Base CHUNKED = 'chunked' def default_options { default_format: :txt, formatters: {}, parsers: {} } end def before negotiate_content_type read_body_input end def after return unless @app_response status, headers, bodies = *@app_response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) @app_response else build_formatted_response(status, headers, bodies) end end private def build_formatted_response(status, headers, bodies) headers = ensure_content_type(headers) if bodies.is_a?(Grape::ServeStream::StreamResponse) Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp| resp.body = bodies.stream end else # Allow content-type to be explicitly overwritten formatter = fetch_formatter(headers, options) bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do bodies.collect { |body| formatter.call(body, env) } end Rack::Response.new(bodymap, status, headers) end rescue Grape::Exceptions::InvalidFormatter => e throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e end def fetch_formatter(headers, options) api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT] Grape::Formatter.formatter_for(api_format, **options) end # Set the content type header for the API format if it is not already present. # # @param headers [Hash] # @return [Hash] def ensure_content_type(headers) if headers[Grape::Http::Headers::CONTENT_TYPE] headers else headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT])) end end def request @request ||= Rack::Request.new(env) end # store read input in env['api.request.input'] def read_body_input return unless (request.post? || request.put? || request.patch? || request.delete?) && (!request.form_data? || !request.media_type) && !request.parseable_data? && (request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED) return unless (input = env[Grape::Env::RACK_INPUT]) input.rewind body = env[Grape::Env::API_REQUEST_INPUT] = input.read begin read_rack_input(body) if body && !body.empty? ensure input.rewind end end # store parsed input in env['api.request.body'] def read_rack_input(body) fmt = request.media_type ? mime_types[request.media_type] : options[:default_format] unless content_type_for(fmt) throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." end parser = Grape::Parser.parser_for fmt, **options if parser begin body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env)) if body.is_a?(Hash) env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env[Grape::Env::RACK_REQUEST_FORM_HASH] env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body) else body end env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT] end rescue Grape::Exceptions::Base => e raise e rescue StandardError => e throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e end else env[Grape::Env::API_REQUEST_BODY] = body end end def negotiate_content_type fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format] if content_type_for(fmt) env[Grape::Env::API_FORMAT] = fmt else throw :error, status: 406, message: "The requested format '#{fmt}' is not supported." end end def format_from_extension parts = request.path.split('.') if parts.size > 1 extension = parts.last # avoid symbol memory leak on an unknown format return extension.to_sym if content_type_for(extension) end nil end def format_from_params fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT] # avoid symbol memory leak on an unknown format return fmt.to_sym if content_type_for(fmt) fmt end def format_from_header mime_array.each do |t| return mime_types[t] if mime_types.key?(t) end nil end def mime_array accept = env[Grape::Http::Headers::HTTP_ACCEPT] return [] unless accept accept_into_mime_and_quality = %r{ ( \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml (?: (?:;[^,]*?)? # optionally multiple formats in a row ;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5) )? }x vendor_prefix_pattern = /vnd\.[^+]+\+/ accept.scan(accept_into_mime_and_quality) .sort_by { |_, quality_preference| -quality_preference.to_f } .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] } end end end end grape-1.5.2/lib/grape/middleware/helpers.rb0000644000004100000410000000033614016744305020623 0ustar www-datawww-data# frozen_string_literal: true module Grape module Middleware # Common methods for all types of Grape middleware module Helpers def context env[Grape::Env::API_ENDPOINT] end end end end grape-1.5.2/lib/grape/middleware/base.rb0000644000004100000410000000467014016744305020100 0ustar www-datawww-data# frozen_string_literal: true require 'grape/dsl/headers' module Grape module Middleware class Base include Helpers attr_reader :app, :env, :options TEXT_HTML = 'text/html' include Grape::DSL::Headers # @param [Rack Application] app The standard argument for a Rack middleware. # @param [Hash] options A hash of options, simply stored for use by subclasses. def initialize(app, *options) @app = app @options = options.any? ? default_options.merge(options.shift) : default_options @app_response = nil end def default_options {} end def call(env) dup.call!(env).to_a end def call!(env) @env = env before begin @app_response = @app.call(@env) ensure begin after_response = after rescue StandardError => e warn "caught error of type #{e.class} in after callback inside #{self.class.name} : #{e.message}" raise e end end response = after_response || @app_response merge_headers response response end # @abstract # Called before the application is called in the middleware lifecycle. def before; end # @abstract # Called after the application is called in the middleware lifecycle. # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards. def after; end def response return @app_response if @app_response.is_a?(Rack::Response) Rack::Response.new(@app_response[2], @app_response[0], @app_response[1]) end def content_type_for(format) HashWithIndifferentAccess.new(content_types)[format] end def content_types ContentTypes.content_types_for(options[:content_types]) end def content_type content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML end def mime_types @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params| types_without_params[v.split(';').first] = k end end private def merge_headers(response) return unless headers.is_a?(Hash) case response when Rack::Response then response.headers.merge!(headers) when Array then response[1].merge!(headers) end end end end end grape-1.5.2/lib/grape/middleware/error.rb0000644000004100000410000001221714016744305020313 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' require 'active_support/core_ext/string/output_safety' module Grape module Middleware class Error < Base def default_options { default_status: 500, # default status returned on error default_message: '', format: :txt, helpers: nil, formatters: {}, error_formatters: {}, rescue_all: false, # true to rescue all exceptions rescue_grape_exceptions: false, rescue_subclasses: true, # rescue subclasses of exceptions listed rescue_options: { backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions original_exception: false # true to display exception }, rescue_handlers: {}, # rescue handler blocks base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class all_rescue_handler: nil # rescue handler block to rescue from all exceptions } end def initialize(app, *options) super self.class.send(:include, @options[:helpers]) if @options[:helpers] end def call!(env) @env = env begin error_response(catch(:error) do return @app.call(@env) end) rescue Exception => e # rubocop:disable Lint/RescueException handler = rescue_handler_for_base_only_class(e.class) || rescue_handler_for_class_or_its_ancestor(e.class) || rescue_handler_for_grape_exception(e.class) || rescue_handler_for_any_class(e.class) || raise run_rescue_handler(handler, e) end end def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil) headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type) rack_response(format_message(message, backtrace, original_exception), status, headers) end def default_rescue_handler(e) error_response(message: e.message, backtrace: e.backtrace, original_exception: e) end # TODO: This method is deprecated. Refactor out. def error_response(error = {}) status = error[:status] || options[:default_status] message = error[:message] || options[:default_message] headers = { Grape::Http::Headers::CONTENT_TYPE => content_type } headers.merge!(error[:headers]) if error[:headers].is_a?(Hash) backtrace = error[:backtrace] || error[:original_exception]&.backtrace || [] original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil rack_response(format_message(message, backtrace, original_exception), status, headers) end def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }) message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML Rack::Response.new([message], status, headers) end def format_message(message, backtrace, original_exception = nil) format = env[Grape::Env::API_FORMAT] || options[:format] formatter = Grape::ErrorFormatter.formatter_for(format, **options) throw :error, status: 406, message: "The requested format '#{format}' is not supported.", backtrace: backtrace, original_exception: original_exception unless formatter formatter.call(message, backtrace, options, env, original_exception) end private def rescue_handler_for_base_only_class(klass) error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err } return unless error handler || :default_rescue_handler end def rescue_handler_for_class_or_its_ancestor(klass) error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err } return unless error handler || :default_rescue_handler end def rescue_handler_for_grape_exception(klass) return unless klass <= Grape::Exceptions::Base return :error_response if klass == Grape::Exceptions::InvalidVersionHeader return unless options[:rescue_grape_exceptions] || !options[:rescue_all] :error_response end def rescue_handler_for_any_class(klass) return unless klass <= StandardError return unless options[:rescue_all] || options[:rescue_grape_exceptions] options[:all_rescue_handler] || :default_rescue_handler end def run_rescue_handler(handler, error) if handler.instance_of?(Symbol) raise NoMethodError, "undefined method `#{handler}'" unless respond_to?(handler) handler = public_method(handler) end response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler) if response.is_a?(Rack::Response) response else run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new) end end end end end grape-1.5.2/lib/grape/middleware/globals.rb0000644000004100000410000000072714016744305020610 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' module Grape module Middleware class Globals < Base def before request = Grape::Request.new(@env, build_params_with: @options[:build_params_with]) @env[Grape::Env::GRAPE_REQUEST] = request @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Grape::Env::RACK_INPUT] end end end end grape-1.5.2/lib/grape/middleware/filter.rb0000644000004100000410000000076614016744305020455 0ustar www-datawww-data# frozen_string_literal: true module Grape module Middleware # This is a simple middleware for adding before and after filters # to Grape APIs. It is used like so: # # use Grape::Middleware::Filter, before: -> { do_something }, after: -> { do_something } class Filter < Base def before app.instance_eval(&options[:before]) if options[:before] end def after app.instance_eval(&options[:after]) if options[:after] end end end end grape-1.5.2/lib/grape/middleware/versioner.rb0000644000004100000410000000163214016744305021175 0ustar www-datawww-data# frozen_string_literal: true # Versioners set env['api.version'] when a version is defined on an API and # on the requests. The current methods for determining version are: # # :header - version from HTTP Accept header. # :path - version from uri. e.g. /v1/resource # :param - version from uri query string, e.g. /v1/resource?apiver=v1 # # See individual classes for details. module Grape module Middleware module Versioner module_function # @param strategy [Symbol] :path, :header or :param # @return a middleware class based on strategy def using(strategy) case strategy when :path Path when :header Header when :param Param when :accept_version_header AcceptVersionHeader else raise Grape::Exceptions::InvalidVersionerOption.new(strategy) end end end end end grape-1.5.2/lib/grape/middleware/stack.rb0000644000004100000410000000613314016744305020267 0ustar www-datawww-data# frozen_string_literal: true module Grape module Middleware # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack # It allows to insert and insert after class Stack class Middleware attr_reader :args, :block, :klass def initialize(klass, *args, &block) @klass = klass @args = args @block = block end def name klass.name end def ==(other) case other when Middleware klass == other.klass when Class klass == other || (name.nil? && klass.superclass == other) end end def inspect klass.to_s end def use_in(builder) builder.use(@klass, *@args, &@block) end end include Enumerable attr_accessor :middlewares, :others def initialize @middlewares = [] @others = [] end def each @middlewares.each { |x| yield x } end def size middlewares.size end def last middlewares.last end def [](i) middlewares[i] end def insert(index, *args, &block) index = assert_index(index, :before) middleware = self.class::Middleware.new(*args, &block) middlewares.insert(index, middleware) end ruby2_keywords :insert if respond_to?(:ruby2_keywords, true) alias insert_before insert def insert_after(index, *args, &block) index = assert_index(index, :after) insert(index + 1, *args, &block) end ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true) def use(*args, &block) middleware = self.class::Middleware.new(*args, &block) middlewares.push(middleware) end ruby2_keywords :use if respond_to?(:ruby2_keywords, true) def merge_with(middleware_specs) middleware_specs.each do |operation, *args| if args.last.is_a?(Proc) last_proc = args.pop public_send(operation, *args, &last_proc) else public_send(operation, *args) end end end # @return [Rack::Builder] the builder object with our middlewares applied def build(builder = Rack::Builder.new) others.shift(others.size).each(&method(:merge_with)) middlewares.each do |m| m.use_in(builder) end builder end # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]]) def concat(other_specs) @others << Array(other_specs).reject { |o| o.first == :use } merge_with(Array(other_specs).select { |o| o.first == :use }) end protected def assert_index(index, where) i = index.is_a?(Integer) ? index : middlewares.index(index) i || raise("No such middleware to insert #{where}: #{index.inspect}") end end end end grape-1.5.2/lib/grape/middleware/auth/0000755000004100000410000000000014016744305017573 5ustar www-datawww-datagrape-1.5.2/lib/grape/middleware/auth/strategies.rb0000644000004100000410000000125614016744305022276 0ustar www-datawww-data# frozen_string_literal: true module Grape module Middleware module Auth module Strategies module_function def add(label, strategy, option_fetcher = ->(_) { [] }) auth_strategies[label] = StrategyInfo.new(strategy, option_fetcher) end def auth_strategies @auth_strategies ||= { http_basic: StrategyInfo.new(Rack::Auth::Basic, ->(settings) { [settings[:realm]] }), http_digest: StrategyInfo.new(Rack::Auth::Digest::MD5, ->(settings) { [settings[:realm], settings[:opaque]] }) } end def [](label) auth_strategies[label] end end end end end grape-1.5.2/lib/grape/middleware/auth/base.rb0000644000004100000410000000173614016744305021041 0ustar www-datawww-data# frozen_string_literal: true require 'rack/auth/basic' module Grape module Middleware module Auth class Base include Helpers attr_accessor :options, :app, :env def initialize(app, *options) @app = app @options = options.shift end def call(env) dup._call(env) end def _call(env) self.env = env if options.key?(:type) auth_proc = options[:proc] auth_proc_context = context strategy_info = Grape::Middleware::Auth::Strategies[options[:type]] throw(:error, status: 401, message: 'API Authorization Failed.') unless strategy_info.present? strategy = strategy_info.create(@app, options) do |*args| auth_proc_context.instance_exec(*args, &auth_proc) end strategy.call(env) else app.call(env) end end end end end end grape-1.5.2/lib/grape/middleware/auth/dsl.rb0000644000004100000410000000241614016744305020705 0ustar www-datawww-data# frozen_string_literal: true require 'rack/auth/basic' require 'active_support/concern' module Grape module Middleware module Auth module DSL extend ActiveSupport::Concern module ClassMethods # Add an authentication type to the API. Currently # only `:http_basic`, `:http_digest` are supported. def auth(type = nil, options = {}, &block) if type namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block)) use Grape::Middleware::Auth::Base, namespace_inheritable(:auth) else namespace_inheritable(:auth) end end # Add HTTP Basic authorization to the API. # # @param [Hash] options A hash of options. # @option options [String] :realm "API Authorization" The HTTP Basic realm. def http_basic(options = {}, &block) options[:realm] ||= 'API Authorization' auth :http_basic, options, &block end def http_digest(options = {}, &block) options[:realm] ||= 'API Authorization' options[:opaque] ||= 'secret' auth :http_digest, options, &block end end end end end end grape-1.5.2/lib/grape/middleware/auth/strategy_info.rb0000644000004100000410000000052414016744305022776 0ustar www-datawww-data# frozen_string_literal: true module Grape module Middleware module Auth StrategyInfo = Struct.new(:auth_class, :settings_fetcher) do def create(app, options, &block) strategy_args = settings_fetcher.call(options) auth_class.new(app, *strategy_args, &block) end end end end end grape-1.5.2/lib/grape/middleware/versioner/0000755000004100000410000000000014016744305020646 5ustar www-datawww-datagrape-1.5.2/lib/grape/middleware/versioner/parse_media_type_patch.rb0000644000004100000410000000131314016744305025662 0ustar www-datawww-data# frozen_string_literal: true module Rack module Accept module Header ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze class << self # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44 def parse_media_type(media_type) # see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names m = media_type&.match(ALLOWED_CHARACTERS) m ? [m[1], m[2], m[3] || ''] : [] end end end class MediaType def parse_media_type(media_type) Header.parse_media_type(media_type) end end end end grape-1.5.2/lib/grape/middleware/versioner/param.rb0000644000004100000410000000334514016744305022300 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the request parameters and removes that parameter from the # request parameters for subsequent middleware and API. # If the version substring does not match any potential initialized # versions, a 404 error is thrown. # If the version substring is not passed the version (highest mounted) # version will be used. # # Example: For a uri path # /resource?apiver=v1 # # The following rack env variables are set and path is rewritten to # '/resource': # # env['api.version'] => 'v1' class Param < Base def default_options { version_options: { parameter: 'apiver' } } end def before potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey] return if potential_version.nil? throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } env[Grape::Env::API_VERSION] = potential_version env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH end private def paramkey version_options[:parameter] || default_options[:version_options][:parameter] end def version_options options[:version_options] end end end end end grape-1.5.2/lib/grape/middleware/versioner/header.rb0000644000004100000410000001517314016744305022432 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' require 'grape/middleware/versioner/parse_media_type_patch' module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the HTTP Accept header with the pattern: # application/vnd.:vendor-:version+:format # # Example: For request header # Accept: application/vnd.mycompany.a-cool-resource-v1+json # # The following rack env variables are set: # # env['api.type'] => 'application' # env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json' # env['api.vendor] => 'mycompany.a-cool-resource' # env['api.version] => 'v1' # env['api.format] => 'json' # # If version does not match this route, then a 406 is raised with # X-Cascade header to alert Grape::Router to attempt the next matched # route. class Header < Base VENDOR_VERSION_HEADER_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/.freeze HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/.freeze def before strict_header_checks if strict? if media_type || env[Grape::Env::GRAPE_ALLOWED_METHODS] media_type_header_handler elsif headers_contain_wrong_vendor? fail_with_invalid_accept_header!('API vendor not found.') elsif headers_contain_wrong_version? fail_with_invalid_version_header!('API version not found.') end end private def strict_header_checks strict_accept_header_presence_check strict_version_vendor_accept_header_presence_check end def strict_accept_header_presence_check return unless header.qvalues.empty? fail_with_invalid_accept_header!('Accept header must be set.') end def strict_version_vendor_accept_header_presence_check return unless versions.present? return if an_accept_header_with_version_and_vendor_is_present? fail_with_invalid_accept_header!('API vendor or version not found.') end def an_accept_header_with_version_and_vendor_is_present? header.qvalues.keys.any? do |h| VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', '')) end end def header @header ||= rack_accept_header end def media_type @media_type ||= header.best_of(available_media_types) end def media_type_header_handler type, subtype = Rack::Accept::Header.parse_media_type(media_type) env[Grape::Env::API_TYPE] = type env[Grape::Env::API_SUBTYPE] = subtype return unless VENDOR_VERSION_HEADER_REGEX =~ subtype env[Grape::Env::API_VENDOR] = Regexp.last_match[1] env[Grape::Env::API_VERSION] = Regexp.last_match[2] # weird that Grape::Middleware::Formatter also does this env[Grape::Env::API_FORMAT] = Regexp.last_match[3] end def fail_with_invalid_accept_header!(message) raise Grape::Exceptions::InvalidAcceptHeader .new(message, error_headers) end def fail_with_invalid_version_header!(message) raise Grape::Exceptions::InvalidVersionHeader .new(message, error_headers) end def available_media_types available_media_types = [] content_types.each_key do |extension| 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_value do |media_type| available_media_types << media_type end available_media_types.flatten end def headers_contain_wrong_vendor? header.values.all? do |header_value| vendor?(header_value) && request_vendor(header_value) != vendor end end def headers_contain_wrong_version? header.values.all? do |header_value| version?(header_value) && !versions.include?(request_version(header_value)) end end def rack_accept_header Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT] rescue RuntimeError => e fail_with_invalid_accept_header!(e.message) end def versions options[:versions] || [] end def vendor version_options && version_options[:vendor] end def strict? version_options && version_options[:strict] end def version_options options[:version_options] end # By default those errors contain an `X-Cascade` header set to `pass`, # which allows nesting and stacking of routes # (see Grape::Router for more # information). To prevent # this behavior, and not add the `X-Cascade` # header, one can set the `:cascade` option to `false`. def cascade? if version_options && version_options.key?(:cascade) version_options[:cascade] else true end end def error_headers cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} end # @param [String] media_type a content type # @return [Boolean] whether the content type sets a vendor def vendor?(media_type) _, subtype = Rack::Accept::Header.parse_media_type(media_type) subtype.present? && subtype[HAS_VENDOR_REGEX] end def request_vendor(media_type) _, subtype = Rack::Accept::Header.parse_media_type(media_type) subtype.match(VENDOR_VERSION_HEADER_REGEX)[1] end def request_version(media_type) _, subtype = Rack::Accept::Header.parse_media_type(media_type) subtype.match(VENDOR_VERSION_HEADER_REGEX)[2] end # @param [String] media_type a content type # @return [Boolean] whether the content type sets an API version def version?(media_type) _, subtype = Rack::Accept::Header.parse_media_type(media_type) subtype.present? && subtype[HAS_VERSION_REGEX] end end end end end grape-1.5.2/lib/grape/middleware/versioner/accept_version_header.rb0000644000004100000410000000417314016744305025514 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the HTTP Accept-Version header # # Example: For request header # Accept-Version: v1 # # The following rack env variables are set: # # env['api.version'] => 'v1' # # If version does not match this route, then a 406 is raised with # X-Cascade header to alert Grape::Router to attempt the next matched # route. class AcceptVersionHeader < Base def before potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip if strict? # If no Accept-Version header: if potential_version.empty? throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.' end end return if potential_version.empty? # If the requested version is not supported: throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version } env[Grape::Env::API_VERSION] = potential_version end private def versions options[:versions] || [] end def strict? options[:version_options] && options[:version_options][:strict] end # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking # of routes (see Grape::Router) for more information). To prevent # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. def cascade? if options[:version_options] && options[:version_options].key?(:cascade) options[:version_options][:cascade] else true end end def error_headers cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {} end end end end end grape-1.5.2/lib/grape/middleware/versioner/path.rb0000644000004100000410000000361414016744305022133 0ustar www-datawww-data# frozen_string_literal: true require 'grape/middleware/base' module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the uri path and removes the version substring from the uri # path. If the version substring does not match any potential initialized # versions, a 404 error is thrown. # # Example: For a uri path # /v1/resource # # The following rack env variables are set and path is rewritten to # '/resource': # # env['api.version'] => 'v1' # class Path < Base def default_options { pattern: /.*/i } end def before path = env[Grape::Http::Headers::PATH_INFO].dup path.sub!(mount_path, '') if mounted_path?(path) if prefix && path.index(prefix) == 0 # rubocop:disable all path.sub!(prefix, '') path = Grape::Router.normalize_path(path) end pieces = path.split('/') potential_version = pieces[1] return unless potential_version&.match?(options[:pattern]) throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } env[Grape::Env::API_VERSION] = potential_version end private def mounted_path?(path) return false unless mount_path && path.start_with?(mount_path) rest = path.slice(mount_path.length..-1) rest.start_with?('/') || rest.empty? end def mount_path @mount_path ||= options[:mount_path] && options[:mount_path] != '/' ? options[:mount_path] : '' end def prefix Grape::Router.normalize_path(options[:prefix].to_s) if options[:prefix] end end end end end grape-1.5.2/lib/grape/endpoint.rb0000644000004100000410000003470714016744305016675 0ustar www-datawww-data# frozen_string_literal: true module Grape # An Endpoint is the proxy scope in which all routing # blocks are executed. In other words, any methods # on the instance level of this class may be called # from inside a `get`, `post`, etc. class Endpoint include Grape::DSL::Settings include Grape::DSL::InsideRoute attr_accessor :block, :source, :options attr_reader :env, :request, :headers, :params class << self def new(*args, &block) self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super end def before_each(new_setup = false, &block) @before_each ||= [] if new_setup == false return @before_each unless block_given? @before_each << block else @before_each = [new_setup] end end def run_before_each(endpoint) superclass.run_before_each(endpoint) unless self == Endpoint before_each.each { |blk| blk.call(endpoint) if blk.respond_to?(:call) } end # @api private # # Create an UnboundMethod that is appropriate for executing an endpoint # route. # # The unbound method allows explicit calls to +return+ without raising a # +LocalJumpError+. The method will be removed, but a +Proc+ reference to # it will be returned. The returned +Proc+ expects a single argument: the # instance of +Endpoint+ to bind to the method during the call. # # @param [String, Symbol] method_name # @return [Proc] # @raise [NameError] an instance method with the same name already exists def generate_api_method(method_name, &block) if method_defined?(method_name) raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") end define_method(method_name, &block) method = instance_method(method_name) remove_method(method_name) proc do |endpoint_instance| ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do method.bind(endpoint_instance).call end end end end # Create a new endpoint. # @param new_settings [InheritableSetting] settings to determine the params, # validations, and other properties from. # @param options [Hash] attributes of this endpoint # @option options path [String or Array] the path to this endpoint, within # the current scope. # @option options method [String or Array] which HTTP method(s) can be used # to reach this endpoint. # @option options route_options [Hash] # @note This happens at the time of API definition, so in this context the # endpoint does not know if it will be mounted under a different endpoint. # @yield a block defining what your API should do when this endpoint is hit def initialize(new_settings, options = {}, &block) require_option(options, :path) require_option(options, :method) self.inheritable_setting = new_settings.point_in_time_copy # now +namespace_stackable(:declared_params)+ contains all params defined for # this endpoint and its parents, but later it will be cleaned up, # see +reset_validations!+ in lib/grape/dsl/validations.rb route_setting(:declared_params, namespace_stackable(:declared_params).flatten) route_setting(:saved_validations, namespace_stackable(:validations)) namespace_stackable(:representations, []) unless namespace_stackable(:representations) namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status) @options = options @options[:path] = Array(options[:path]) @options[:path] << '/' if options[:path].empty? @options[:method] = Array(options[:method]) @options[:route_options] ||= {} @lazy_initialize_lock = Mutex.new @lazy_initialized = nil @block = nil @status = nil @stream = nil @body = nil @proc = nil return unless block_given? @source = block @block = self.class.generate_api_method(method_name, &block) end # Update our settings from a given set of stackable parameters. Used when # the endpoint's API is mounted under another one. def inherit_settings(namespace_stackable) inheritable_setting.route[:saved_validations] += namespace_stackable[:validations] parent_declared_params = namespace_stackable[:declared_params] if parent_declared_params inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) end endpoints && endpoints.each { |e| e.inherit_settings(namespace_stackable) } end def require_option(options, key) raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key) end def method_name [options[:method], Namespace.joined_space(namespace_stackable(:namespace)), (namespace_stackable(:mount_path) || []).join('/'), options[:path].join('/')] .join(' ') end def routes @routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes end def reset_routes! endpoints.each(&:reset_routes!) if endpoints @namespace = nil @routes = nil end def mount_in(router) if endpoints endpoints.each { |e| e.mount_in(router) } else reset_routes! routes.each do |route| methods = [route.request_method] if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET methods << Grape::Http::Headers::HEAD end methods.each do |method| unless route.request_method == method route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) end router.append(route.apply(self)) end end end end def to_routes route_options = prepare_default_route_attributes map_routes do |method, path| path = prepare_path(path) params = merge_route_options(**route_options.merge(suffix: path.suffix)) route = Router::Route.new(method, path.path, **params) route.apply(self) end.flatten end def prepare_routes_requirements endpoint_requirements = options[:route_options][:requirements] || {} all_requirements = (namespace_stackable(:namespace).map(&:requirements) << endpoint_requirements) all_requirements.reduce({}) do |base_requirements, single_requirements| base_requirements.merge!(single_requirements) end end def prepare_default_route_attributes { namespace: namespace, version: prepare_version, requirements: prepare_routes_requirements, prefix: namespace_inheritable(:root_prefix), anchor: options[:route_options].fetch(:anchor, true), settings: inheritable_setting.route.except(:declared_params, :saved_validations), forward_match: options[:forward_match] } end def prepare_version version = namespace_inheritable(:version) || [] return if version.empty? version.length == 1 ? version.first.to_s : version end def merge_route_options(**default) options[:route_options].clone.merge!(**default) end def map_routes options[:method].map { |method| options[:path].map { |path| yield method, path } } end def prepare_path(path) path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable]) Path.prepare(path, namespace, path_settings) end def namespace @namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace)) end def call(env) lazy_initialize! dup.call!(env) end def call!(env) env[Grape::Env::API_ENDPOINT] = self @env = env @app.call(env) end # Return the collection of endpoints within this endpoint. # This is the case when an Grape::API mounts another Grape::API. def endpoints options[:app].endpoints if options[:app] && options[:app].respond_to?(:endpoints) end def equals?(e) (options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash) end protected def run ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do @header = {} @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with)) @params = @request.params @headers = @request.headers begin cookies.read(@request) self.class.run_before_each(self) run_filters befores, :before if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS]) raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options? header 'Allow', allowed_methods response_object = '' status 204 else run_filters before_validations, :before_validation run_validators validations, request remove_renamed_params run_filters after_validations, :after_validation response_object = execute end run_filters afters, :after cookies.write(header) # status verifies body presence when DELETE @body ||= response_object # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object response_object = stream || [body] [status, header, response_object] ensure run_filters finallies, :finally end end end def build_stack(helpers) stack = Grape::Middleware::Stack.new stack.use Rack::Head stack.use Class.new(Grape::Middleware::Error), helpers: helpers, format: namespace_inheritable(:format), content_types: namespace_stackable_with_hash(:content_types), default_status: namespace_inheritable(:default_error_status), rescue_all: namespace_inheritable(:rescue_all), rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions), default_error_formatter: namespace_inheritable(:default_error_formatter), error_formatters: namespace_stackable_with_hash(:error_formatters), rescue_options: namespace_stackable_with_hash(:rescue_options) || {}, rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {}, base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {}, all_rescue_handler: namespace_inheritable(:all_rescue_handler) stack.concat namespace_stackable(:middleware) if namespace_inheritable(:version) stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]), versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil, version_options: namespace_inheritable(:version_options), prefix: namespace_inheritable(:root_prefix), mount_path: namespace_stackable(:mount_path).first end stack.use Grape::Middleware::Formatter, format: namespace_inheritable(:format), default_format: namespace_inheritable(:default_format) || :txt, content_types: namespace_stackable_with_hash(:content_types), formatters: namespace_stackable_with_hash(:formatters), parsers: namespace_stackable_with_hash(:parsers) builder = stack.build builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run } builder.to_app end def build_helpers helpers = namespace_stackable(:helpers) || [] Module.new { helpers.each { |mod_to_include| include mod_to_include } } end def remove_renamed_params return unless route_setting(:renamed_params) route_setting(:renamed_params).flat_map(&:keys).each do |renamed_param| @params.delete(renamed_param) end end private :build_stack, :build_helpers, :remove_renamed_params def execute @block ? @block.call(self) : nil end def helpers lazy_initialize! && @helpers end def lazy_initialize! return true if @lazy_initialized @lazy_initialize_lock.synchronize do return true if @lazy_initialized @helpers = build_helpers.tap { |mod| self.class.send(:include, mod) } @app = options[:app] || build_stack(@helpers) @lazy_initialized = true end end def run_validators(validator_factories, request) validation_errors = [] validators = validator_factories.map { |options| Grape::Validations::ValidatorFactory.create_validator(**options) } ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do validators.each do |validator| begin validator.validate(request) rescue Grape::Exceptions::Validation => e validation_errors << e break if validator.fail_fast? rescue Grape::Exceptions::ValidationArrayErrors => e validation_errors.concat e.errors break if validator.fail_fast? end end end validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header)) end def run_filters(filters, type = :other) ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do (filters || []).each { |filter| instance_eval(&filter) } end post_extension = DSL::InsideRoute.post_filter_methods(type) extend post_extension if post_extension end def befores namespace_stackable(:befores) || [] end def before_validations namespace_stackable(:before_validations) || [] end def after_validations namespace_stackable(:after_validations) || [] end def afters namespace_stackable(:afters) || [] end def finallies namespace_stackable(:finallies) || [] end def validations route_setting(:saved_validations) || [] end def options? options[:options_route_enabled] && env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS end end end grape-1.5.2/lib/grape/exceptions/0000755000004100000410000000000014016744305016676 5ustar www-datawww-datagrape-1.5.2/lib/grape/exceptions/unknown_parameter.rb0000644000004100000410000000034414016744305022763 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class UnknownParameter < Base def initialize(param) super(message: compose_message(:unknown_parameter, param: param)) end end end end grape-1.5.2/lib/grape/exceptions/validation.rb0000644000004100000410000000132314016744305021354 0ustar www-datawww-data# frozen_string_literal: true require 'grape/exceptions/base' module Grape module Exceptions class Validation < Grape::Exceptions::Base attr_accessor :params attr_accessor :message_key def initialize(params:, message: nil, **args) @params = params if message @message_key = message if message.is_a?(Symbol) args[:message] = translate_message(message) end super(**args) end # Remove all the unnecessary stuff from Grape::Exceptions::Base like status # and headers when converting a validation error to json or string def as_json(*_args) to_s end def to_s message end end end end grape-1.5.2/lib/grape/exceptions/missing_mime_type.rb0000644000004100000410000000036214016744305022745 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class MissingMimeType < Base def initialize(new_format) super(message: compose_message(:missing_mime_type, new_format: new_format)) end end end end grape-1.5.2/lib/grape/exceptions/missing_group_type.rb0000644000004100000410000000032514016744305023151 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class MissingGroupTypeError < Base def initialize super(message: compose_message(:missing_group_type)) end end end end grape-1.5.2/lib/grape/exceptions/invalid_versioner_option.rb0000644000004100000410000000037214016744305024337 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidVersionerOption < Base def initialize(strategy) super(message: compose_message(:invalid_versioner_option, strategy: strategy)) end end end end grape-1.5.2/lib/grape/exceptions/incompatible_option_values.rb0000644000004100000410000000050614016744305024641 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class IncompatibleOptionValues < Base def initialize(option1, value1, option2, value2) super(message: compose_message(:incompatible_option_values, option1: option1, value1: value1, option2: option2, value2: value2)) end end end end grape-1.5.2/lib/grape/exceptions/base.rb0000644000004100000410000000515514016744305020143 0ustar www-datawww-data# frozen_string_literal: true module 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(status: nil, message: nil, headers: nil, **_options) @status = status @message = message @headers = headers end def [](index) send index end protected # TODO: translate attribute first # if BASE_ATTRIBUTES_KEY.key respond to a string message, then short_message is returned # if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution def compose_message(key, **attributes) short_message = translate_message(key, **attributes) if short_message.is_a? Hash @problem = problem(key, **attributes) @summary = summary(key, **attributes) @resolution = resolution(key, **attributes) [['Problem', @problem], ['Summary', @summary], ['Resolution', @resolution]].each_with_object(+'') do |detail_array, message| message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank? message end else short_message end end def problem(key, **attributes) translate_message("#{key}.problem".to_sym, **attributes) end def summary(key, **attributes) translate_message("#{key}.summary".to_sym, **attributes) end def resolution(key, **attributes) translate_message("#{key}.resolution".to_sym, **attributes) end def translate_attributes(keys, **options) keys.map do |key| translate("#{BASE_ATTRIBUTES_KEY}.#{key}", default: key, **options) end.join(', ') end def translate_message(key, **options) case key when Symbol translate("#{BASE_MESSAGES_KEY}.#{key}", default: '', **options) when Proc key.call else key end end def translate(key, **options) options = options.dup options[:default] &&= options[:default].to_s message = ::I18n.translate(key, **options) message.present? ? message : fallback_message(key, **options) end def fallback_message(key, **options) if ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE) key else ::I18n.translate(key, locale: FALLBACK_LOCALE, **options) end end end end end grape-1.5.2/lib/grape/exceptions/unknown_options.rb0000644000004100000410000000034614016744305022500 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class UnknownOptions < Base def initialize(options) super(message: compose_message(:unknown_options, options: options)) end end end end grape-1.5.2/lib/grape/exceptions/invalid_formatter.rb0000644000004100000410000000040514016744305022733 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidFormatter < Base def initialize(klass, to_format) super(message: compose_message(:invalid_formatter, klass: klass, to_format: to_format)) end end end end grape-1.5.2/lib/grape/exceptions/validation_errors.rb0000644000004100000410000000255114016744305022754 0ustar www-datawww-data# frozen_string_literal: true require 'grape/exceptions/base' module Grape module Exceptions class ValidationErrors < Grape::Exceptions::Base ERRORS_FORMAT_KEY = 'grape.errors.format' DEFAULT_ERRORS_FORMAT = '%{attributes} %{message}' include Enumerable attr_reader :errors def initialize(errors: [], headers: {}, **_options) @errors = {} errors.each do |validation_error| @errors[validation_error.params] ||= [] @errors[validation_error.params] << validation_error end super message: full_messages.join(', '), status: 400, headers: headers end def each errors.each_pair do |attribute, errors| errors.each do |error| yield attribute, error end end end def as_json(**_opts) errors.map do |k, v| { params: k, messages: v.map(&:to_s) } end end def to_json(*_opts) as_json.to_json end def full_messages messages = map do |attributes, error| I18n.t( ERRORS_FORMAT_KEY, default: DEFAULT_ERRORS_FORMAT, attributes: translate_attributes(attributes), message: error.message ) end messages.uniq! messages end end end end grape-1.5.2/lib/grape/exceptions/missing_option.rb0000644000004100000410000000034114016744305022262 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class MissingOption < Base def initialize(option) super(message: compose_message(:missing_option, option: option)) end end end end grape-1.5.2/lib/grape/exceptions/missing_vendor_option.rb0000644000004100000410000000032614016744305023642 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class MissingVendorOption < Base def initialize super(message: compose_message(:missing_vendor_option)) end end end end grape-1.5.2/lib/grape/exceptions/invalid_accept_header.rb0000644000004100000410000000043114016744305023476 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidAcceptHeader < Base def initialize(message, headers) super(message: compose_message(:invalid_accept_header, message: message), status: 406, headers: headers) end end end end grape-1.5.2/lib/grape/exceptions/unknown_validator.rb0000644000004100000410000000037714016744305022776 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class UnknownValidator < Base def initialize(validator_type) super(message: compose_message(:unknown_validator, validator_type: validator_type)) end end end end grape-1.5.2/lib/grape/exceptions/invalid_message_body.rb0000644000004100000410000000041014016744305023365 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidMessageBody < Base def initialize(body_format) super(message: compose_message(:invalid_message_body, body_format: body_format), status: 400) end end end end grape-1.5.2/lib/grape/exceptions/unsupported_group_type.rb0000644000004100000410000000033514016744305024071 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class UnsupportedGroupTypeError < Base def initialize super(message: compose_message(:unsupported_group_type)) end end end end grape-1.5.2/lib/grape/exceptions/invalid_with_option_for_represent.rb0000644000004100000410000000035414016744305026233 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidWithOptionForRepresent < Base def initialize super(message: compose_message(:invalid_with_option_for_represent)) end end end end grape-1.5.2/lib/grape/exceptions/invalid_version_header.rb0000644000004100000410000000043314016744305023726 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidVersionHeader < Base def initialize(message, headers) super(message: compose_message(:invalid_version_header, message: message), status: 406, headers: headers) end end end end grape-1.5.2/lib/grape/exceptions/method_not_allowed.rb0000644000004100000410000000034514016744305023074 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class MethodNotAllowed < Base def initialize(headers) super(message: '405 Not Allowed', status: 405, headers: headers) end end end end grape-1.5.2/lib/grape/exceptions/invalid_response.rb0000644000004100000410000000031514016744305022566 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class InvalidResponse < Base def initialize super(message: compose_message(:invalid_response)) end end end end grape-1.5.2/lib/grape/exceptions/validation_array_errors.rb0000644000004100000410000000032414016744305024146 0ustar www-datawww-data# frozen_string_literal: true module Grape module Exceptions class ValidationArrayErrors < Base attr_reader :errors def initialize(errors) @errors = errors end end end end grape-1.5.2/lib/grape/cookies.rb0000644000004100000410000000155514016744305016504 0ustar www-datawww-data# frozen_string_literal: true module 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-1.5.2/lib/grape/path.rb0000644000004100000410000000404514016744305016001 0ustar www-datawww-data# frozen_string_literal: true require 'grape/util/cache' module Grape # Represents a path to an endpoint. class Path def self.prepare(raw_path, namespace, settings) Path.new(raw_path, namespace, settings) end attr_reader :raw_path, :namespace, :settings def initialize(raw_path, namespace, settings) @raw_path = raw_path @namespace = namespace @settings = settings end def mount_path settings[:mount_path] end def root_prefix split_setting(:root_prefix) end def uses_specific_format? if settings.key?(:format) && settings.key?(:content_types) (settings[:format] && Array(settings[:content_types]).size == 1) else false end end def uses_path_versioning? if settings.key?(:version) && settings[:version_options] && settings[:version_options].key?(:using) (settings[:version] && settings[:version_options][:using] == :path) else false end end def namespace? namespace&.match?(/^\S/) && namespace != '/' end def path? raw_path&.match?(/^\S/) && raw_path != '/' end def suffix if uses_specific_format? "(.#{settings[:format]})" elsif !uses_path_versioning? || (namespace? || path?) '(.:format)' else '(/.:format)' end end def path Grape::Router.normalize_path(PartsCache[parts]) end def path_with_suffix "#{path}#{suffix}" end def to_s path_with_suffix end private class PartsCache < Grape::Util::Cache def initialize @cache = Hash.new do |h, parts| h[parts] = -parts.join('/') end end end def parts parts = [mount_path, root_prefix].compact parts << ':version' if uses_path_versioning? parts << namespace.to_s parts << raw_path.to_s parts.flatten.reject { |part| part == '/' } end def split_setting(key) return if settings[key].nil? settings[key].to_s.split('/') end end end grape-1.5.2/lib/grape/config.rb0000644000004100000410000000116614016744305016313 0ustar www-datawww-data# frozen_string_literal: true module Grape module Config class Configuration ATTRIBUTES = %i[ param_builder ].freeze attr_accessor(*ATTRIBUTES) def initialize reset end def reset self.param_builder = Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder end end def self.extended(base) def base.configure block_given? ? yield(config) : config end def base.config @configuration ||= Grape::Config::Configuration.new end end end end Grape.extend Grape::Config Grape.config.reset grape-1.5.2/lib/grape/validations/0000755000004100000410000000000014016744305017032 5ustar www-datawww-datagrape-1.5.2/lib/grape/validations/attributes_iterator.rb0000644000004100000410000000363014016744305023460 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class AttributesIterator include Enumerable attr_reader :scope def initialize(validator, scope, params) @scope = scope @attrs = validator.attrs @original_params = scope.params(params) @params = Array.wrap(@original_params) end def each(&block) do_each(@params, &block) # because we need recursion for nested arrays end private def do_each(params_to_process, parent_indicies = [], &block) params_to_process.each_with_index do |resource_params, index| # when we get arrays of arrays it means that target element located inside array # we need this because we want to know parent arrays indicies if resource_params.is_a?(Array) do_each(resource_params, [index] + parent_indicies, &block) next end if @scope.type == Array next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array # fill current and parent scopes with correct array indicies parent_scope = @scope.parent parent_indicies.each do |parent_index| parent_scope.index = parent_index parent_scope = parent_scope.parent end @scope.index = index end yield_attributes(resource_params, @attrs, &block) end end def yield_attributes(_resource_params, _attrs) raise NotImplementedError end # This is a special case so that we can ignore tree's where option # values are missing lower down. Unfortunately we can remove this # are the parameter parsing stage as they are required to ensure # the correct indexing is maintained def skip?(val) val == Grape::DSL::Parameters::EmptyOptionalValue end end end end grape-1.5.2/lib/grape/validations/types/0000755000004100000410000000000014016744305020176 5ustar www-datawww-datagrape-1.5.2/lib/grape/validations/types/array_coercer.rb0000644000004100000410000000334314016744305023346 0ustar www-datawww-data# frozen_string_literal: true require_relative 'dry_type_coercer' module Grape module Validations module Types # Coerces elements in an array. It might be an array of strings or integers or # an array of arrays of integers. # # It could've been possible to use an +of+ # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/) # provided by dry-types. Unfortunately, it doesn't work for Grape because of # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer` # maintains Virtus behavior in coercing. class ArrayCoercer < DryTypeCoercer register_collection Array def initialize(type, strict = false) super @coercer = scope::Array @subtype = type.first end def call(_val) collection = super return collection if collection.is_a?(InvalidValue) coerce_elements collection end protected attr_reader :subtype def coerce_elements(collection) return if collection.nil? collection.each_with_index do |elem, index| return InvalidValue.new if reject?(elem) coerced_elem = elem_coercer.call(elem) return coerced_elem if coerced_elem.is_a?(InvalidValue) collection[index] = coerced_elem end collection end # This method maintains logic which was defined by Virtus for arrays. # Virtus doesn't allow nil in arrays. def reject?(val) val.nil? end def elem_coercer @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict) end end end end end grape-1.5.2/lib/grape/validations/types/multiple_type_coercer.rb0000644000004100000410000000421314016744305025121 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations module Types # This class is intended for use with Grape endpoint parameters that # have been declared to be of variant-type using the +:types+ option. # +MultipleTypeCoercer+ will build a coercer for each type declared # in the array passed to +:types+ using {Types.build_coercer}. It will # apply these coercers to parameter values in the order given to # +:types+, and will return the value returned by the first coercer # to successfully coerce the parameter value. Therefore if +String+ is # an allowed type it should be declared last, since it will always # successfully "coerce" the value. class MultipleTypeCoercer # Construct a new coercer that will attempt to coerce # values to the given list of types in the given order. # # @param types [Array] list of allowed types # @param method [#call,#parse] method by which values should be # coerced. See class docs for default behaviour. def initialize(types, method = nil) @method = method.respond_to?(:parse) ? method.method(:parse) : method @type_coercers = types.map do |type| if Types.multiple? type VariantCollectionCoercer.new type, @method else Types.build_coercer type, strict: !@method.nil? end end end # Coerces the given value. # # @param val [String] value to be coerced, in grape # this should always be a string. # @return [Object,InvalidValue] the coerced result, or an instance # of {InvalidValue} if the value could not be coerced. def call(val) # once the value is coerced by the custom method, its type should be checked val = @method.call(val) if @method coerced_val = InvalidValue.new @type_coercers.each do |coercer| coerced_val = coercer.call(val) return coerced_val unless coerced_val.is_a?(InvalidValue) end coerced_val end end end end end grape-1.5.2/lib/grape/validations/types/invalid_value.rb0000644000004100000410000000107614016744305023351 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations module Types # Instances of this class may be used as tokens to denote that a parameter value could not be # coerced. The given message will be used as a validation error. class InvalidValue attr_reader :message def initialize(message = nil) @message = message end end end end end # only exists to make it shorter for external use module Grape module Types InvalidValue = Class.new(Grape::Validations::Types::InvalidValue) end end grape-1.5.2/lib/grape/validations/types/primitive_coercer.rb0000644000004100000410000000415414016744305024241 0ustar www-datawww-data# frozen_string_literal: true require_relative 'dry_type_coercer' module Grape module Validations module Types # Coerces the given value to a type defined via a +type+ argument during # initialization. When +strict+ is true, it doesn't coerce a value but check # that it has the proper type. class PrimitiveCoercer < DryTypeCoercer MAPPING = { Grape::API::Boolean => DryTypes::Params::Bool, BigDecimal => DryTypes::Params::Decimal, # unfortunately, a +Params+ scope doesn't contain String String => DryTypes::Coercible::String }.freeze STRICT_MAPPING = { Grape::API::Boolean => DryTypes::Strict::Bool, BigDecimal => DryTypes::Strict::Decimal }.freeze def initialize(type, strict = false) super @type = type @coercer = if strict STRICT_MAPPING.fetch(type) { scope.const_get(type.name) } else MAPPING.fetch(type) { scope.const_get(type.name) } end end def call(val) return InvalidValue.new if reject?(val) return nil if val.nil? || treat_as_nil?(val) super end protected attr_reader :type # This method maintains logic which was defined by Virtus. For example, # dry-types is ok to convert an array or a hash to a string, it is supported, # but Virtus wouldn't accept it. So, this method only exists to not introduce # breaking changes. def reject?(val) (val.is_a?(Array) && type == String) || (val.is_a?(String) && type == Hash) || (val.is_a?(Hash) && type == String) end # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as # absence of a value and coerces it into nil. See a discussion there # https://github.com/ruby-grape/grape/pull/2045 def treat_as_nil?(val) val == '' && type != String end end end end end grape-1.5.2/lib/grape/validations/types/json.rb0000644000004100000410000000440314016744305021475 0ustar www-datawww-data# frozen_string_literal: true require 'json' module Grape module Validations module Types # Handles coercion and type checking for parameters that are complex # types given as JSON-encoded strings. It accepts both JSON objects # and arrays of objects, and will coerce the input to a +Hash+ # or +Array+ object respectively. In either case the Grape # validation system will apply nested validation rules to # all returned objects. class Json class << self # Coerce the input into a JSON-like data structure. # # @param input [String] a JSON-encoded parameter value # @return [Hash,Array,nil] def parse(input) return input if parsed?(input) # Allow nulls and blank strings return if input.nil? || input.match?(/^\s*$/) JSON.parse(input, symbolize_names: true) end # Checks that the input was parsed successfully # and isn't something odd such as an array of primitives. # # @param value [Object] result of {#parse} # @return [true,false] def parsed?(value) value.is_a?(::Hash) || coerced_collection?(value) end protected # Is the value an array of JSON-like objects? # # @param value [Object] result of {#parse} # @return [true,false] def coerced_collection?(value) value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash } end end end # Specialization of the {Json} attribute that is guaranteed # to return an array of objects. Accepts both JSON-encoded # objects and arrays of objects, but wraps single objects # in an Array. class JsonArray < Json class << self # See {Json#parse}. Wraps single objects in an array. # # @param input [String] JSON-encoded parameter value # @return [Array] def parse(input) json = super Array.wrap(json) unless json.nil? end # See {Json#coerced_collection?} def parsed?(value) coerced_collection? value end end end end end end grape-1.5.2/lib/grape/validations/types/custom_type_coercer.rb0000644000004100000410000001440114016744305024600 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations module Types # This class will detect type classes that implement # a class-level +parse+ method. The method should accept one # +String+ argument and should return the value coerced to # the appropriate type. The method may raise an exception if # there are any problems parsing the string. # # Alternately an optional +method+ may be supplied (see the # +coerce_with+ option of {Grape::Dsl::Parameters#requires}). # This may be any class or object implementing +parse+ or +call+, # with the same contract as described above. # # Type Checking # ------------- # # Calls to +coerced?+ will consult this class to check # that the coerced value produced above is in fact of the # expected type. By default this class performs a basic check # against the type supplied, but this behaviour will be # overridden if the class implements a class-level # +coerced?+ or +parsed?+ method. This method # will receive a single parameter that is the coerced value # and should return +true+ if the value meets type expectations. # Arbitrary assertions may be made here but the grape validation # system should be preferred. # # Alternately a proc or other object responding to +call+ may be # supplied in place of a type. This should implement the same # contract as +coerced?+, and must be supplied with a coercion # +method+. class CustomTypeCoercer # A new coercer for the given type specification # and coercion method. # # @param type [Class,#coerced?,#parsed?,#call?] # specifier for the target type. See class docs. # @param method [#parse,#call] # optional coercion method. See class docs. def initialize(type, method = nil) coercion_method = infer_coercion_method type, method @method = enforce_symbolized_keys type, coercion_method @type_check = infer_type_check(type) end # Coerces the given value. # # @param value [String] value to be coerced, in grape # this should always be a string. # @return [Object] the coerced result def call(val) return if val.nil? coerced_val = @method.call(val) return coerced_val if coerced_val.is_a?(InvalidValue) return InvalidValue.new unless coerced?(coerced_val) coerced_val end def coerced?(val) val.nil? || @type_check.call(val) end private # Determine the coercion method we're expected to use # based on the parameters given. # # @param type see #new # @param method see #new # @return [#call] coercion method def infer_coercion_method(type, method) if method if method.respond_to? :parse method.method :parse else method end else # Try to use parse() declared on the target type. # This may raise an exception, but we are out of ideas anyway. type.method :parse end end # Determine how the type validity of a coerced # value should be decided. # # @param type see #new # @return [#call] a procedure which accepts a single parameter # and returns +true+ if the passed object is of the correct type. def infer_type_check(type) # First check for special class methods if type.respond_to? :coerced? type.method :coerced? elsif type.respond_to? :parsed? type.method :parsed? elsif type.respond_to? :call # Arbitrary proc passed for type validation. # Note that this will fail unless a method is also # passed, or if the type also implements a parse() method. type elsif type.is_a?(Enumerable) lambda do |value| value.is_a?(Enumerable) && value.all? do |val| recursive_type_check(type.first, val) end end else # By default, do a simple type check ->(value) { value.is_a? type } end end def recursive_type_check(type, value) if type.is_a?(Enumerable) && value.is_a?(Enumerable) value.all? { |val| recursive_type_check(type.first, val) } else !type.is_a?(Enumerable) && value.is_a?(type) end end # Enforce symbolized keys for complex types # by wrapping the coercion method such that # any Hash objects in the immediate heirarchy # have their keys recursively symbolized. # This helps common libs such as JSON to work easily. # # @param type see #new # @param method see #infer_coercion_method # @return [#call] +method+ wrapped in an additional # key-conversion step, or just returns +method+ # itself if no conversion is deemed to be # necessary. def enforce_symbolized_keys(type, method) # Collections have all values processed individually if [Array, Set].include?(type) lambda do |val| method.call(val).tap do |new_val| new_val.map do |item| item.is_a?(Hash) ? symbolize_keys(item) : item end end end # Hash objects are processed directly elsif type == Hash lambda do |val| symbolize_keys method.call(val) end # Simple types are not processed. # This includes Array types. else method end end def symbolize_keys!(hash) hash.each_key do |key| hash[key.to_sym] = hash.delete(key) if key.respond_to?(:to_sym) end hash end def symbolize_keys(hash) hash.inject({}) do |new_hash, (key, value)| new_key = key.respond_to?(:to_sym) ? key.to_sym : key new_hash.merge!(new_key => value) end end end end end end grape-1.5.2/lib/grape/validations/types/dry_type_coercer.rb0000644000004100000410000000451714016744305024073 0ustar www-datawww-data# frozen_string_literal: true require 'dry-types' module DryTypes # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is # a container in this case. Check documentation for more information # https://dry-rb.org/gems/dry-types/1.2/getting-started/ include Dry.Types() end module Grape module Validations module Types # A base class for classes which must identify a coercer to be used. # If the +strict+ argument is true, it won't coerce the given value # but check its type. More information there # https://dry-rb.org/gems/dry-types/1.2/built-in-types/ class DryTypeCoercer class << self # Registers a collection coercer which could be found by a type, # see +collection_coercer_for+ method below. This method is meant for inheritors. def register_collection(type) DryTypeCoercer.collection_coercers[type] = self end # Returns a collection coercer which corresponds to a given type. # Example: # # collection_coercer_for(Array) # #=> Grape::Validations::Types::ArrayCoercer def collection_coercer_for(type) collection_coercers[type] end # Returns an instance of a coercer for a given type def coercer_instance_for(type, strict = false) return PrimitiveCoercer.new(type, strict) if type.class == Class # in case of a collection (Array[Integer]) the type is an instance of a collection, # so we need to figure out the actual type collection_coercer_for(type.class).new(type, strict) end protected def collection_coercers @collection_coercers ||= {} end end def initialize(type, strict = false) @type = type @strict = strict @scope = strict ? DryTypes::Strict : DryTypes::Params end # Coerces the given value to a type which was specified during # initialization as a type argument. # # @param val [Object] def call(val) return if val.nil? @coercer[val] rescue Dry::Types::CoercionError => _e InvalidValue.new end protected attr_reader :scope, :type, :strict end end end end grape-1.5.2/lib/grape/validations/types/file.rb0000644000004100000410000000170114016744305021441 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations module Types # Implementation for parameters that are multipart file objects. # Actual handling of these objects is provided by +Rack::Request+; # this class is here only to assert that rack's handling has succeeded. class File class << self def parse(input) return if input.nil? return InvalidValue.new unless parsed?(input) # Processing of multipart file objects # is already taken care of by Rack::Request. # Nothing to do here. input end def parsed?(value) # Rack::Request creates a Hash with filename, # content type and an IO object. Do a bit of basic # duck-typing. value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile) end end end end end end grape-1.5.2/lib/grape/validations/types/set_coercer.rb0000644000004100000410000000154614016744305023026 0ustar www-datawww-data# frozen_string_literal: true require 'set' require_relative 'array_coercer' module Grape module Validations module Types # Takes the given array and converts it to a set. Every element of the set # is also coerced. class SetCoercer < ArrayCoercer register_collection Set def initialize(type, strict = false) super @coercer = nil end def call(value) return InvalidValue.new unless value.is_a?(Array) coerce_elements(value) end protected def coerce_elements(collection) collection.each_with_object(Set.new) do |elem, memo| coerced_elem = elem_coercer.call(elem) return coerced_elem if coerced_elem.is_a?(InvalidValue) memo.add(coerced_elem) end end end end end end grape-1.5.2/lib/grape/validations/types/build_coercer.rb0000644000004100000410000000644714016744305023337 0ustar www-datawww-data# frozen_string_literal: true require_relative 'array_coercer' require_relative 'set_coercer' require_relative 'primitive_coercer' module Grape module Validations module Types # Chooses the best coercer for the given type. For example, if the type # is Integer, it will return a coercer which will be able to coerce a value # to the integer. # # There are a few very special coercers which might be returned. # # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when # the given type implies values in an array with different types. # For example, +[Integer, String]+ allows integer and string values in # an array. # # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when # a method is specified by a user with +coerce_with+ option or the user # specifies a custom type which implements requirments of # +Grape::Types::CustomTypeCoercer+. # # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the # previous one, but it expects an array or set of values having a custom # type implemented by the user. # # There is also a group of custom types implemented by Grape, check # +Grape::Validations::Types::SPECIAL+ to get the full list. # # @param type [Class] the type to which input strings # should be coerced # @param method [Class,#call] the coercion method to use # @return [Object] object to be used # for coercion and type validation def self.build_coercer(type, method: nil, strict: false) cache_instance(type, method, strict) do create_coercer_instance(type, method, strict) end end def self.create_coercer_instance(type, method, strict) # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! type = Types.map_special(type) # Use a special coercer for multiply-typed parameters. if Types.multiple?(type) MultipleTypeCoercer.new(type, method) # Use a special coercer for custom types and coercion methods. elsif method || Types.custom?(type) CustomTypeCoercer.new(type, method) # Special coercer for collections of types that implement a parse method. # CustomTypeCoercer (above) already handles such types when an explicit coercion # method is supplied. elsif Types.collection_of_custom?(type) Types::CustomTypeCollectionCoercer.new( Types.map_special(type.first), type.is_a?(Set) ) else DryTypeCoercer.coercer_instance_for(type, strict) end end def self.cache_instance(type, method, strict, &_block) key = cache_key(type, method, strict) return @__cache[key] if @__cache.key?(key) instance = yield @__cache_write_lock.synchronize do @__cache[key] = instance end instance end def self.cache_key(type, method, strict) [type, method, strict].each_with_object(+'_') do |val, memo| next if val.nil? memo << '_' << val.to_s end end instance_variable_set(:@__cache, {}) instance_variable_set(:@__cache_write_lock, Mutex.new) end end end grape-1.5.2/lib/grape/validations/types/variant_collection_coercer.rb0000644000004100000410000000354014016744305026106 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations module Types # This class wraps {MultipleTypeCoercer}, for use with collections # that allow members of more than one type. class VariantCollectionCoercer # Construct a new coercer that will attempt to coerce # a list of values such that all members are of one of # the given types. The container may also optionally be # coerced to a +Set+. An arbitrary coercion +method+ may # be supplied, which will be passed the entire collection # as a parameter and should return a new collection, or # may return the same one if no coercion was required. # # @param types [Array,Set] list of allowed types, # also specifying the container type # @param method [#call,#parse] method by which values should be coerced def initialize(types, method = nil) @types = types @method = method.respond_to?(:parse) ? method.method(:parse) : method # If we have a coercion method, pass it in here to save # building another one, even though we call it directly. @member_coercer = MultipleTypeCoercer.new types, method end # Coerce the given value. # # @param value [Array] collection of values to be coerced # @return [Array,Set,InvalidValue] # the coerced result, or an instance # of {InvalidValue} if the value could not be coerced. def call(value) return unless value.is_a? Array value = if @method @method.call(value) else value.map { |v| @member_coercer.call(v) } end return Set.new value if @types.is_a? Set value end end end end end grape-1.5.2/lib/grape/validations/types/custom_type_collection_coercer.rb0000644000004100000410000000412314016744305027013 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations module Types # See {CustomTypeCoercer} for details on types # that will be supported by this by this coercer. # This coercer works in the same way as +CustomTypeCoercer+ # except that it expects to receive an array of strings to # coerce and will return an array (or optionally, a set) # of coerced values. # # +CustomTypeCoercer+ is already capable of providing type # checking for arrays where an independent coercion method # is supplied. As such, +CustomTypeCollectionCoercer+ does # not allow for such a method to be supplied independently # of the type. class CustomTypeCollectionCoercer < CustomTypeCoercer # A new coercer for collections of the given type. # # @param type [Class,#parse] # type to which items in the array should be coerced. # Must implement a +parse+ method which accepts a string, # and for the purposes of type-checking it may either be # a class, or it may implement a +coerced?+, +parsed?+ or # +call+ method (in that order of precedence) which # accepts a single argument and returns true if the given # array item has been coerced correctly. # @param set [Boolean] # when true, a +Set+ will be returned by {#call} instead # of an +Array+ and duplicate items will be discarded. def initialize(type, set = false) super(type) @set = set end # Coerces the given value. # # @param value [Array] an array of values to be coerced # @return [Array,Set] the coerced result. May be an +Array+ or a # +Set+ depending on the setting given to the constructor def call(value) coerced = value.map do |item| coerced_item = super(item) return coerced_item if coerced_item.is_a?(InvalidValue) coerced_item end @set ? Set.new(coerced) : coerced end end end end end grape-1.5.2/lib/grape/validations/validators/0000755000004100000410000000000014016744305021202 5ustar www-datawww-datagrape-1.5.2/lib/grape/validations/validators/default.rb0000644000004100000410000000251214016744305023153 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class DefaultValidator < Base def initialize(attrs, options, required, scope, **opts) @default = options super end def validate_param!(attr_name, params) params[attr_name] = if @default.is_a? Proc @default.call elsif @default.frozen? || !duplicatable?(@default) @default else duplicate(@default) end end def validate!(params) attrs = SingleAttributeIterator.new(self, @scope, params) attrs.each do |resource_params, attr_name| next unless @scope.meets_dependency?(resource_params, params) validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? end end private # return true if we might be able to dup this object def duplicatable?(obj) !obj.nil? && obj != true && obj != false && !obj.is_a?(Symbol) && !obj.is_a?(Numeric) end # make a best effort to dup the object def duplicate(obj) obj.dup rescue TypeError obj end end end end grape-1.5.2/lib/grape/validations/validators/regexp.rb0000644000004100000410000000077414016744305023031 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class RegexpValidator < Base def validate_param!(attr_name, params) return unless params.respond_to?(:key?) && params.key?(attr_name) return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) } raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) end end end end grape-1.5.2/lib/grape/validations/validators/at_least_one_of.rb0000644000004100000410000000061114016744305024646 0ustar www-datawww-data# frozen_string_literal: true require 'grape/validations/validators/multiple_params_base' module Grape module Validations class AtLeastOneOfValidator < MultipleParamsBase def validate_params!(params) return unless keys_in_common(params).empty? raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) end end end end grape-1.5.2/lib/grape/validations/validators/all_or_none.rb0000644000004100000410000000067114016744305024022 0ustar www-datawww-data# frozen_string_literal: true require 'grape/validations/validators/multiple_params_base' module Grape module Validations class AllOrNoneOfValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.empty? || keys.length == all_keys.length raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) end end end end grape-1.5.2/lib/grape/validations/validators/values.rb0000644000004100000410000000524314016744305023032 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class ValuesValidator < Base def initialize(attrs, options, required, scope, **opts) if options.is_a?(Hash) @excepts = options[:except] @values = options[:value] @proc = options[:proc] warn '[DEPRECATION] The values validator except option is deprecated. ' \ 'Use the except validator instead.' if @excepts raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc) warn '[DEPRECATION] The values validator proc option is deprecated. ' \ 'The lambda expression can now be assigned directly to values.' if @proc else @excepts = nil @values = nil @proc = nil @values = options end super end def validate_param!(attr_name, params) return unless params.is_a?(Hash) val = params[attr_name] return if val.nil? && !required_for_root_scope? # don't forget that +false.blank?+ is true return if val != false && val.blank? && @allow_blank param_array = val.nil? ? [nil] : Array.wrap(val) raise validation_exception(attr_name, except_message) \ unless check_excepts(param_array) raise validation_exception(attr_name, message(:values)) \ unless check_values(param_array, attr_name) raise validation_exception(attr_name, message(:values)) \ if @proc && !param_array.all? { |param| @proc.call(param) } end private def check_values(param_array, attr_name) values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values return true if values.nil? begin return param_array.all? { |param| values.call(param) } if values.is_a? Proc rescue StandardError => e warn "Error '#{e}' raised while validating attribute '#{attr_name}'" return false end param_array.all? { |param| values.include?(param) } end def check_excepts(param_array) excepts = @excepts.is_a?(Proc) ? @excepts.call : @excepts return true if excepts.nil? param_array.none? { |param| excepts.include?(param) } end def except_message options = instance_variable_get(:@option) options_key?(:except_message) ? options[:except_message] : message(:except_values) end def required_for_root_scope? @required && @scope.root? end def validation_exception(attr_name, message) Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message) end end end end grape-1.5.2/lib/grape/validations/validators/except_values.rb0000644000004100000410000000137214016744305024401 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class ExceptValuesValidator < Base def initialize(attrs, options, required, scope, **opts) @except = options.is_a?(Hash) ? options[:value] : options super end def validate_param!(attr_name, params) return unless params.respond_to?(:key?) && params.key?(attr_name) excepts = @except.is_a?(Proc) ? @except.call : @except return if excepts.nil? param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) } end end end end grape-1.5.2/lib/grape/validations/validators/same_as.rb0000644000004100000410000000123314016744305023136 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class SameAsValidator < Base def validate_param!(attr_name, params) confirmation = options_key?(:value) ? @option[:value] : @option return if params[attr_name] == params[confirmation] raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: build_message ) end private def build_message if options_key?(:message) @option[:message] else format I18n.t(:same_as, scope: 'grape.errors.messages'), parameter: @option end end end end end grape-1.5.2/lib/grape/validations/validators/base.rb0000644000004100000410000000625214016744305022446 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class Base attr_reader :attrs # Creates a new Validator from options specified # by a +requires+ or +optional+ directive during # parameter definition. # @param attrs [Array] names of attributes to which the Validator applies # @param options [Object] implementation-dependent Validator options # @param required [Boolean] attribute(s) are required or optional # @param scope [ParamsScope] parent scope for this Validator # @param opts [Array] additional validation options def initialize(attrs, options, required, scope, *opts) @attrs = Array(attrs) @option = options @required = required @scope = scope opts = opts.any? ? opts.shift : {} @fail_fast = opts.fetch(:fail_fast, false) @allow_blank = opts.fetch(:allow_blank, false) end # Validates a given request. # @note Override #validate! unless you need to access the entire request. # @param request [Grape::Request] the request currently being handled # @raise [Grape::Exceptions::Validation] if validation failed # @return [void] def validate(request) return unless @scope.should_validate?(request.params) validate!(request.params) end # Validates a given parameter hash. # @note Override #validate if you need to access the entire request. # @param params [Hash] parameters to validate # @raise [Grape::Exceptions::Validation] if validation failed # @return [void] def validate!(params) attributes = SingleAttributeIterator.new(self, @scope, params) # we collect errors inside array because # there may be more than one error per field array_errors = [] attributes.each do |val, attr_name, empty_val, skip_value| next if skip_value next if !@scope.required? && empty_val next unless @scope.meets_dependency?(val, params) begin validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name) rescue Grape::Exceptions::Validation => e array_errors << e end end raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any? end def self.convert_to_short_name(klass) ret = klass.name.gsub(/::/, '/') ret.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') ret.gsub!(/([a-z\d])([A-Z])/, '\1_\2') ret.tr!('-', '_') ret.downcase! File.basename(ret, '_validator') end def self.inherited(klass) return unless klass.name.present? Validations.register_validator(convert_to_short_name(klass), klass) end def message(default_key = nil) options = instance_variable_get(:@option) options_key?(:message) ? options[:message] : default_key end def options_key?(key, options = nil) options = instance_variable_get(:@option) if options.nil? options.respond_to?(:key?) && options.key?(key) && !options[key].nil? end def fail_fast? @fail_fast end end end end grape-1.5.2/lib/grape/validations/validators/presence.rb0000644000004100000410000000054714016744305023341 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class PresenceValidator < Base def validate_param!(attr_name, params) return if params.respond_to?(:key?) && params.key?(attr_name) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) end end end end grape-1.5.2/lib/grape/validations/validators/allow_blank.rb0000644000004100000410000000101514016744305024011 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class AllowBlankValidator < Base def validate_param!(attr_name, params) return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) value = params[attr_name] value = value.strip if value.respond_to?(:strip) return if value == false || value.present? raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank)) end end end end grape-1.5.2/lib/grape/validations/validators/exactly_one_of.rb0000644000004100000410000000102514016744305024523 0ustar www-datawww-data# frozen_string_literal: true require 'grape/validations/validators/multiple_params_base' module Grape module Validations class ExactlyOneOfValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.length == 1 raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.length.zero? raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end end end end grape-1.5.2/lib/grape/validations/validators/multiple_params_base.rb0000644000004100000410000000156714016744305025730 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class MultipleParamsBase < Base def validate!(params) attributes = MultipleAttributesIterator.new(self, @scope, params) array_errors = [] attributes.each do |resource_params, skip_value| next if skip_value begin validate_params!(resource_params) rescue Grape::Exceptions::Validation => e array_errors << e end end raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any? end private def keys_in_common(resource_params) return [] unless resource_params.is_a?(Hash) all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } end def all_keys attrs.map { |attr| @scope.full_name(attr) } end end end end grape-1.5.2/lib/grape/validations/validators/as.rb0000644000004100000410000000052614016744305022135 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class AsValidator < Base def initialize(attrs, options, required, scope, **opts) @renamed_options = options super end def validate_param!(attr_name, params) params[@renamed_options] = params[attr_name] end end end end grape-1.5.2/lib/grape/validations/validators/mutual_exclusion.rb0000644000004100000410000000064114016744305025130 0ustar www-datawww-data# frozen_string_literal: true require 'grape/validations/validators/multiple_params_base' module Grape module Validations class MutualExclusionValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.length <= 1 raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end end end end grape-1.5.2/lib/grape/validations/validators/coerce.rb0000644000004100000410000000464414016744305022777 0ustar www-datawww-data# frozen_string_literal: true module Grape class API class Boolean def self.build(val) return nil if val != true && val != false new end end class Instance Boolean = Grape::API::Boolean end end module Validations class CoerceValidator < Base def initialize(attrs, options, required, scope, **opts) super @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) type else Types.build_coercer(type, method: @option[:method]) end end def validate(request) super end def validate_param!(attr_name, params) raise validation_exception(attr_name) unless params.is_a? Hash new_value = coerce_value(params[attr_name]) raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value) # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash # which looses wrappers for hashes and arrays after reassigning values # # h = Hashie::Mash.new(list: [1, 2, 3, 4]) # => #> # list = h.list # h[:list] = list # h # => # return if params[attr_name].class == new_value.class && params[attr_name] == new_value params[attr_name] = new_value end private # @!attribute [r] converter # Object that will be used for parameter coercion and type checking. # # See {Types.build_coercer} # # @return [Object] attr_reader :converter def valid_type?(val) !val.is_a?(Types::InvalidValue) end def coerce_value(val) converter.call(val) # Some custom types might fail, so it should be treated as an invalid value rescue StandardError Types::InvalidValue.new end # Type to which the parameter will be coerced. # # @return [Class] def type @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] end def validation_exception(attr_name, custom_msg = nil) Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: custom_msg || message(:coerce) ) end end end end grape-1.5.2/lib/grape/validations/params_scope.rb0000644000004100000410000004400214016744305022033 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class ParamsScope attr_accessor :element, :parent, :index attr_reader :type include Grape::DSL::Parameters # Open up a new ParamsScope, allowing parameter definitions per # Grape::DSL::Params. # @param opts [Hash] options for this scope # @option opts :element [Symbol] the element that contains this scope; for # this to be relevant, @parent must be set # @option opts :parent [ParamsScope] the scope containing this scope # @option opts :api [API] the API endpoint to modify # @option opts :optional [Boolean] whether or not this scope needs to have # any parameters set or not # @option opts :type [Class] a type meant to govern this scope (deprecated) # @option opts :type [Hash] group options for this scope # @option opts :dependent_on [Symbol] if present, this scope should only # validate if this param is present in the parent scope # @yield the instance context, open for parameter definitions def initialize(opts, &block) @element = opts[:element] @parent = opts[:parent] @api = opts[:api] @optional = opts[:optional] || false @type = opts[:type] @group = opts[:group] || {} @dependent_on = opts[:dependent_on] @declared_params = [] @index = nil instance_eval(&block) if block_given? configure_declared_params end def configuration @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration end # @return [Boolean] whether or not this entire scope needs to be # validated def should_validate?(parameters) scoped_params = params(parameters) return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params)) return false unless meets_dependency?(scoped_params, parameters) return true if parent.nil? parent.should_validate?(parameters) end def meets_dependency?(params, request_params) return true unless @dependent_on if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params) return false end return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) # params might be anything what looks like a hash, so it must implement a `key?` method return false unless params.respond_to?(:key?) @dependent_on.each do |dependency| if dependency.is_a?(Hash) dependency_key = dependency.keys[0] proc = dependency.values[0] return false unless proc.call(params.try(:[], dependency_key)) elsif params.respond_to?(:key?) && params.try(:[], dependency).blank? return false end end true end # @return [String] the proper attribute name, with nesting considered. def full_name(name, index: nil) if nested? # Find our containing element's name, and append ours. "#{@parent.full_name(@element)}#{brackets(@index || index)}#{brackets(name)}" elsif lateral? # Find the name of the element as if it was at the same nesting level # as our parent. We need to forward our index upward to achieve this. @parent.full_name(name, index: @index) else # We must be the root scope, so no prefix needed. name.to_s end end def brackets(val) "[#{val}]" if val end # @return [Boolean] whether or not this scope is the root-level scope def root? !@parent end # A nested scope is contained in one of its parent's elements. # @return [Boolean] whether or not this scope is nested def nested? @parent && @element end # A lateral scope is subordinate to its parent, but its keys are at the # same level as its parent and thus is not contained within an element. # @return [Boolean] whether or not this scope is lateral def lateral? @parent && !@element end # @return [Boolean] whether or not this scope needs to be present, or can # be blank def required? !@optional end protected # Adds a parameter declaration to our list of validations. # @param attrs [Array] (see Grape::DSL::Parameters#requires) def push_declared_params(attrs, **opts) if lateral? @parent.push_declared_params(attrs, **opts) else if opts && opts[:as] @api.route_setting(:renamed_params, @api.route_setting(:renamed_params) || []) @api.route_setting(:renamed_params) << { attrs.first => opts[:as] } attrs = [opts[:as]] end @declared_params.concat attrs end end private def require_required_and_optional_fields(context, opts) if context == :all optional_fields = Array(opts[:except]) required_fields = opts[:using].keys - optional_fields else # context == :none required_fields = Array(opts[:except]) optional_fields = opts[:using].keys - required_fields end required_fields.each do |field| field_opts = opts[:using][field] raise ArgumentError, "required field not exist: #{field}" unless field_opts requires(field, field_opts) end optional_fields.each do |field| field_opts = opts[:using][field] optional(field, field_opts) if field_opts end end def require_optional_fields(context, opts) optional_fields = opts[:using].keys optional_fields -= Array(opts[:except]) unless context == :all optional_fields.each do |field| field_opts = opts[:using][field] optional(field, field_opts) if field_opts end end def validate_attributes(attrs, opts, &block) validations = opts.clone validations[:type] ||= Array if block validates(attrs, validations) end # Returns a new parameter scope, subordinate to the current one and nested # under the parameter corresponding to `attrs.first`. # @param attrs [Array] the attributes passed to the `requires` or # `optional` invocation that opened this scope. # @param optional [Boolean] whether the parameter this are nested under # is optional or not (and hence, whether this block's params will be). # @yield parameter scope def new_scope(attrs, optional = false, &block) # if required params are grouped and no type or unsupported type is provided, raise an error type = attrs[1] ? attrs[1][:type] : nil if attrs.first && !optional raise Grape::Exceptions::MissingGroupTypeError.new if type.nil? raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type) end self.class.new( api: @api, element: attrs[1][:as] || attrs.first, parent: self, optional: optional, type: type || Array, &block ) end # Returns a new parameter scope, not nested under any current-level param # but instead at the same level as the current scope. # @param options [Hash] options to control how this new scope behaves # @option options :dependent_on [Symbol] if given, specifies that this # scope should only validate if this parameter from the above scope is # present # @yield parameter scope def new_lateral_scope(options, &block) self.class.new( api: @api, element: nil, parent: self, options: @optional, type: type == Array ? Array : Hash, dependent_on: options[:dependent_on], &block ) end # Returns a new parameter scope, subordinate to the current one and nested # under the parameter corresponding to `attrs.first`. # @param attrs [Array] the attributes passed to the `requires` or # `optional` invocation that opened this scope. # @yield parameter scope def new_group_scope(attrs, &block) self.class.new( api: @api, parent: self, group: attrs.first, &block ) end # Pushes declared params to parent or settings def configure_declared_params if nested? @parent.push_declared_params [element => @declared_params] else @api.namespace_stackable(:declared_params, @declared_params) end # params were stored in settings, it can be cleaned from the params scope @declared_params = nil end def validates(attrs, validations) doc_attrs = { required: validations.key?(:presence) } coerce_type = infer_coercion(validations) doc_attrs[:type] = coerce_type.to_s if coerce_type desc = validations.delete(:desc) || validations.delete(:description) doc_attrs[:desc] = desc if desc default = validations[:default] doc_attrs[:default] = default if validations.key?(:default) if (values_hash = validations[:values]).is_a? Hash values = values_hash[:value] # NB: excepts is deprecated excepts = values_hash[:except] else values = validations[:values] end doc_attrs[:values] = values if values except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values] # NB. values and excepts should be nil, Proc, Array, or Range. # Specifically, values should NOT be a Hash # use values or excepts to guess coerce type when stated type is Array coerce_type = guess_coerce_type(coerce_type, values, except_values, excepts) # default value should be present in values array, if both exist and are not procs check_incompatible_option_values(default, values, except_values, excepts) # type should be compatible with values array, if both exist validate_value_coercion(coerce_type, values, except_values, excepts) doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation) full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } } @api.document_attribute(full_attrs, doc_attrs) opts = derive_validator_options(validations) # Validate for presence before any other validators if validations.key?(:presence) && validations[:presence] validate('presence', validations[:presence], attrs, doc_attrs, opts) validations.delete(:presence) validations.delete(:message) if validations.key?(:message) end # Before we run the rest of the validators, let's handle # whatever coercion so that we are working with correctly # type casted values coerce_type validations, attrs, doc_attrs, opts validations.each do |type, options| validate(type, options, attrs, doc_attrs, opts) end end # Validate and comprehend the +:type+, +:types+, and +:coerce_with+ # options that have been supplied to the parameter declaration. # The +:type+ and +:types+ options will be removed from the # validations list, replaced appropriately with +:coerce+ and # +:coerce_with+ options that will later be passed to # {Validators::CoerceValidator}. The type that is returned may be # used for documentation and further validation of parameter # options. # # @param validations [Hash] list of validations supplied to the # parameter declaration # @return [class-like] type to which the parameter will be coerced # @raise [ArgumentError] if the given type options are invalid def infer_coercion(validations) if validations.key?(:type) && validations.key?(:types) raise ArgumentError, ':type may not be supplied with :types' end validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type) validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type) validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types) validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types) validations.delete(:types) if validations.key?(:types) coerce_type = validations[:coerce] # Special case - when the argument is a single type that is a # variant-type collection. if Types.multiple?(coerce_type) && validations.key?(:type) validations[:coerce] = Types::VariantCollectionCoercer.new( coerce_type, validations.delete(:coerce_with) ) end validations.delete(:type) coerce_type end # Enforce correct usage of :coerce_with parameter. # We do not allow coercion without a type, nor with # +JSON+ as a type since this defines its own coercion # method. def check_coerce_with(validations) return unless validations.key?(:coerce_with) # type must be supplied for coerce_with.. raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce) # but not special JSON types, which # already imply coercion method return unless [JSON, Array[JSON]].include? validations[:coerce] raise ArgumentError, 'coerce_with disallowed for type: JSON' end # Add type coercion validation to this scope, # if any has been specified. # This validation has special handling since it is # composited from more than one +requires+/+optional+ # parameter, and needs to be run before most other # validations. def coerce_type(validations, attrs, doc_attrs, opts) check_coerce_with(validations) return unless validations.key?(:coerce) coerce_options = { type: validations[:coerce], method: validations[:coerce_with], message: validations[:coerce_message] } validate('coerce', coerce_options, attrs, doc_attrs, opts) validations.delete(:coerce_with) validations.delete(:coerce) validations.delete(:coerce_message) end def guess_coerce_type(coerce_type, *values_list) return coerce_type unless coerce_type == Array values_list.each do |values| next if !values || values.is_a?(Proc) return values.first.class if values.is_a?(Range) || !values.empty? end coerce_type end def check_incompatible_option_values(default, values, except_values, excepts) return unless default && !default.is_a?(Proc) if values && !values.is_a?(Proc) raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) \ unless Array(default).all? { |def_val| values.include?(def_val) } end if except_values && !except_values.is_a?(Proc) raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \ unless Array(default).none? { |def_val| except_values.include?(def_val) } end return unless excepts && !excepts.is_a?(Proc) raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, excepts) \ unless Array(default).none? { |def_val| excepts.include?(def_val) } end def validate(type, options, attrs, doc_attrs, opts) validator_class = Validations.validators[type.to_s] raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class validator_options = { attributes: attrs, options: options, required: doc_attrs[:required], params_scope: self, opts: opts, validator_class: validator_class } @api.namespace_stackable(:validations, validator_options) end def validate_value_coercion(coerce_type, *values_list) return unless coerce_type coerce_type = coerce_type.first if coerce_type.is_a?(Array) values_list.each do |values| next if !values || values.is_a?(Proc) value_types = values.is_a?(Range) ? [values.begin, values.end] : values if coerce_type == Grape::API::Boolean value_types = value_types.map { |type| Grape::API::Boolean.build(type) } end unless value_types.all? { |v| v.is_a? coerce_type } raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) end end end def extract_message_option(attrs) return nil unless attrs.is_a?(Array) opts = attrs.last.is_a?(Hash) ? attrs.pop : {} opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil end def options_key?(type, key, validations) validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil? end def all_element_blank?(scoped_params) scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?) end # Validators don't have access to each other and they don't need, however, # some validators might influence others, so their options should be shared def derive_validator_options(validations) allow_blank = validations[:allow_blank] { allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank, fail_fast: validations.delete(:fail_fast) || false } end end end end grape-1.5.2/lib/grape/validations/validator_factory.rb0000644000004100000410000000071014016744305023071 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class ValidatorFactory def self.create_validator(**options) options[:validator_class].new(options[:attributes], options[:options], options[:required], options[:params_scope], **options[:opts]) end end end end grape-1.5.2/lib/grape/validations/types.rb0000644000004100000410000001106314016744305020524 0ustar www-datawww-data# frozen_string_literal: true require_relative 'types/build_coercer' require_relative 'types/custom_type_coercer' require_relative 'types/custom_type_collection_coercer' require_relative 'types/multiple_type_coercer' require_relative 'types/variant_collection_coercer' require_relative 'types/json' require_relative 'types/file' require_relative 'types/invalid_value' module Grape module Validations # Module for code related to grape's system for # coercion and type validation of incoming request # parameters. # # Grape uses a number of tests and assertions to # work out exactly how a parameter should be handled, # based on the +type+ and +coerce_with+ options that # may be supplied to {Grape::Dsl::Parameters#requires} # and {Grape::Dsl::Parameters#optional}. The main # entry point for this process is {Types.build_coercer}. module Types # Types representing a single value, which are coerced. PRIMITIVES = [ # Numerical Integer, Float, BigDecimal, Numeric, # Date/time Date, DateTime, Time, # Misc Grape::API::Boolean, String, Symbol, TrueClass, FalseClass ].freeze # Types representing data structures. STRUCTURES = [ Hash, Array, Set ].freeze # Special custom types provided by Grape. SPECIAL = { JSON => Json, Array[JSON] => JsonArray, ::File => File, Rack::Multipart::UploadedFile => File }.freeze GROUPS = [ Array, Hash, JSON, Array[JSON] ].freeze # Is the given class a primitive type as recognized by Grape? # # @param type [Class] type to check # @return [Boolean] whether or not the type is known by Grape as a valid # type for a single value def self.primitive?(type) PRIMITIVES.include?(type) end # Is the given class a standard data structure (collection or map) # as recognized by Grape? # # @param type [Class] type to check # @return [Boolean] whether or not the type is known by Grape as a valid # data structure type def self.structure?(type) STRUCTURES.include?(type) end # Is the declared type in fact an array of multiple allowed types? # For example the declaration +types: [Integer,String]+ will attempt # first to coerce given values to integer, but will also accept any # other string. # # @param type [Array,Set] type (or type list!) to check # @return [Boolean] +true+ if the given value will be treated as # a list of types. def self.multiple?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.size > 1 end # Does Grape provide special coercion and validation # routines for the given class? This does not include # automatic handling for primitives, structures and # otherwise recognized types. See {Types::SPECIAL}. # # @param type [Class] type to check # @return [Boolean] +true+ if special routines are available def self.special?(type) SPECIAL.key? type end # Is the declared type a supported group type? # Currently supported group types are Array, Hash, JSON, and Array[JSON] # # @param type [Array,Class] type to check # @return [Boolean] +true+ if the type is a supported group type def self.group?(type) GROUPS.include? type end # A valid custom type must implement a class-level `parse` method, taking # one String argument and returning the parsed value in its correct type. # # @param type [Class] type to check # @return [Boolean] whether or not the type can be used as a custom type def self.custom?(type) !primitive?(type) && !structure?(type) && !multiple?(type) && type.respond_to?(:parse) && type.method(:parse).arity == 1 end # Is the declared type an +Array+ or +Set+ of a {#custom?} type? # # @param type [Array,Class] type to check # @return [Boolean] true if +type+ is a collection of a type that implements # its own +#parse+ method. def self.collection_of_custom?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.length == 1 && (custom?(type.first) || special?(type.first)) end def self.map_special(type) SPECIAL.fetch(type, type) end end end end grape-1.5.2/lib/grape/validations/multiple_attributes_iterator.rb0000644000004100000410000000042014016744305025365 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class MultipleAttributesIterator < AttributesIterator private def yield_attributes(resource_params, _attrs) yield resource_params, skip?(resource_params) end end end end grape-1.5.2/lib/grape/validations/single_attribute_iterator.rb0000644000004100000410000000110214016744305024626 0ustar www-datawww-data# frozen_string_literal: true module Grape module Validations class SingleAttributeIterator < AttributesIterator private def yield_attributes(val, attrs) attrs.each do |attr_name| yield val, attr_name, empty?(val), skip?(val) end end # Primitives like Integers and Booleans don't respond to +empty?+. # It could be possible to use +blank?+ instead, but # # false.blank? # => true def empty?(val) val.respond_to?(:empty?) ? val.empty? : val.nil? end end end end grape-1.5.2/lib/grape/extensions/0000755000004100000410000000000014016744305016714 5ustar www-datawww-datagrape-1.5.2/lib/grape/extensions/deep_mergeable_hash.rb0000644000004100000410000000105514016744305023165 0ustar www-datawww-data# frozen_string_literal: true module Grape module Extensions class DeepMergeableHash < ::Hash def deep_merge!(other_hash) other_hash.each_pair do |current_key, other_value| this_value = self[current_key] self[current_key] = if this_value.is_a?(::Hash) && other_value.is_a?(::Hash) this_value.deep_merge(other_value) else other_value end end self end end end end grape-1.5.2/lib/grape/extensions/hash.rb0000644000004100000410000000122614016744305020165 0ustar www-datawww-data# frozen_string_literal: true module Grape module Extensions module Hash module ParamBuilder extend ::ActiveSupport::Concern included do namespace_inheritable(:build_params_with, Grape::Extensions::Hash::ParamBuilder) end def build_params params = Grape::Extensions::DeepMergeableHash[rack_params] params.deep_merge!(grape_routing_args) if env[Grape::Env::GRAPE_ROUTING_ARGS] post_process_params(params) end def post_process_params(params) Grape::Extensions::DeepSymbolizeHash.deep_symbolize_keys_in(params) end end end end end grape-1.5.2/lib/grape/extensions/deep_symbolize_hash.rb0000644000004100000410000000133214016744305023255 0ustar www-datawww-data# frozen_string_literal: true module Grape module Extensions module DeepSymbolizeHash def self.deep_symbolize_keys_in(object) case object when ::Hash object.each_with_object({}) do |(key, value), new_hash| new_hash[symbolize_key(key)] = deep_symbolize_keys_in(value) end when ::Array object.map { |element| deep_symbolize_keys_in(element) } else object end end def self.symbolize_key(key) if key.is_a?(Symbol) key elsif key.is_a?(String) key.to_sym elsif key.respond_to?(:to_sym) key.to_sym else key end end end end end grape-1.5.2/lib/grape/extensions/hashie/0000755000004100000410000000000014016744305020155 5ustar www-datawww-datagrape-1.5.2/lib/grape/extensions/hashie/mash.rb0000644000004100000410000000121514016744305021431 0ustar www-datawww-data# frozen_string_literal: true module Grape module Extensions module Hashie module Mash module ParamBuilder extend ::ActiveSupport::Concern included do namespace_inheritable(:build_params_with, Grape::Extensions::Hashie::Mash::ParamBuilder) end def params_builder Grape::Extensions::Hashie::Mash::ParamBuilder end def build_params params = ::Hashie::Mash.new(rack_params) params.deep_merge!(grape_routing_args) if env[Grape::Env::GRAPE_ROUTING_ARGS] params end end end end end end grape-1.5.2/lib/grape/extensions/active_support/0000755000004100000410000000000014016744305021763 5ustar www-datawww-datagrape-1.5.2/lib/grape/extensions/active_support/hash_with_indifferent_access.rb0000644000004100000410000000137614016744305030173 0ustar www-datawww-data# frozen_string_literal: true module Grape module Extensions module ActiveSupport module HashWithIndifferentAccess module ParamBuilder extend ::ActiveSupport::Concern included do namespace_inheritable(:build_params_with, Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder) end def params_builder Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder end def build_params params = ::ActiveSupport::HashWithIndifferentAccess.new(rack_params) params.deep_merge!(grape_routing_args) if env[Grape::Env::GRAPE_ROUTING_ARGS] params end end end end end end grape-1.5.2/lib/grape/api/0000755000004100000410000000000014016744305015266 5ustar www-datawww-datagrape-1.5.2/lib/grape/api/helpers.rb0000644000004100000410000000021314016744305017251 0ustar www-datawww-data# frozen_string_literal: true module Grape class API module Helpers include Grape::DSL::Helpers::BaseHelper end end end grape-1.5.2/lib/grape/api/instance.rb0000644000004100000410000002362514016744305017427 0ustar www-datawww-data# frozen_string_literal: true require 'grape/router' module Grape class API # The API Instance class, is the engine behind Grape::API. Each class that inherits # from this will represent a different API instance class Instance include Grape::DSL::API class << self attr_reader :instance attr_reader :base attr_accessor :configuration def given(conditional_option, &block) evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block_given? end def mounted(&block) evaluate_as_instance_with_configuration(block, lazy: true) end def base=(grape_api) @base = grape_api grape_api.instances << self end def to_s (base && base.to_s) || super end def base_instance? self == base.base_instance end # A class-level lock to ensure the API is not compiled by multiple # threads simultaneously within the same process. LOCK = Mutex.new # Clears all defined routes, endpoints, etc., on this API. def reset! reset_endpoints! reset_routes! reset_validations! end # Parses the API's definition and compiles it into an instance of # Grape::API. def compile @instance ||= new end # Wipe the compiled API so we can recompile after changes were made. def change! @instance = nil end # This is the interface point between Rack and Grape; it accepts a request # from Rack and ultimately returns an array of three values: the status, # the headers, and the body. See [the rack specification] # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more. def call(env) compile! call!(env) end # A non-synchronized version of ::call. def call!(env) instance.call(env) end # (see #cascade?) def cascade(value = nil) if value.nil? inheritable_setting.namespace_inheritable.key?(:cascade) ? !namespace_inheritable(:cascade).nil? : true else namespace_inheritable(:cascade, value) end end def compile! return if instance LOCK.synchronize { compile unless instance } end # see Grape::Router#recognize_path def recognize_path(path) compile! instance.router.recognize_path(path) end protected def prepare_routes endpoints.map(&:routes).flatten end # Execute first the provided block, then each of the # block passed in. Allows for simple 'before' setups # of settings stack pushes. def nest(*blocks, &block) blocks.reject!(&:nil?) if blocks.any? evaluate_as_instance_with_configuration(block) if block_given? blocks.each { |b| evaluate_as_instance_with_configuration(b) } reset_validations! else instance_eval(&block) end end def evaluate_as_instance_with_configuration(block, lazy: false) lazy_block = Grape::Util::LazyBlock.new do |configuration| value_for_configuration = configuration if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? self.configuration = value_for_configuration.evaluate end response = instance_eval(&block) self.configuration = value_for_configuration response end if base && base_instance? && lazy lazy_block else lazy_block.evaluate_from(configuration) end end def inherited(subclass) subclass.reset! subclass.logger = logger.clone end def inherit_settings(other_settings) top_level_setting.inherit_from other_settings.point_in_time_copy # Propagate any inherited params down to our endpoints, and reset any # compiled routes. endpoints.each do |e| e.inherit_settings(top_level_setting.namespace_stackable) e.reset_routes! end reset_routes! end end attr_reader :router # Builds the routes from the defined endpoints, effectively compiling # this API into a usable form. def initialize @router = Router.new add_head_not_allowed_methods_and_options_methods self.class.endpoints.each do |endpoint| endpoint.mount_in(@router) end @router.compile! @router.freeze end # Handle a request. See Rack documentation for what `env` is. def call(env) result = @router.call(env) result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade? result end # Some requests may return a HTTP 404 error if grape cannot find a matching # route. In this case, Grape::Router adds a X-Cascade header to the response # and sets it to 'pass', indicating to grape's parents they should keep # looking for a matching route on other resources. # # In some applications (e.g. mounting grape on rails), one might need to trap # errors from reaching upstream. This is effectivelly done by unsetting # X-Cascade. Default :cascade is true. def cascade? return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.key?(: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 versioned_route_configs = collect_route_config_per_pattern # 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 versioned_route_configs.each do |config| allowed_methods = config[:methods].dup unless self.class.namespace_inheritable(:do_not_route_head) allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET) end allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods) unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS) config[:endpoint].options[:options_route_enabled] = true end attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) generate_not_allowed_method(config[:pattern], **attributes) end end end end def collect_route_config_per_pattern all_routes = self.class.endpoints.map(&:routes).flatten routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp } # Build the configuration based on the first endpoint and the collection of methods supported. routes_by_regexp.values.map do |routes| last_route = routes.last # Most of the configuration is taken from the last endpoint matching_wildchar = routes.any? { |route| route.request_method == '*' } { options: {}, pattern: last_route.pattern, requirements: last_route.requirements, path: last_route.origin, endpoint: last_route.app, methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method) } end end # Generate a route that returns an HTTP 405 response for a user defined # path on methods not specified def generate_not_allowed_method(pattern, allowed_methods: [], **attributes) supported_methods = if self.class.namespace_inheritable(:do_not_route_options) Grape::Http::Headers::SUPPORTED_METHODS else Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS end not_allowed_methods = supported_methods - allowed_methods return if not_allowed_methods.empty? @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes) end # Allows definition of endpoints that ignore the versioning configuration # used by the rest of your API. def without_versioning(&_block) old_version = self.class.namespace_inheritable(:version) old_version_options = self.class.namespace_inheritable(:version_options) self.class.namespace_inheritable_to_nil(:version) self.class.namespace_inheritable_to_nil(:version_options) yield self.class.namespace_inheritable(:version, old_version) self.class.namespace_inheritable(:version_options, old_version_options) end # Allows definition of endpoints that ignore the root prefix used by the # rest of your API. def without_root_prefix(&_block) old_prefix = self.class.namespace_inheritable(:root_prefix) self.class.namespace_inheritable_to_nil(:root_prefix) yield self.class.namespace_inheritable(:root_prefix, old_prefix) end end end end grape-1.5.2/lib/grape.rb0000644000004100000410000001415414016744305015047 0ustar www-datawww-data# frozen_string_literal: true require 'logger' require 'rack' require 'rack/builder' require 'rack/accept' require 'rack/auth/basic' require 'rack/auth/digest/md5' require 'set' require 'active_support/version' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/conversions' require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'i18n' I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__) module Grape extend ::ActiveSupport::Autoload eager_autoload do autoload :API autoload :Endpoint autoload :Namespace autoload :Path autoload :Cookies autoload :Validations autoload :ErrorFormatter autoload :Formatter autoload :Parser autoload :Request autoload :Env, 'grape/util/env' autoload :Json, 'grape/util/json' autoload :Xml, 'grape/util/xml' end module Http extend ::ActiveSupport::Autoload eager_autoload do autoload :Headers end end module Exceptions extend ::ActiveSupport::Autoload eager_autoload do autoload :Base autoload :Validation autoload :ValidationArrayErrors 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 autoload :InvalidVersionHeader autoload :MethodNotAllowed autoload :InvalidResponse end end module Extensions extend ::ActiveSupport::Autoload eager_autoload do autoload :DeepMergeableHash autoload :DeepSymbolizeHash autoload :Hash end module ActiveSupport extend ::ActiveSupport::Autoload eager_autoload do autoload :HashWithIndifferentAccess end end module Hashie extend ::ActiveSupport::Autoload eager_autoload do autoload :Mash end end end module Middleware extend ::ActiveSupport::Autoload eager_autoload do autoload :Base autoload :Versioner autoload :Formatter autoload :Error autoload :Globals autoload :Stack autoload :Helpers end module Auth extend ::ActiveSupport::Autoload eager_autoload do autoload :Base autoload :DSL autoload :StrategyInfo autoload :Strategies end end module Versioner extend ::ActiveSupport::Autoload eager_autoload do autoload :Path autoload :Header autoload :Param autoload :AcceptVersionHeader end end end module Util extend ::ActiveSupport::Autoload eager_autoload do autoload :InheritableValues autoload :StackableValues autoload :ReverseStackableValues autoload :InheritableSetting autoload :StrictHashConfiguration autoload :Registrable end end module ErrorFormatter extend ::ActiveSupport::Autoload eager_autoload do autoload :Base autoload :Json autoload :Txt autoload :Xml end end module Formatter extend ::ActiveSupport::Autoload eager_autoload do autoload :Json autoload :SerializableHash autoload :Txt autoload :Xml end end module Parser extend ::ActiveSupport::Autoload eager_autoload do autoload :Json autoload :Xml end 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 autoload :Logger autoload :Desc end end class API extend ::ActiveSupport::Autoload eager_autoload do autoload :Helpers end end module Presenters extend ::ActiveSupport::Autoload eager_autoload do autoload :Presenter end end module ServeStream extend ::ActiveSupport::Autoload eager_autoload do autoload :FileBody autoload :SendfileResponse autoload :StreamResponse end end end require 'grape/config' require 'grape/content_types' require 'grape/util/lazy_value' require 'grape/util/lazy_block' require 'grape/util/endpoint_configuration' require 'grape/validations/validators/base' require 'grape/validations/attributes_iterator' require 'grape/validations/single_attribute_iterator' require 'grape/validations/multiple_attributes_iterator' require 'grape/validations/validators/allow_blank' require 'grape/validations/validators/as' 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/same_as' require 'grape/validations/validators/values' require 'grape/validations/validators/except_values' require 'grape/validations/params_scope' require 'grape/validations/validators/all_or_none' require 'grape/validations/types' require 'grape/validations/validator_factory' require 'grape/version' grape-1.5.2/UPGRADING.md0000644000004100000410000011477514016744305014532 0ustar www-datawww-dataUpgrading Grape =============== ### Upgrading to >= 1.5.1 #### Dependent params If you use [dependent params](https://github.com/ruby-grape/grape#dependent-parameters) with `Grape::Extensions::Hash::ParamBuilder`, make sure a parameter to be dependent on is set as a Symbol. If a String is given, a parameter that other parameters depend on won't be found even if it is present. _Correct_: ```ruby given :matrix do # dependent params end ``` _Wrong_: ```ruby given 'matrix' do # dependent params end ``` ### Upgrading to >= 1.5.0 Prior to 1.3.3, the `declared` helper would always return the complete params structure if `include_missing=true` was set. In 1.3.3 a regression was introduced such that a missing Hash with or without nested parameters would always resolve to `{}`. In 1.5.0 this behavior is reverted, so the whole params structure will always be available via `declared`, regardless of whether any params are passed. The following rules now apply to the `declared` helper when params are missing and `include_missing=true`: * Hash params with children will resolve to a Hash with keys for each declared child. * Hash params with no children will resolve to `{}`. * Set params will resolve to `Set.new`. * Array params will resolve to `[]`. * All other params will resolve to `nil`. #### Example ```ruby class Api < Grape::API params do optional :outer, type: Hash do optional :inner, type: Hash do optional :value, type: String end end end get 'example' do declared(params, include_missing: true) end end ``` ``` get '/example' # 1.3.3 = {} # 1.5.0 = {outer: {inner: {value:null}}} ``` For more information see [#2103](https://github.com/ruby-grape/grape/pull/2103). ### Upgrading to >= 1.4.0 #### Reworking stream and file and un-deprecating stream like-objects Previously in 0.16 stream-like objects were deprecated. This release restores their functionality for use-cases other than file streaming. This release deprecated `file` in favor of `sendfile` to better document its purpose. To deliver a file via the Sendfile support in your web server and have the Rack::Sendfile middleware enabled. See [`Rack::Sendfile`](https://www.rubydoc.info/gems/rack/Rack/Sendfile). ```ruby class API < Grape::API get '/' do sendfile '/path/to/file' end end ``` Use `stream` to stream file content in chunks. ```ruby class API < Grape::API get '/' do stream '/path/to/file' end end ``` Or use `stream` to stream other kinds of content. In the following example a streamer class streams paginated data from a database. ```ruby class MyObject attr_accessor :result def initialize(query) @result = query end def each yield '[' # Do paginated DB fetches and return each page formatted first = false result.find_in_batches do |records| yield process_records(records, first) first = false end yield ']' end def process_records(records, first) buffer = +'' buffer << ',' unless first buffer << records.map(&:to_json).join(',') buffer end end class API < Grape::API get '/' do stream MyObject.new(Sprocket.all) end end ``` ### Upgrading to >= 1.3.3 #### Nil values for structures Nil values always been a special case when dealing with types especially with the following structures: - Array - Hash - Set The behavior for these structures has change through out the latest releases. For example: ```ruby class Api < Grape::API params do require :my_param, type: Array[Integer] end get 'example' do params[:my_param] end get '/example', params: { my_param: nil } # 1.3.1 = [] # 1.3.2 = nil end ``` For now on, `nil` values stay `nil` values for all types, including arrays, sets and hashes. If you want to have the same behavior as 1.3.1, apply a `default` validator: ```ruby class Api < Grape::API params do require :my_param, type: Array[Integer], default: [] end get 'example' do params[:my_param] end get '/example', params: { my_param: nil } # => [] end ``` #### Default validator Default validator is now applied for `nil` values. ```ruby class Api < Grape::API params do requires :my_param, type: Integer, default: 0 end get 'example' do params[:my_param] end get '/example', params: { my_param: nil } #=> before: nil, after: 0 end ``` ### Upgrading to >= 1.3.0 #### Ruby After adding dry-types, Ruby 2.4 or newer is required. #### Coercion [Virtus](https://github.com/solnic/virtus) has been replaced by [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter coercion. If your project depends on Virtus outside of Grape, explicitly add it to your `Gemfile`. Here's an example of how to migrate a custom type from Virtus to dry-types: ```ruby # Legacy Grape parser class SecureUriType < Virtus::Attribute def coerce(input) URI.parse value end def value_coerced?(input) value.is_a? String end end params do requires :secure_uri, type: SecureUri end ``` To use dry-types, we need to: 1. Remove the inheritance of `Virtus::Attribute` 1. Rename `coerce` to `self.parse` 1. Rename `value_coerced?` to `self.parsed?` The custom type must have a class-level `parse` method to the model. A class-level `parsed?` is needed if the parsed type differs from the defined type. In the example below, since `SecureUri` is not the same as `URI::HTTPS`, `self.parsed?` is needed: ```ruby # New dry-types parser class SecureUri def self.parse(value) URI.parse value end def self.parsed?(value) value.is_a? URI::HTTPS end end params do requires :secure_uri, type: SecureUri end ``` #### Coercing to `FalseClass` or `TrueClass` no longer works Previous Grape versions allowed this, though it wasn't documented: ```ruby requires :true_value, type: TrueClass requires :bool_value, types: [FalseClass, TrueClass] ``` This is no longer supported, if you do this, your values will never be valid. Instead you should do this: ```ruby requires :true_value, type: Boolean # in your endpoint you should validate if this is actually `true` requires :bool_value, type: Boolean ``` #### Ensure that Array types have explicit coercions Unlike Virtus, dry-types does not perform any implict coercions. If you have any uses of `Array[String]`, `Array[Integer]`, etc. be sure they use a `coerce_with` block. For example: ```ruby requires :values, type: Array[String] ``` It's quite common to pass a comma-separated list, such as `tag1,tag2` as `values`. Previously Virtus would implicitly coerce this to `Array(values)` so that `["tag1,tag2"]` would pass the type checks, but with `dry-types` the values are no longer coerced for you. To fix this, you might do: ```ruby requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) } ``` Likewise, for `Array[Integer]`, you might do: ```ruby requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(',').map(&:strip).map(&:to_i) } ``` For more information see [#1920](https://github.com/ruby-grape/grape/pull/1920). ### Upgrading to >= 1.2.4 #### Headers in `error!` call Headers in `error!` will be merged with `headers` hash. If any header need to be cleared on `error!` call, make sure to move it to the `after` block. ```ruby class SampleApi < Grape::API before do header 'X-Before-Header', 'before_call' end get 'ping' do header 'X-App-Header', 'on_call' error! :pong, 400, 'X-Error-Details' => 'Invalid token' end end ``` **Former behaviour** ```ruby response.headers['X-Before-Header'] # => nil response.headers['X-App-Header'] # => nil response.headers['X-Error-Details'] # => Invalid token ``` **Current behaviour** ```ruby response.headers['X-Before-Header'] # => 'before_call' response.headers['X-App-Header'] # => 'on_call' response.headers['X-Error-Details'] # => Invalid token ``` ### Upgrading to >= 1.2.1 #### Obtaining the name of a mounted class In order to make obtaining the name of a mounted class simpler, we've delegated `.to_s` to `base.name` **Deprecated in 1.2.0** ```ruby payload[:endpoint].options[:for].name ``` **New** ```ruby payload[:endpoint].options[:for].to_s ``` ### Upgrading to >= 1.2.0 #### Changes in the Grape::API class ##### Patching the class In an effort to make APIs re-mountable, The class `Grape::API` no longer refers to an API instance, rather, what used to be `Grape::API` is now `Grape::API::Instance` and `Grape::API` was replaced with a class that can contain several instances of `Grape::API`. This changes were done in such a way that no code-changes should be required. However, if experiencing problems, or relying on private methods and internal behaviour too deeply, it is possible to restore the prior behaviour by replacing the references from `Grape::API` to `Grape::API::Instance`. Note, this is particularly relevant if you are opening the class `Grape::API` for modification. **Deprecated** ```ruby class Grape::API # your patched logic ... end ``` **New** ```ruby class Grape::API::Instance # your patched logic ... end ``` ##### `name` (and other caveats) of the mounted API After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class which inherit from `Grape::API::Instance`. What this means in practice, is: - Generally: you can access the named class from the instance calling the getter `base`. - In particular: If you need the `name`, you can use `base`.`name`. **Deprecated** ```ruby payload[:endpoint].options[:for].name ``` **New** ```ruby payload[:endpoint].options[:for].base.name ``` #### Changes in rescue_from returned object Grape will now check the object returned from `rescue_from` and ensure that it is a `Rack::Response`. That makes sure response is valid and avoids exposing service information. Change any code that invoked `Rack::Response.new(...).finish` in a custom `rescue_from` block to `Rack::Response.new(...)` to comply with the validation. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| # version prior to 1.2.0 Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }).finish # 1.2.0 version Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }) end end ``` See [#1757](https://github.com/ruby-grape/grape/pull/1757) and [#1776](https://github.com/ruby-grape/grape/pull/1776) for more information. ### Upgrading to >= 1.1.0 #### Changes in HTTP Response Code for Unsupported Content Type For PUT, POST, PATCH, and DELETE requests where a non-empty body and a "Content-Type" header is supplied that is not supported by the Grape API, Grape will no longer return a 406 "Not Acceptable" HTTP status code and will instead return a 415 "Unsupported Media Type" so that the usage of HTTP status code falls more in line with the specification of [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt). ### Upgrading to >= 1.0.0 #### Changes in XML and JSON Parsers Grape no longer uses `multi_json` or `multi_xml` by default and uses `JSON` and `ActiveSupport::XmlMini` instead. This has no visible impact on JSON processing, but the default behavior of the XML parser has changed. For example, an XML POST containing `Bobby T.` was parsed as `Bobby T.` with `multi_xml`, and as now parsed as `{"__content__"=>"Bobby T."}` with `XmlMini`. If you were using `MultiJson.load`, `MultiJson.dump` or `MultiXml.parse`, you can substitute those with `Grape::Json.load`, `Grape::Json.dump`, `::Grape::Xml.parse`, or directly with `JSON.load`, `JSON.dump`, `XmlMini.parse`, etc. To restore previous behavior, add `multi_json` or `multi_xml` to your `Gemfile` and `require` it. See [#1623](https://github.com/ruby-grape/grape/pull/1623) for more information. #### Changes in Parameter Class The default class for `params` has changed from `Hashie::Mash` to `ActiveSupport::HashWithIndifferentAccess` and the `hashie` dependency has been removed. This means that by default you can no longer access parameters by method name. ```ruby class API < Grape::API params do optional :color, type: String end get do params[:color] # use params[:color] instead of params.color end end ``` To restore the behavior of prior versions, add `hashie` to your `Gemfile` and `include Grape::Extensions::Hashie::Mash::ParamBuilder` in your API. ```ruby class API < Grape::API include Grape::Extensions::Hashie::Mash::ParamBuilder params do optional :color, type: String end get do # params.color works end end ``` This behavior can also be overridden on individual parameter blocks using `build_with`. ```ruby params do build_with Grape::Extensions::Hash::ParamBuilder optional :color, type: String end ``` If you're constructing your own `Grape::Request` in a middleware, you can pass different parameter handlers to create the desired `params` class with `build_params_with`. ```ruby def request Grape::Request.new(env, build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder) end ``` See [#1610](https://github.com/ruby-grape/grape/pull/1610) for more information. #### The `except`, `except_message`, and `proc` options of the `values` validator are deprecated. The new `except_values` validator should be used in place of the `except` and `except_message` options of the `values` validator. Arity one Procs may now be used directly as the `values` option to explicitly test param values. **Deprecated** ```ruby params do requires :a, values: { value: 0..99, except: [3] } requires :b, values: { value: 0..99, except: [3], except_message: 'not allowed' } requires :c, values: { except: ['admin'] } requires :d, values: { proc: -> (v) { v.even? } } end ``` **New** ```ruby params do requires :a, values: 0..99, except_values: [3] requires :b, values: 0..99, except_values: { value: [3], message: 'not allowed' } requires :c, except_values: ['admin'] requires :d, values: -> (v) { v.even? } end ``` See [#1616](https://github.com/ruby-grape/grape/pull/1616) for more information. ### Upgrading to >= 0.19.1 #### DELETE now defaults to status code 200 for responses with a body, or 204 otherwise Prior to this version, DELETE requests defaulted to a status code of 204 No Content, even when the response included content. This behavior confused some clients and prevented the formatter middleware from running properly. As of this version, DELETE requests will only default to a 204 No Content status code if no response body is provided, and will default to 200 OK otherwise. Specifically, DELETE behaviour has changed as follows: - In versions < 0.19.0, all DELETE requests defaulted to a 200 OK status code. - In version 0.19.0, all DELETE requests defaulted to a 204 No Content status code, even when content was included in the response. - As of version 0.19.1, DELETE requests default to a 204 No Content status code, unless content is supplied, in which case they default to a 200 OK status code. To achieve the old behavior, one can specify the status code explicitly: ```ruby delete :id do status 204 # or 200, for < 0.19.0 behavior 'foo successfully deleted' end ``` One can also use the new `return_no_content` helper to explicitly return a 204 status code and an empty body for any request type: ```ruby delete :id do return_no_content 'this will not be returned' end ``` See [#1550](https://github.com/ruby-grape/grape/pull/1550) for more information. ### Upgrading to >= 0.18.1 #### Changes in priority of :any routes Prior to this version, `:any` routes were searched after matching first route and 405 routes. This behavior has changed and `:any` routes are now searched before 405 processing. In the following example the `:any` route will match first when making a request with an unsupported verb. ```ruby post :example do 'example' end route :any, '*path' do error! :not_found, 404 end get '/example' #=> before: 405, after: 404 ``` #### Removed param processing from built-in OPTIONS handler When a request is made to the built-in `OPTIONS` handler, only the `before` and `after` callbacks associated with the resource will be run. The `before_validation` and `after_validation` callbacks and parameter validations will be skipped. See [#1505](https://github.com/ruby-grape/grape/pull/1505) for more information. #### Changed endpoint params validation Grape now correctly returns validation errors for all params when multiple params are passed to a requires. The following code will return `one is missing, two is missing` when calling the endpoint without parameters. ```ruby params do requires :one, :two end ``` Prior to this version the response would be `one is missing`. See [#1510](https://github.com/ruby-grape/grape/pull/1510) for more information. #### The default status code for DELETE is now 204 instead of 200. Breaking change: Sets the default response status code for a delete request to 204. A status of 204 makes the response more distinguishable and therefore easier to handle on the client side, particularly because a DELETE request typically returns an empty body as the resource was deleted or voided. To achieve the old behavior, one has to set it explicitly: ```ruby delete :id do status 200 'foo successfully deleted' end ``` For more information see: [#1532](https://github.com/ruby-grape/grape/pull/1532). ### Upgrading to >= 0.17.0 #### Removed official support for Ruby < 2.2.2 Grape is no longer automatically tested against versions of Ruby prior to 2.2.2. This is because of its dependency on activesupport which, with version 5.0.0, now requires at least Ruby 2.2.2. See [#1441](https://github.com/ruby-grape/grape/pull/1441) for nmore information. #### Changed priority of `rescue_from` clauses applying The `rescue_from` clauses declared inside a namespace would take a priority over ones declared in the root scope. This could possibly affect those users who use different `rescue_from` clauses in root scope and in namespaces. See [#1405](https://github.com/ruby-grape/grape/pull/1405) for more information. #### Helper methods injected inside `rescue_from` in middleware Helper methods are injected inside `rescue_from` may cause undesirable effects. For example, definining a helper method called `error!` will take precendence over the built-in `error!` method and should be renamed. See [#1451](https://github.com/ruby-grape/grape/issues/1451) for an example. ### Upgrading to >= 0.16.0 #### Replace rack-mount with new router The `Route#route_xyz` methods have been deprecated since 0.15.1. Please use `Route#xyz` instead. Note that the `Route#route_method` was replaced by `Route#request_method`. The following code would work correctly. ```ruby TwitterAPI::versions # yields [ 'v1', 'v2' ] TwitterAPI::routes # yields an array of Grape::Route objects TwitterAPI::routes[0].version # => 'v1' TwitterAPI::routes[0].description # => 'Includes custom settings.' TwitterAPI::routes[0].settings[:custom] # => { key: 'value' } TwitterAPI::routes[0].request_method # => 'GET' ``` #### `file` method accepts path to file Now to serve files via Grape just pass the path to the file. Functionality with FileStreamer-like objects is deprecated. Please, replace your FileStreamer-like objects with paths of served files. Old style: ```ruby class FileStreamer def initialize(file_path) @file_path = file_path end def each(&blk) File.open(@file_path, 'rb') do |file| file.each(10, &blk) end end end # ... class API < Grape::API get '/' do file FileStreamer.new('/path/to/file') end end ``` New style: ```ruby class API < Grape::API get '/' do file '/path/to/file' end end ``` ### Upgrading to >= 0.15.0 #### Changes to availability of `:with` option of `rescue_from` method The `:with` option of `rescue_from` does not accept value except Proc, String or Symbol now. If you have been depending the old behavior, you should use lambda block instead. ```ruby class API < Grape::API rescue_from :all, with: -> { Rack::Response.new('rescued with a method', 400) } end ``` #### Changes to behavior of `after` method of middleware on error The `after` method of the middleware is now also called on error. The following code would work correctly. ```ruby class ErrorMiddleware < Grape::Middleware::Base def after return unless @app_response && @app_response[0] == 500 env['rack.logger'].debug("Raised error on #{env['PATH_INFO']}") end end ``` See [#1147](https://github.com/ruby-grape/grape/issues/1147) and [#1240](https://github.com/ruby-grape/grape/issues/1240) for discussion of the issues. A warning will be logged if an exception is raised in an `after` callback, which points you to middleware that was not called in the previous version and is called now. ``` caught error of type NoMethodError in after callback inside Api::Middleware::SomeMiddleware : undefined method `headers' for nil:NilClass ``` See [#1285](https://github.com/ruby-grape/grape/pull/1285) for more information. #### Changes to Method Not Allowed routes A `405 Method Not Allowed` error now causes `Grape::Exceptions::MethodNotAllowed` to be raised, which will be rescued via `rescue_from :all`. Restore old behavior with the following error handler. ```ruby rescue_from Grape::Exceptions::MethodNotAllowed do |e| error! e.message, e.status, e.headers end ``` See [#1283](https://github.com/ruby-grape/grape/pull/1283) for more information. #### Changes to Grape::Exceptions::Validation parameters When raising `Grape::Exceptions::Validation` explicitly, replace `message_key` with `message`. For example, ```ruby fail Grape::Exceptions::Validation, params: [:oauth_token_secret], message_key: :presence ``` becomes ```ruby fail Grape::Exceptions::Validation, params: [:oauth_token_secret], message: :presence ``` See [#1295](https://github.com/ruby-grape/grape/pull/1295) for more information. ### Upgrading to >= 0.14.0 #### Changes to availability of DSL methods in filters The `#declared` method of the route DSL is no longer available in the `before` filter. Using `declared` in a `before` filter will now raise `Grape::DSL::InsideRoute::MethodNotYetAvailable`. See [#1074](https://github.com/ruby-grape/grape/issues/1074) for discussion of the issue. #### Changes to header versioning and invalid header version handling Identical endpoints with different versions now work correctly. A regression introduced in Grape 0.11.0 caused all but the first-mounted version for such an endpoint to wrongly throw an `InvalidAcceptHeader`. As a side effect, requests with a correct vendor but invalid version can no longer be rescued from a `rescue_from` block. See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information. #### Bypasses formatters when status code indicates no content To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) See [#1190](https://github.com/ruby-grape/grape/pull/1190) for more information. #### Redirects respond as plain text with message `#redirect` now uses `text/plain` regardless of whether that format has been enabled. This prevents formatters from attempting to serialize the message body and allows for a descriptive message body to be provided - and optionally overridden - that better fulfills the theme of the HTTP spec. See [#1194](https://github.com/ruby-grape/grape/pull/1194) for more information. ### Upgrading to >= 0.12.0 #### Changes in middleware The Rack response object is no longer converted to an array by the formatter, enabling streaming. If your custom middleware is accessing `@app_response`, update it to expect a `Rack::Response` instance instead of an array. For example, ```ruby class CacheBusterMiddleware < Grape::Middleware::Base def after @app_response[1]['Expires'] = Time.at(0).utc.to_s @app_response end end ``` becomes ```ruby class CacheBusterMiddleware < Grape::Middleware::Base def after @app_response.headers['Expires'] = Time.at(0).utc.to_s @app_response end end ``` See [#1029](https://github.com/ruby-grape/grape/pull/1029) for more information. There is a known issue because of this change. When Grape is used with an older than 1.2.4 version of [warden](https://github.com/hassox/warden) there may be raised the following exception having the [rack-mount](https://github.com/jm/rack-mount) gem's lines as last ones in the backtrace: ``` NoMethodError: undefined method `[]' for nil:NilClass ``` The issue can be solved by upgrading warden to 1.2.4 version. See [#1151](https://github.com/ruby-grape/grape/issues/1151) for more information. #### Changes in present Using `present` with objects that responded to `merge` would cause early evaluation of the represented object, with unexpected side-effects, such as missing parameters or environment within rendering code. Grape now only merges represented objects with a previously rendered body, usually when multiple `present` calls are made in the same route. See [grape-with-roar#5](https://github.com/dblock/grape-with-roar/issues/5) and [#1023](https://github.com/ruby-grape/grape/issues/1023). #### Changes to regexp validator Parameters with `nil` value will now pass `regexp` validation. To disallow `nil` value for an endpoint, add `allow_blank: false`. ```ruby params do requires :email, allow_blank: false, regexp: /.+@.+/ end ``` See [#957](https://github.com/ruby-grape/grape/pull/957) for more information. #### Replace error_response with error! in rescue_from blocks Note: `error_response` is being deprecated, not removed. ```ruby def error!(message, status = options[:default_status], headers = {}, backtrace = []) headers = { 'Content-Type' => content_type }.merge(headers) rack_response(format_message(message, backtrace), status, headers) end ``` For example, ``` error_response({ message: { message: 'No such page.', id: 'missing_page' }, status: 404, headers: { 'Content-Type' => 'api/error' }) ``` becomes ``` error!({ message: 'No such page.', id: 'missing_page' }, 404, { 'Content-Type' => 'api/error' }) ``` `error!` also supports just passing a message. `error!('Server error.')` and `format: :json` returns the following JSON response ``` { 'error': 'Server error.' } ``` with a status code of 500 and a Content Type of text/error. Optionally, also replace `Rack::Response.new` with `error!.` The following are equivalent: ``` Rack::Response.new([ e.message ], 500, { "Content-type" => "text/error" }).finish error!(e) ``` See [#889](https://github.com/ruby-grape/grape/issues/889) for more information. #### Changes to routes when using `format` Version 0.10.0 has introduced a change via [#809](https://github.com/ruby-grape/grape/pull/809) whereas routes no longer got file-type suffixes added if you declared a single API `format`. This has been reverted, it's now again possible to call API with proper suffix when single `format` is defined: ```ruby class API < Grape::API format :json get :hello do { hello: 'world' } end end ``` Will respond with JSON to `/hello` **and** `/hello.json`. Will respond with 404 to `/hello.xml`, `/hello.txt` etc. See the [#1001](https://github.com/ruby-grape/grape/pull/1001) and [#914](https://github.com/ruby-grape/grape/issues/914) for more info. ### Upgrading to >= 0.11.0 #### Added Rack 1.6.0 support Grape now supports, but doesn't require Rack 1.6.0. If you encounter an issue with parsing requests larger than 128KB, explictly require Rack 1.6.0 in your Gemfile. ```ruby gem 'rack', '~> 1.6.0' ``` See [#559](https://github.com/ruby-grape/grape/issues/559) for more information. #### Removed route_info Key route_info is excluded from params. See [#879](https://github.com/ruby-grape/grape/pull/879) for more information. #### Fix callbacks within a version block Callbacks defined in a version block are only called for the routes defined in that block. This was a regression introduced in Grape 0.10.0, and is fixed in this version. See [#901](https://github.com/ruby-grape/grape/pull/901) for more information. #### Make type of group of parameters required Groups of parameters now require their type to be set explicitly as Array or Hash. Not setting the type now results in MissingGroupTypeError, unsupported type will raise UnsupportedTypeError. See [#886](https://github.com/ruby-grape/grape/pull/886) for more information. ### Upgrading to >= 0.10.1 #### Changes to `declared(params, include_missing: false)` Attributes with `nil` values or with values that evaluate to `false` are no longer considered *missing* and will be returned when `include_missing` is set to `false`. See [#864](https://github.com/ruby-grape/grape/pull/864) for more information. ### Upgrading to >= 0.10.0 #### Changes to content-types The following content-types have been removed: * atom (application/atom+xml) * rss (application/rss+xml) * jsonapi (application/jsonapi) This is because they have never been properly supported. #### Changes to desc New block syntax: Former: ```ruby desc "some descs", detail: 'more details', entity: API::Entities::Entity, params: API::Entities::Status.documentation, named: 'a name', headers: [XAuthToken: { description: 'Valdates your identity', required: true } get nil, http_codes: [ [401, 'Unauthorized', API::Entities::BaseError], [404, 'not found', API::Entities::Error] ] do ``` Now: ```ruby desc "some descs" do detail 'more details' params API::Entities::Status.documentation success API::Entities::Entity failure [ [401, 'Unauthorized', API::Entities::BaseError], [404, 'not found', API::Entities::Error] ] named 'a name' headers [ XAuthToken: { description: 'Valdates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } ] end ``` #### Changes to Route Options and Descriptions A common hack to extend Grape with custom DSL methods was manipulating `@last_description`. ``` ruby module Grape module Extensions module SortExtension def sort(value) @last_description ||= {} @last_description[:sort] ||= {} @last_description[:sort].merge! value value end end Grape::API.extend self end end ``` You could access this value from within the API with `route.route_sort` or, more generally, via `env['api.endpoint'].options[:route_options][:sort]`. This will no longer work, use the documented and supported `route_setting`. ``` ruby module Grape module Extensions module SortExtension def sort(value) route_setting :sort, sort: value value end end Grape::API.extend self end end ``` To retrieve this value at runtime from within an API, use `env['api.endpoint'].route_setting(:sort)` and when introspecting a mounted API, use `route.route_settings[:sort]`. #### Accessing Class Variables from Helpers It used to be possible to fetch an API class variable from a helper function. For example: ```ruby @@static_variable = 42 helpers do def get_static_variable @@static_variable end end get do get_static_variable end ``` This will no longer work. Use a class method instead of a helper. ```ruby @@static_variable = 42 def self.get_static_variable @@static_variable end get do get_static_variable end ``` For more information see [#836](https://github.com/ruby-grape/grape/issues/836). #### Changes to Custom Validators To implement a custom validator, you need to inherit from `Grape::Validations::Base` instead of `Grape::Validations::Validator`. For more information see [Custom Validators](https://github.com/ruby-grape/grape#custom-validators) in the documentation. #### Changes to Raising Grape::Exceptions::Validation In previous versions raising `Grape::Exceptions::Validation` required a single `param`. ```ruby raise Grape::Exceptions::Validation, param: :id, message_key: :presence ``` The `param` argument has been deprecated and is now an array of `params`, accepting multiple values. ```ruby raise Grape::Exceptions::Validation, params: [:id], message_key: :presence ``` #### Changes to routes when using `format` Routes will no longer get file-type suffixes added if you declare a single API `format`. For example, ```ruby class API < Grape::API format :json get :hello do { hello: 'world' } end end ``` Pre-0.10.0, this would respond with JSON to `/hello`, `/hello.json`, `/hello.xml`, `/hello.txt`, etc. Now, this will only respond with JSON to `/hello`, but will be a 404 when trying to access `/hello.json`, `/hello.xml`, `/hello.txt`, etc. If you declare further `content_type`s, this behavior will be circumvented. For example, the following API will respond with JSON to `/hello`, `/hello.json`, `/hello.xml`, `/hello.txt`, etc. ```ruby class API < Grape::API format :json content_type :json, 'application/json' get :hello do { hello: 'world' } end end ``` See the [the updated API Formats documentation](https://github.com/ruby-grape/grape#api-formats) and [#809](https://github.com/ruby-grape/grape/pull/809) for more info. #### Changes to Evaluation of Permitted Parameter Values Permitted and default parameter values are now only evaluated lazily for each request when declared as a proc. The following code would raise an error at startup time. ```ruby params do optional :v, values: -> { [:x, :y] }, default: -> { :z } end ``` Remove the proc to get the previous behavior. ```ruby params do optional :v, values: [:x, :y], default: :z end ``` See [#801](https://github.com/ruby-grape/grape/issues/801) for more information. #### Changes to version If version is used with a block, the callbacks defined within that version block are not scoped to that individual block. In other words, the callback would be inherited by all versions blocks that follow the first one e.g ```ruby class API < Grape::API resource :foo do version 'v1', :using => :path do before do @output ||= 'hello1' end get '/' do @output += '-v1' end end version 'v2', :using => :path do before do @output ||= 'hello2' end get '/:id' do @output += '-v2' end end end end ``` when making a API call `GET /foo/v2/1`, the API would set instance variable `@output` to `hello1-v2` See [#898](https://github.com/ruby-grape/grape/issues/898) for more information. ### Upgrading to >= 0.9.0 #### Changes in Authentication The following middleware classes have been removed: * `Grape::Middleware::Auth::Basic` * `Grape::Middleware::Auth::Digest` * `Grape::Middleware::Auth::OAuth2` When you use theses classes directly like: ```ruby module API class Root < Grape::API class Protected < Grape::API use Grape::Middleware::Auth::OAuth2, token_class: 'AccessToken', parameter: %w(access_token api_key) ``` you have to replace these classes. As replacement can be used * `Grape::Middleware::Auth::Basic` => [`Rack::Auth::Basic`](https://github.com/rack/rack/blob/master/lib/rack/auth/basic.rb) * `Grape::Middleware::Auth::Digest` => [`Rack::Auth::Digest::MD5`](https://github.com/rack/rack/blob/master/lib/rack/auth/digest/md5.rb) * `Grape::Middleware::Auth::OAuth2` => [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth) and host these files within your application See [#703](https://github.com/ruby-grape/Grape/pull/703) for more information. ### Upgrading to >= 0.7.0 #### Changes in Exception Handling Assume you have the following exception classes defined. ```ruby class ParentError < StandardError; end class ChildError < ParentError; end ``` In Grape <= 0.6.1, the `rescue_from` keyword only handled the exact exception being raised. The following code would rescue `ParentError`, but not `ChildError`. ```ruby rescue_from ParentError do |e| # only rescue ParentError end ``` This made it impossible to rescue an exception hieararchy, which is a more sensible default. In Grape 0.7.0 or newer, both `ParentError` and `ChildError` are rescued. ```ruby rescue_from ParentError do |e| # rescue both ParentError and ChildError end ``` To only rescue the base exception class, set `rescue_subclasses: false`. ```ruby rescue_from ParentError, rescue_subclasses: false do |e| # only rescue ParentError end ``` See [#544](https://github.com/ruby-grape/grape/pull/544) for more information. #### Changes in the Default HTTP Status Code In Grape <= 0.6.1, the default status code returned from `error!` was 403. ```ruby error! "You may not reticulate this spline!" # yields HTTP error 403 ``` This was a bad default value, since 403 means "Forbidden". Change any call to `error!` that does not specify a status code to specify one. The new default value is a more sensible default of 500, which is "Internal Server Error". ```ruby error! "You may not reticulate this spline!", 403 # yields HTTP error 403 ``` You may also use `default_error_status` to change the global default. ```ruby default_error_status 400 ``` See [#525](https://github.com/ruby-grape/Grape/pull/525) for more information. #### Changes in Parameter Declaration and Validation In Grape <= 0.6.1, `group`, `optional` and `requires` keywords with a block accepted either an `Array` or a `Hash`. ```ruby params do requires :id, type: Integer group :name do requires :first_name requires :last_name end end ``` This caused the ambiguity and unexpected errors described in [#543](https://github.com/ruby-grape/Grape/issues/543). In Grape 0.7.0, the `group`, `optional` and `requires` keywords take an additional `type` attribute which defaults to `Array`. This means that without a `type` attribute, these nested parameters will no longer accept a single hash, only an array (of hashes). Whereas in 0.6.1 the API above accepted the following json, it no longer does in 0.7.0. ```json { "id": 1, "name": { "first_name": "John", "last_name" : "Doe" } } ``` The `params` block should now read as follows. ```ruby params do requires :id, type: Integer requires :name, type: Hash do requires :first_name requires :last_name end end ``` See [#545](https://github.com/ruby-grape/Grape/pull/545) for more information. ### Upgrading to 0.6.0 In Grape <= 0.5.0, only the first validation error was raised and processing aborted. Validation errors are now collected and a single `Grape::Exceptions::ValidationErrors` exception is raised. You can access the collection of validation errors as `.errors`. ```ruby rescue_from Grape::Exceptions::Validations do |e| Rack::Response.new({ status: 422, message: e.message, errors: e.errors }.to_json, 422) end ``` For more information see [#462](https://github.com/ruby-grape/grape/issues/462). grape-1.5.2/CONTRIBUTING.md0000644000004100000410000000670514016744305015112 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-1.5.2/grape.gemspec0000644000004100000410000000267514016744305015326 0ustar www-datawww-data# frozen_string_literal: true $LOAD_PATH.unshift 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.metadata = { 'bug_tracker_uri' => 'https://github.com/ruby-grape/grape/issues', 'changelog_uri' => "https://github.com/ruby-grape/grape/blob/v#{s.version}/CHANGELOG.md", 'documentation_uri' => "https://www.rubydoc.info/gems/grape/#{s.version}", 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}" } s.add_runtime_dependency 'activesupport' s.add_runtime_dependency 'builder' s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.0.0' s.add_runtime_dependency 'rack', '>= 1.3.0' s.add_runtime_dependency 'rack-accept' s.files = %w[CHANGELOG.md CONTRIBUTING.md README.md grape.png UPGRADING.md LICENSE] s.files += %w[grape.gemspec] s.files += Dir['lib/**/*'] s.test_files = Dir['spec/**/*'] s.require_paths = ['lib'] s.required_ruby_version = '>= 2.4.0' end