diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index 9377e4c4e0..372d31042c 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -10,6 +10,9 @@ en: values: 'does not have a valid value' except_values: 'has a value not allowed' same_as: 'is not the same as %{parameter}' + length_mismatch_both: 'is expected to have a length within %{min_length} and %{max_length}' + length_mismatch_min_only: 'is expected to have a length greater than %{min_length}' + length_mismatch_max_only: 'is expected to have a length less than %{max_length}' missing_vendor_option: problem: 'missing :vendor option' summary: 'when version using header, you must specify :vendor option' diff --git a/lib/grape/validations/attributes_doc.rb b/lib/grape/validations/attributes_doc.rb index f9e15c148a..ab835dceb0 100644 --- a/lib/grape/validations/attributes_doc.rb +++ b/lib/grape/validations/attributes_doc.rb @@ -28,6 +28,12 @@ def extract_details(validations) details[:documentation] = documentation if documentation details[:default] = validations[:default] if validations.key?(:default) + + return unless validations.key?(:length) + + length_validation = validations[:length] + details[:min_length] = length_validation[:min] if length_validation.key?(:min) + details[:max_length] = length_validation[:max] if length_validation.key?(:max) end def document(attrs) diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb new file mode 100644 index 0000000000..fc322e3f96 --- /dev/null +++ b/lib/grape/validations/validators/length_validator.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Grape + module Validations + module Validators + class LengthValidator < Base + def initialize(attrs, options, required, scope, **opts) + @min_length = options[:min] + @max_length = options[:max] + super + end + + def validate_param!(attr_name, params) + param = params[attr_name] + return unless param.respond_to?(:length) + return unless (@min_length && param.length < @min_length) || (@max_length && param.length > @max_length) + + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) + end + + def build_message + if options_key?(:message) + @option[:message] + elsif @min_length && @max_length + format I18n.t(:length_mismatch_both, scope: 'grape.errors.messages'), min_length: @min_length, max_length: @max_length + elsif @min_length + format I18n.t(:length_mismatch_min_only, scope: 'grape.errors.messages'), min_length: @min_length + else + format I18n.t(:length_mismatch_max_only, scope: 'grape.errors.messages'), max_length: @max_length + end + end + end + end + end +end diff --git a/spec/grape/validations/attributes_doc_spec.rb b/spec/grape/validations/attributes_doc_spec.rb index f1ae0c93e1..d5ae81d2ff 100644 --- a/spec/grape/validations/attributes_doc_spec.rb +++ b/spec/grape/validations/attributes_doc_spec.rb @@ -31,7 +31,8 @@ presence: true, desc: 'Age of...', documentation: 'Age is...', - default: 1 + default: 1, + length: { min: 1, max: 13 } } end @@ -77,7 +78,9 @@ documentation: validations[:documentation], default: validations[:default], type: 'Integer', - values: valid_values + values: valid_values, + min_length: validations[:length][:min], + max_length: validations[:length][:max] } end diff --git a/spec/grape/validations/validators/length_spec.rb b/spec/grape/validations/validators/length_spec.rb new file mode 100644 index 0000000000..bc597c0dc9 --- /dev/null +++ b/spec/grape/validations/validators/length_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +describe Grape::Validations::Validators::LengthValidator do + let_it_be(:app) do + Class.new(Grape::API) do + params do + requires :list, length: { min: 2, max: 3 } + end + post 'with_min_max' do + end + + params do + requires :list, type: [Integer], length: { min: 2 } + end + post 'with_min_only' do + end + + params do + requires :list, type: [Integer], length: { max: 3 } + end + post 'with_max_only' do + end + + params do + requires :list, type: Integer, length: { max: 3 } + end + post 'type_is_not_array' do + end + + params do + requires :list, type: [Integer], length: { min: 2, message: 'not match' } + end + post '/custom-message' do + end + end + end + + describe '/with_min_max' do + context 'when length is within limits' do + it do + post '/with_min_max', list: [1, 2] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is exceeded' do + it do + post '/with_min_max', list: [1, 2, 3, 4, 5] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have a length within 2 and 3') + end + end + + context 'when length is less than minimum' do + it do + post '/with_min_max', list: [1] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have a length within 2 and 3') + end + end + end + + describe '/with_max_only' do + context 'when length is less than limits' do + it do + post '/with_max_only', list: [1, 2] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is exceeded' do + it do + post '/with_max_only', list: [1, 2, 3, 4, 5] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have a length less than 3') + end + end + end + + describe '/with_min_only' do + context 'when length is greater than limit' do + it do + post '/with_min_only', list: [1, 2] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'when length is less than limit' do + it do + post '/with_min_only', list: [1] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list is expected to have a length greater than 2') + end + end + end + + describe '/type_is_not_array' do + context 'is no op' do + it do + post 'type_is_not_array', list: 12 + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + end + + describe '/custom-message' do + context 'is within limits' do + it do + post '/custom-message', list: [1, 2, 3] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('') + end + end + + context 'is outside limit' do + it do + post '/custom-message', list: [1] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('list not match') + end + end + end +end