diff --git a/CHANGELOG.md b/CHANGELOG.md index 63badfc1ae..8526c0a6e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,9 @@ * [#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): 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). * Your contribution here. - + #### Fixes * [#1430](https://github.com/ruby-grape/grape/pull/1430): Fix for `declared(params)` inside `route_param` - [@Arkanain](https://github.com/Arkanain). diff --git a/README.md b/README.md index fd0bd1f311..7c7592271e 100644 --- a/README.md +++ b/README.md @@ -987,6 +987,19 @@ params do 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 +``` + ### Built-in Validators #### `allow_blank` diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index bafe14754f..1dd2b1f2eb 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -167,7 +167,8 @@ def all_or_none_of(*attrs) # @yield a parameter definition DSL def given(*attrs, &block) attrs.each do |attr| - raise Grape::Exceptions::UnknownParameter.new(attr) unless declared_param?(attr) + attr_ = attr.is_a?(Hash) ? attr.keys[0] : attr + raise Grape::Exceptions::UnknownParameter.new(attr_) unless declared_param?(attr_) end new_lateral_scope(dependent_on: attrs, &block) end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 032ae69f31..08a4d9e856 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -39,7 +39,13 @@ def should_validate?(parameters) any_element_blank?(parameters)) @dependent_on.each do |dependency| - return false if params(parameters).try(:[], dependency).blank? + if dependency.is_a?(Hash) + dependency_key = dependency.keys[0] + proc = dependency.values[0] + return false unless proc.call(params(parameters).try(:[], dependency_key)) + elsif params(parameters).try(:[], dependency).blank? + return false + end end if @dependent_on return true if parent.nil? diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 693205ca90..0eddd548a0 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -405,4 +405,91 @@ def initialize(value) expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } }) end end + + context 'when validations are dependent on a parameter with specific value' do + before do + subject.params do + optional :a + given a: ->(val) { val == 'x' } do + requires :b + end + end + subject.get('/test') { declared(params).to_json } + end + + it 'applies the validations only if the parameter has the specific value' do + get '/test' + expect(last_response.status).to eq(200) + + get '/test', a: 'x' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('b is missing') + + get '/test', a: 'x', b: true + expect(last_response.status).to eq(200) + end + + it 'raises an error if the dependent parameter was never specified' do + expect do + subject.params do + given :c do + end + end + end.to raise_error(Grape::Exceptions::UnknownParameter) + end + + it 'includes the parameter within #declared(params)' do + get '/test', a: true, b: true + + expect(JSON.parse(last_response.body)).to eq('a' => 'true', 'b' => 'true') + end + + it 'returns a sensible error message within a nested context' do + subject.params do + requires :bar, type: Hash do + optional :a + given a: ->(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 + end end