diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d1b804e0..f1000c1a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). * [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). * [#2464](https://github.com/ruby-grape/grape/pull/2464): The `length` validator only takes effect for parameters with types that support `#length` method - [@OuYangJinTing](https://github.com/OuYangJinTing). +* [#2485](https://github.com/ruby-grape/grape/pull/2485): Add `is:` param to length validator - [@dakad](https://github.com/dakad). * Your contribution here. ### 2.1.3 (2024-07-13) diff --git a/README.md b/README.md index add3905b87..ef738c9fff 100644 --- a/README.md +++ b/README.md @@ -1714,10 +1714,11 @@ end Parameters with types that support `#length` method can be restricted to have a specific length with the `:length` option. -The validator accepts `:min` or `:max` or both options to validate that the value of the parameter is within the given limits. +The validator accepts `:min` or `:max` or both options or only `:is` to validate that the value of the parameter is within the given limits. ```ruby params do + requires :code, type: String, length: { is: 2 } requires :str, type: String, length: { min: 3 } requires :list, type: [Integer], length: { min: 3, max: 5 } requires :hash, type: Hash, length: { max: 5 } @@ -2045,6 +2046,7 @@ end ```ruby params do + requires :code, type: String, length: { is: 2, message: 'code is expected to be exactly 2 characters long' } requires :str, type: String, length: { min: 5, message: 'str is expected to be atleast 5 characters long' } requires :list, type: [Integer], length: { min: 2, max: 3, message: 'list is expected to have between 2 and 3 elements' } end diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 3ed7bcc3ab..6b1b6ae06f 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -11,6 +11,7 @@ en: except_values: 'has a value not allowed' same_as: 'is not the same as %{parameter}' length: 'is expected to have length within %{min} and %{max}' + length_is: 'is expected to have length exactly equal to %{is}' length_min: 'is expected to have length greater than or equal to %{min}' length_max: 'is expected to have length less than or equal to %{max}' missing_vendor_option: diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb index ed266fe842..f844f047ec 100644 --- a/lib/grape/validations/validators/length_validator.rb +++ b/lib/grape/validations/validators/length_validator.rb @@ -7,12 +7,17 @@ class LengthValidator < Base def initialize(attrs, options, required, scope, **opts) @min = options[:min] @max = options[:max] + @is = options[:is] super raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?) raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?) raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max + + return if @is.nil? + raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive? + raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil? end def validate_param!(attr_name, params) @@ -20,7 +25,7 @@ def validate_param!(attr_name, params) return unless param.respond_to?(:length) - return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) + return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) end @@ -32,8 +37,10 @@ def build_message format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max elsif @min format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min - else + elsif @max format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max + else + format I18n.t(:length_is, scope: 'grape.errors.messages'), is: @is end end end diff --git a/spec/grape/validations/validators/length_spec.rb b/spec/grape/validations/validators/length_spec.rb index 9334b7a204..7e85b4dd84 100644 --- a/spec/grape/validations/validators/length_spec.rb +++ b/spec/grape/validations/validators/length_spec.rb @@ -86,6 +86,24 @@ end post '/custom-message' do end + + params do + requires :code, length: { is: 2 } + end + post 'is' do + end + + params do + requires :code, length: { is: -2 } + end + post 'negative_is' do + end + + params do + requires :code, length: { is: 2, max: 10 } + end + post 'is_with_max' do + end end end @@ -298,4 +316,54 @@ end end end + + describe '/is' do + context 'when length is exact' do + it do + post 'is', code: 'ZZ' + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length exceeds the limit' do + it do + post 'is', code: 'aze' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('code is expected to have length exactly equal to 2') + end + end + + context 'when length is less than the limit' do + it do + post 'is', code: 'a' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('code is expected to have length exactly equal to 2') + end + end + + context 'when length is zero' do + it do + post 'is', code: '' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('code is expected to have length exactly equal to 2') + end + end + end + + describe '/negative_is' do + context 'when `is` is negative' do + it do + expect { post 'negative_is', code: 'ZZ' }.to raise_error(ArgumentError, 'is must be an integer greater than zero') + end + end + end + + describe '/is_with_max' do + context 'when `is` is combined with max' do + it do + expect { post 'is_with_max', code: 'ZZ' }.to raise_error(ArgumentError, 'is cannot be combined with min or max') + end + end + end end