Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#### Fixes

* [#2655](https://github.com/ruby-grape/grape/pull/2655): Fix `before_each` method to handle `nil` parameter correctly - [@ericproulx](https://github.com/ericproulx).
* [#2658](https://github.com/ruby-grape/grape/pull/XXXX): Custom validators resolved from API namespace without explicit require (fixes #1178) - [@GulfGulfinson](https://github.com/GulfGulfinson).
* Your contribution here.

### 3.1.0 (2026-01-25)
Expand Down
1 change: 1 addition & 0 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class << self
extend Forwardable

attr_accessor :configuration
attr_reader :base

def_delegators :@base, :to_s

Expand Down
36 changes: 33 additions & 3 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,40 @@ module Validations

module_function

def require_validator(short_name)
raise Grape::Exceptions::UnknownValidator, short_name unless registry.key?(short_name)
def require_validator(short_name, api_class: nil)
return registry[short_name] if registry.key?(short_name)

registry[short_name]
klass = find_validator_in_namespace(short_name, api_class) if api_class
raise Grape::Exceptions::UnknownValidator, short_name unless klass

klass
end

def find_validator_in_namespace(short_name, api_class)
const_name = short_name.to_s.camelize
lookup_class = api_class
lookup_class = lookup_class.superclass while lookup_class && lookup_class.name.blank?
name = lookup_class&.name
return nil if name.blank?

parent_name = name.rpartition('::').first
return nil if parent_name.blank?

namespace = Object.const_get(parent_name)
while namespace.is_a?(Module) && namespace != Object
if namespace.const_defined?(:Validators, false)
validators_mod = namespace.const_get(:Validators)
if validators_mod.const_defined?(const_name, false)
klass = validators_mod.const_get(const_name)
return klass if klass.is_a?(Class) && klass < Validators::Base
end
end
parent_name = namespace.name.rpartition('::').first
break if parent_name.blank?

namespace = Object.const_get(parent_name)
end
nil
end

def build_short_name(klass)
Expand Down
4 changes: 3 additions & 1 deletion lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -464,13 +464,15 @@ def check_incompatible_option_values(default, values, except_values)
end

def validate(type, options, attrs, required, opts)
api_class = @api.is_a?(Module) ? @api : @api.class
api_class = api_class.base if api_class.respond_to?(:base) && api_class.base
validator_options = {
attributes: attrs,
options: options,
required: required,
params_scope: self,
opts: opts,
validator_class: Validations.require_validator(type)
validator_class: Validations.require_validator(type, api_class: api_class)
}
@api.inheritable_setting.namespace_stackable[:validations] = validator_options
end
Expand Down
70 changes: 70 additions & 0 deletions spec/grape/api/custom_validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,74 @@ def validate_param!(_attr_name, _params)
expect(last_response.status).to eq 200
end
end

describe 'custom validator namespace lookup' do
let(:uuid_validator) do
regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
Class.new(Grape::Validations::Validators::Base) do
define_singleton_method(:regex) { regex }
def validate_param!(attr_name, params)
return if params[attr_name].to_s.match?(self.class.regex)

raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: 'must be a valid UUID')
end
end
end

context 'when constants are set up' do
before do
described_class.deregister(:uuid)
stub_const('Api', Module.new)
stub_const('Api::V2', Module.new)
stub_const('Api::V2::Validators', Module.new)
Api::V2::Validators.const_set(:Uuid, uuid_validator)
Api::V2.const_set(:Jobs, Class.new(Grape::API))
end

after do
described_class.deregister(:uuid)
end

it 'finds validator via require_validator with api_class' do
expect(described_class.require_validator(:uuid, api_class: Api::V2::Jobs)).to eq Api::V2::Validators::Uuid
end
end

context 'when API uses uuid param' do
before do
described_class.deregister(:uuid)
stub_const('Api', Module.new)
stub_const('Api::V2', Module.new)
stub_const('Api::V2::Validators', Module.new)
Api::V2::Validators.const_set(:Uuid, uuid_validator)
Api::V2.const_set(:Jobs, Class.new(Grape::API))
Api::V2::Jobs.class_eval do
params do
requires :id, type: String, uuid: true
end
get ':id' do
'ok'
end
end
end

after do
described_class.deregister(:uuid)
end

let(:app) { Api::V2::Jobs }

it 'finds validator in API namespace without explicit require' do
get '/550e8400-e29b-41d4-a716-446655440000'
expect(last_response.status).to eq 200
expect(last_response.body).to eq 'ok'
end

it 'validates with namespace validator and returns 400 for invalid UUID' do
get '/not-a-uuid'
expect(last_response.status).to eq 400
expect(last_response.body).to include 'must be a valid UUID'
end
end
end
end
Loading