diff --git a/.gitignore b/.gitignore index af485e01f0..1d5c25bcce 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,11 @@ coverage doc pkg .rvmrc +.ruby-version +.ruby-gemset .bundle .yardoc/* +.byebug_history dist Gemfile.lock gemfiles/*.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a171f9ce3..6974707f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### 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). * Your contribution here. #### Fixes @@ -34,21 +35,21 @@ #### Features -* [#1536](https://github.com/ruby-grape/grape/pull/1536): Updates `invalid_versioner_option` translation - [@Lavode](https://github.com/Lavode). -* [#1543](https://github.com/ruby-grape/grape/pull/1543): Support ruby 2.4 - [@LeFnord](https://github.com/LeFnord), [@namusyaka](https://github.com/namusyaka). +* [#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): 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): Use 200 as default status for deletes that reply with content - [@jthornec](https://github.com/jthornec). +* [#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 (12/18/2016) #### Features -* [#1503](https://github.com/ruby-grape/grape/pull/1503): Allow to use regexp validator with arrays - [@akoltun](https://github.com/akoltun). -* [#1507](https://github.com/ruby-grape/grape/pull/1507): Add group attributes for parameter definitions - [@304](https://github.com/304). -* [#1532](https://github.com/ruby-grape/grape/pull/1532): Sets 204 as default status for delete - [@LeFnord](https://github.com/LeFnord). +* [#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 @@ -56,36 +57,35 @@ * [#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): Reduce warnings caused by instance variables not initialized - [@cpetschnig](https://github.com/cpetschnig). -* [#1531](https://github.com/ruby-grape/grape/pull/1531): Updates gem dependencies - [@LeFnord](https://github.com/LeFnord). +* [#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 (10/7/2016) #### Features -* [#1480](https://github.com/ruby-grape/grape/pull/1480): Use the ruby-grape-danger gem for PR linting - [@dblock](https://github.com/dblock). +* [#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): Drop support for ruby-2.0 - [@namusyaka](https://github.com/namusyaka). -* [#1490](https://github.com/ruby-grape/grape/pull/1490): Switch 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). +* [#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): Skip validations in inactive given blocks - [@jlfaber](https://github.com/jlfaber). -* [#1479](https://github.com/ruby-grape/grape/pull/1479): 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): 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): Coercion and lambda fails params validation - [@jonmchan](https://github.com/jonmchan). +* [#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 (7/29/2016) #### 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): Allow inserting middleware at arbitrary points in the middleware stack - [@rosa](https://github.com/rosa). -* [#1366](https://github.com/ruby-grape/grape/pull/1366): Store `message_key` on `Grape::Exceptions::Validation` - [@mkou](https://github.com/mkou). -* [#1398](https://github.com/ruby-grape/grape/pull/1398): Add `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): Extend `given` to receive a `Proc` - [@glaucocustodio](https://github.com/glaucocustodio). -* [#1455](https://github.com/ruby-grape/grape/pull/1455): Add an automated PR linter - [@orta](https://github.com/orta). +* [#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 diff --git a/Gemfile b/Gemfile index 58a721ea7a..406fe34eba 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/README.md b/README.md index 0958a5f3d7..b44ec6f2ba 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ - [Param](#param) - [Describing Methods](#describing-methods) - [Parameters](#parameters) + - [Params Class](#params-class) - [Declared](#declared) - [Include Missing](#include-missing) - [Parameter Validation and Coercion](#parameter-validation-and-coercion) @@ -495,7 +496,36 @@ In the case of conflict between either of: * `GET`, `POST` and `PUT` parameters * the contents of the request body on `POST` and `PUT` -route string parameters will have precedence. +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 +``` + +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 @@ -509,7 +539,7 @@ post 'users/signup' do end ```` -If we do not specify any params, `declared` will return an empty `Hashie::Mash` instance. +If you do not specify any parameters, `declared` will return an empty hash. **Request** @@ -526,7 +556,7 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d ```` -Once we add parameters requirements, grape will start returning only the declared params. +Once we add parameters requirements, grape will start returning only the declared parameters. ````ruby format :json @@ -562,17 +592,11 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d } ```` -The returned hash is a `Hashie::Mash` instance, allowing you to access parameters via dot notation: - -```ruby - declared(params).user == declared(params)['user'] -``` - +The returned hash is an `ActiveSupport::HashWithIndifferentAccess`. -The `#declared` method is not available to `before` filters, as those are evaluated prior -to parameter coercion. +The `#declared` method is not available to `before` filters, as those are evaluated prior to parameter coercion. -### Include parent namespaces +### 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`. @@ -897,18 +921,16 @@ 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`: +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 - # Parameter will be wrapped using Hashie: - params.avatar.filename # => 'avatar.png' - params.avatar.type # => 'image/png' - params.avatar.tempfile # => # + params[:avatar][:filename] # => 'avatar.png' + params[:avatar][:avatar] # => 'image/png' + params[:avatar][:tempfile] # => # end ``` @@ -1381,7 +1403,7 @@ class Admin < Grape::Validations::Base # @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 + 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 diff --git a/UPGRADING.md b/UPGRADING.md index c517dc6092..165e03443a 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,57 @@ Upgrading Grape =============== +### Upgrading to >= 1.0.0 + +#### 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. + +``` +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. + +``` +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. + ### Upgrading to >= 0.19.1 #### DELETE now defaults to status code 200 for responses with a body, or 204 otherwise diff --git a/gemfiles/rack_1.5.2.gemfile b/gemfiles/rack_1.5.2.gemfile index 632a14311c..ae9e8174d4 100644 --- a/gemfiles/rack_1.5.2.gemfile +++ b/gemfiles/rack_1.5.2.gemfile @@ -8,6 +8,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index b05ce40319..589bf59c45 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -8,6 +8,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/gemfiles/rails_3.gemfile b/gemfiles/rails_3.gemfile index 705be15125..29f753f494 100644 --- a/gemfiles/rails_3.gemfile +++ b/gemfiles/rails_3.gemfile @@ -9,6 +9,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/gemfiles/rails_4.gemfile b/gemfiles/rails_4.gemfile index c3dc4ae0ad..e3cab94687 100644 --- a/gemfiles/rails_4.gemfile +++ b/gemfiles/rails_4.gemfile @@ -8,6 +8,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 66a30197a6..e00ea92243 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -8,6 +8,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index 08dd3d6469..972f7fdee7 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -8,6 +8,7 @@ group :development, :test do gem 'bundler' gem 'rake' gem 'rubocop', '0.47.0' + gem 'hashie' end group :development do diff --git a/grape.gemspec b/grape.gemspec index 7a1caae813..93e7777690 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -18,7 +18,6 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'activesupport' s.add_runtime_dependency 'multi_json', '>= 1.3.2' s.add_runtime_dependency 'multi_xml', '>= 0.5.2' - s.add_runtime_dependency 'hashie', '>= 2.1.0' s.add_runtime_dependency 'virtus', '>= 1.0.0' s.add_runtime_dependency 'builder' diff --git a/lib/grape.rb b/lib/grape.rb index 8a24429fe5..932ef5107c 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -4,7 +4,6 @@ require 'rack/accept' require 'rack/auth/basic' require 'rack/auth/digest/md5' -require 'hashie' require 'set' require 'active_support/version' require 'active_support/core_ext/hash/indifferent_access' @@ -27,7 +26,7 @@ I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__) module Grape - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload eager_autoload do autoload :API @@ -48,14 +47,14 @@ module Grape end module Http - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload eager_autoload do autoload :Headers end end module Exceptions - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Base autoload :Validation autoload :ValidationArrayErrors @@ -78,16 +77,38 @@ module Exceptions autoload :MethodNotAllowed end + module Extensions + extend ::ActiveSupport::Autoload + + autoload :DeepMergeableHash + autoload :DeepSymbolizeHash + autoload :DeepHashWithIndifferentAccess + autoload :Hash + + module ActiveSupport + extend ::ActiveSupport::Autoload + + autoload :HashWithIndifferentAccess + end + + module Hashie + extend ::ActiveSupport::Autoload + + autoload :Mash + end + end + module Middleware - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Base autoload :Versioner autoload :Formatter autoload :Error autoload :Globals + autoload :Stack module Auth - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Base autoload :DSL autoload :StrategyInfo @@ -95,7 +116,7 @@ module Auth end module Versioner - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Path autoload :Header autoload :Param @@ -104,7 +125,7 @@ module Versioner end module Util - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :InheritableValues autoload :StackableValues autoload :ReverseStackableValues @@ -114,7 +135,7 @@ module Util end module ErrorFormatter - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Base autoload :Json autoload :Txt @@ -122,7 +143,7 @@ module ErrorFormatter end module Formatter - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Json autoload :SerializableHash autoload :Txt @@ -130,13 +151,13 @@ module Formatter end module Parser - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Json autoload :Xml end module DSL - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload eager_autoload do autoload :API autoload :Callbacks @@ -155,17 +176,17 @@ module DSL end class API - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Helpers end module Presenters - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :Presenter end module ServeFile - extend ActiveSupport::Autoload + extend ::ActiveSupport::Autoload autoload :FileResponse autoload :FileBody autoload :SendfileResponse diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 5cc6e13be7..281ff834ec 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -46,7 +46,7 @@ def declared_array(passed_params, options, declared_params) end def declared_hash(passed_params, options, declared_params) - declared_params.each_with_object(Hashie::Mash.new) do |declared_param, memo| + declared_params.each_with_object({}) do |declared_param, memo| # If it is not a Hash then it does not have children. # Find its value or set it to nil. if !declared_param.is_a?(Hash) @@ -56,7 +56,7 @@ def declared_hash(passed_params, options, declared_params) declared_param.each_pair do |declared_parent_param, declared_children_params| next unless options[:include_missing] || passed_params.key?(declared_parent_param) - passed_children_params = passed_params[declared_parent_param] || Hashie::Mash.new + passed_children_params = passed_params[declared_parent_param] || {} memo[optioned_param_key(declared_parent_param, options)] = declared(passed_children_params, options, declared_children_params) end end diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 7d3986ac09..2f427ad4fb 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -8,6 +8,32 @@ module DSL 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. # diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 9792199edd..3ff2d6c380 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -1,5 +1,3 @@ -require 'grape/middleware/stack' - module Grape # An Endpoint is the proxy scope in which all routing # blocks are executed. In other words, any methods @@ -239,15 +237,12 @@ def equals?(e) def run ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do @header = {} - - @request = Grape::Request.new(env) + @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with)) @params = @request.params @headers = @request.headers cookies.read(@request) - self.class.run_before_each(self) - run_filters befores, :before if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS]) diff --git a/lib/grape/extensions/active_support/hash_with_indifferent_access.rb b/lib/grape/extensions/active_support/hash_with_indifferent_access.rb new file mode 100644 index 0000000000..c6bf2ca1aa --- /dev/null +++ b/lib/grape/extensions/active_support/hash_with_indifferent_access.rb @@ -0,0 +1,26 @@ +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[rack_params] + params.deep_merge!(grape_routing_args) if env[Grape::Env::GRAPE_ROUTING_ARGS] + # TODO: remove, in Rails 4 or later ::ActiveSupport::HashWithIndifferentAccess converts nested Hashes into indifferent access ones + DeepHashWithIndifferentAccess.deep_hash_with_indifferent_access(params) + end + end + end + end + end +end diff --git a/lib/grape/extensions/deep_hash_with_indifferent_access.rb b/lib/grape/extensions/deep_hash_with_indifferent_access.rb new file mode 100644 index 0000000000..e18601f8d8 --- /dev/null +++ b/lib/grape/extensions/deep_hash_with_indifferent_access.rb @@ -0,0 +1,18 @@ +module Grape + module Extensions + module DeepHashWithIndifferentAccess + def self.deep_hash_with_indifferent_access(object) + case object + when ::Hash + object.inject(::ActiveSupport::HashWithIndifferentAccess.new) do |new_hash, (key, value)| + new_hash.merge!(key => deep_hash_with_indifferent_access(value)) + end + when ::Array + object.map { |element| deep_hash_with_indifferent_access(element) } + else + object + end + end + end + end +end diff --git a/lib/grape/extensions/deep_mergeable_hash.rb b/lib/grape/extensions/deep_mergeable_hash.rb new file mode 100644 index 0000000000..8f9903d62f --- /dev/null +++ b/lib/grape/extensions/deep_mergeable_hash.rb @@ -0,0 +1,19 @@ +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 diff --git a/lib/grape/extensions/deep_symbolize_hash.rb b/lib/grape/extensions/deep_symbolize_hash.rb new file mode 100644 index 0000000000..310a658af1 --- /dev/null +++ b/lib/grape/extensions/deep_symbolize_hash.rb @@ -0,0 +1,30 @@ +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 diff --git a/lib/grape/extensions/hash.rb b/lib/grape/extensions/hash.rb new file mode 100644 index 0000000000..77d0757630 --- /dev/null +++ b/lib/grape/extensions/hash.rb @@ -0,0 +1,23 @@ +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 diff --git a/lib/grape/extensions/hashie/mash.rb b/lib/grape/extensions/hashie/mash.rb new file mode 100644 index 0000000000..50958f3224 --- /dev/null +++ b/lib/grape/extensions/hashie/mash.rb @@ -0,0 +1,25 @@ +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 diff --git a/lib/grape/middleware/globals.rb b/lib/grape/middleware/globals.rb index 961536bf59..f356030e34 100644 --- a/lib/grape/middleware/globals.rb +++ b/lib/grape/middleware/globals.rb @@ -4,7 +4,7 @@ module Grape module Middleware class Globals < Base def before - request = Grape::Request.new(@env) + 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] diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 21f3c00869..0f11dff9e6 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -4,6 +4,11 @@ class Request < Rack::Request alias rack_params params + def initialize(env, options = {}) + extend options[:build_params_with] || Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + super(env) + end + def params @params ||= build_params end @@ -14,16 +19,12 @@ def headers private - def build_params - params = Hashie::Mash.new(rack_params) - if env[Grape::Env::GRAPE_ROUTING_ARGS] - args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup - # preserve version from query string parameters - args.delete(:version) - args.delete(:route_info) - params.deep_merge!(args) - end - params + 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 diff --git a/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index d273375fcc..da28474eb0 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -147,7 +147,7 @@ def infer_type_check(type) # Enforce symbolized keys for complex types # by wrapping the coercion method such that # any Hash objects in the immediate heirarchy - # are passed through +Hashie.symbolize_keys!+. + # have their keys recursively symbolized. # This helps common libs such as JSON to work easily. # # @param type see #new @@ -161,8 +161,8 @@ def enforce_symbolized_keys(type, method) if type == Array || type == Set lambda do |val| method.call(val).tap do |new_value| - new_value.each do |item| - Hashie.symbolize_keys!(item) if item.is_a? Hash + new_value.map do |item| + item.is_a?(Hash) ? symbolize_keys(item) : item end end end @@ -170,7 +170,7 @@ def enforce_symbolized_keys(type, method) # Hash objects are processed directly elsif type == Hash lambda do |val| - Hashie.symbolize_keys! method.call(val) + symbolize_keys method.call(val) end # Simple types are not processed. @@ -179,6 +179,20 @@ def enforce_symbolized_keys(type, method) 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 diff --git a/lib/grape/validations/types/file.rb b/lib/grape/validations/types/file.rb index be8b7be636..fe1aedc327 100644 --- a/lib/grape/validations/types/file.rb +++ b/lib/grape/validations/types/file.rb @@ -17,10 +17,9 @@ def coerce(input) def value_coerced?(value) # Rack::Request creates a Hash with filename, - # content type and an IO object. Grape wraps that - # using hashie for convenience. Do a bit of basic + # content type and an IO object. Do a bit of basic # duck-typing. - value.is_a?(Hashie::Mash) && value.key?(:tempfile) + value.is_a?(::Hash) && value.key?(:tempfile) end end end diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index f4710a6bb6..76db19d47c 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -10,6 +10,10 @@ def initialize(*_args) @converter = Types.build_coercer(type, @option[:method]) end + def validate(request) + super + end + def validate_param!(attr_name, params) raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless params.is_a? Hash new_value = coerce_value(params[attr_name]) diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 744555fe2a..1af9b73b94 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -249,6 +249,35 @@ def app end end + describe '#params' do + context 'default class' do + it 'should be a ActiveSupport::HashWithIndifferentAccess' do + subject.get '/foo' do + params.class + end + + get '/foo' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') + end + end + + context 'sets a value to params' do + it 'params' do + subject.params do + requires :a, type: String + end + subject.get '/foo' do + params[:a] = 'bar' + end + + get '/foo', a: 'foo' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('bar') + end + end + end + describe '#declared' do before do subject.format :json @@ -775,6 +804,7 @@ def app 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') @@ -911,6 +941,21 @@ def app 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 diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb new file mode 100644 index 0000000000..27371fd983 --- /dev/null +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -0,0 +1,83 @@ +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 diff --git a/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb b/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb new file mode 100644 index 0000000000..1c2ea2c4f9 --- /dev/null +++ b/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb @@ -0,0 +1,105 @@ +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 diff --git a/spec/grape/extensions/param_builders/hashie/mash_spec.rb b/spec/grape/extensions/param_builders/hashie/mash_spec.rb new file mode 100644 index 0000000000..0a5975f2d5 --- /dev/null +++ b/spec/grape/extensions/param_builders/hashie/mash_spec.rb @@ -0,0 +1,79 @@ +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 diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index 45b9ef0264..b763532ca4 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -30,8 +30,18 @@ module Grape } end - it 'returns params' do - expect(request.params).to eq('a' => '123', 'b' => 'xyz') + 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 @@ -47,7 +57,7 @@ module Grape end it 'cuts version and route_info' do - expect(request.params).to eq('a' => '123', 'b' => 'xyz', 'c' => 'ccc') + expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) end end end diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index ca7ef224ae..f4f723d48a 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -285,7 +285,7 @@ class User requires :file, type: Rack::Multipart::UploadedFile end subject.post '/upload' do - params[:file].filename + params[:file][:filename] end post '/upload', file: Rack::Test::UploadedFile.new(__FILE__) @@ -302,7 +302,7 @@ class User requires :file, coerce: File end subject.post '/upload' do - params[:file].filename + params[:file][:filename] end post '/upload', file: Rack::Test::UploadedFile.new(__FILE__) @@ -625,7 +625,7 @@ class User get '/', a: %w(the other) expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') + expect(last_response.body).to eq('["the", "other"]') get '/', a: { a: 1, b: 2 } expect(last_response.status).to eq(400) @@ -633,27 +633,27 @@ class User get '/', a: [1, 2, 3] expect(last_response.status).to eq(200) - expect(last_response.body).to eq('#') + 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('#') + 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('#') + 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('#') + 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('#') + expect(last_response.body).to eq('[1, 2, "true", "three"]') get '/', d: '1' expect(last_response.status).to eq(200) @@ -669,6 +669,78 @@ class User 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