Skip to content
Draft
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 @@
* [#2663](https://github.com/ruby-grape/grape/pull/2663): Refactor `ParamsScope` and `Parameters` DSL to use named kwargs - [@ericproulx](https://github.com/ericproulx).
* [#2664](https://github.com/ruby-grape/grape/pull/2664): Drop `test-prof` dependency - [@ericproulx](https://github.com/ericproulx).
* [#2665](https://github.com/ruby-grape/grape/pull/2665): Pass `attrs` directly to `AttributesIterator` instead of `validator` - [@ericproulx](https://github.com/ericproulx).
* [#2657](https://github.com/ruby-grape/grape/pull/2657): Instantiate validators at definition time - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
21 changes: 7 additions & 14 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def run
status 204
else
run_filters before_validations, :before_validation
run_validators validations, request
run_validators request: request
run_filters after_validations, :after_validation
response_object = execute
end
Expand Down Expand Up @@ -205,11 +205,14 @@ def execute
end
end

def run_validators(validators, request)
def run_validators(request:)
validators = inheritable_setting.route[:saved_validations]
return if validators.empty?

validation_errors = []

Grape::Validations::ParamScopeTracker.track do
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request:) do
validators.each do |validator|
validator.validate(request)
rescue Grape::Exceptions::Validation => e
Expand All @@ -222,7 +225,7 @@ def run_validators(validators, request)
end
end

validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header))
raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header)) if validation_errors.any?
end

def run_filters(filters, type = :other)
Expand All @@ -239,16 +242,6 @@ def run_filters(filters, type = :other)
end
end

def validations
saved_validations = inheritable_setting.route[:saved_validations]
return if saved_validations.nil?
return enum_for(:validations) unless block_given?

saved_validations.each do |saved_validation|
yield Grape::Validations::ValidatorFactory.create_validator(saved_validation)
end
end

def options?
options[:options_route_enabled] &&
env[Rack::REQUEST_METHOD] == Rack::OPTIONS
Expand Down
41 changes: 41 additions & 0 deletions lib/grape/util/deep_freeze.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Grape
module Util
module DeepFreeze
module_function

# Recursively freezes Hash (keys and values), Array (elements), and String
# objects. All other types are returned as-is.
#
# Already-frozen objects (including Symbols, Integers, true/false/nil, and
# any object that was previously frozen) are returned immediately via the
# +obj.frozen?+ guard.
#
# Intentionally left unfrozen:
# - Procs / lambdas — may be deferred DB-backed callables
# - Coercers (e.g. ArrayCoercer) — use lazy ivar memoization at request time
# - Classes / Modules — shared constants that must remain open
# - ParamsScope — self-freezes at the end of its own initialize
def deep_freeze(obj)
return obj if obj.frozen?

case obj
when Hash
obj.each do |k, v|
deep_freeze(k)
deep_freeze(v)
end
obj.freeze
when Array
obj.each { |v| deep_freeze(v) }
obj.freeze
when String
obj.freeze
else
obj
end
end
end
end
end
8 changes: 1 addition & 7 deletions lib/grape/validations/contract_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,7 @@ def initialize(api, contract = nil, &block)
end

api.inheritable_setting.namespace_stackable[:contract_key_map] = key_map

validator_options = {
validator_class: Grape::Validations.require_validator(:contract_scope),
opts: { schema: contract, fail_fast: false }
}

api.inheritable_setting.namespace_stackable[:validations] = validator_options
api.inheritable_setting.namespace_stackable[:validations] = Validators::ContractScopeValidator.new(schema: contract)
end
end
end
Expand Down
77 changes: 38 additions & 39 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
module Grape
module Validations
class ParamsScope
attr_accessor :element, :parent
attr_reader :type, :nearest_array_ancestor
attr_reader :parent, :type, :nearest_array_ancestor, :full_path

def qualifying_params
ParamScopeTracker.current&.qualifying_params(self)
Expand All @@ -21,7 +20,7 @@ def qualifying_params
SPECIAL_JSON = [JSON, Array[JSON]].freeze

class Attr
attr_accessor :key, :scope
attr_reader :key, :scope

# Open up a new ParamsScope::Attr
# @param key [Hash, Symbol] key of attr
Expand Down Expand Up @@ -77,11 +76,13 @@ def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional:
# instance_eval, so local variables from initialize are unreachable.
# configure_declared_params consumes it and clears @declared_params to nil.
@declared_params = []
@full_path = build_full_path

instance_eval(&block) if block

configure_declared_params
@nearest_array_ancestor = find_nearest_array_ancestor
freeze
end

def configuration
Expand All @@ -95,9 +96,9 @@ def should_validate?(parameters)

return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
return false unless meets_dependency?(scoped_params, parameters)
return true if parent.nil?
return true if @parent.nil?

parent.should_validate?(parameters)
@parent.should_validate?(parameters)
end

def meets_dependency?(params, request_params)
Expand All @@ -124,17 +125,14 @@ def meets_hash_dependency?(params)
# params might be anything what looks like a hash, so it must implement a `key?` method
return false unless params.respond_to?(:key?)

@dependent_on.each do |dependency|
@dependent_on.all? do |dependency|
if dependency.is_a?(Hash)
dependency_key = dependency.keys[0]
proc = dependency.values[0]
return false unless proc.call(params[dependency_key])
elsif params[dependency].blank?
return false
key, callable = dependency.first
callable.call(params[key])
else
params[dependency].present?
end
end

true
end

# @return [String] the proper attribute name, with nesting considered.
Expand Down Expand Up @@ -194,21 +192,18 @@ def push_declared_params(attrs, **opts)
@declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
end

# Get the full path of the parameter scope in the hierarchy.
#
# @return [Array<Symbol>] the nesting/path of the current parameter scope
def full_path
private

def build_full_path
if nested?
(@parent.full_path + [@element])
@parent.full_path + [@element]
elsif lateral?
@parent.full_path
else
[]
end
end

private

# Add a new parameter which should be renamed when using the +#declared+
# method.
#
Expand Down Expand Up @@ -314,17 +309,19 @@ def new_group_scope(group, &)
self.class.new(api: @api, parent: self, group: group, &)
end

# Pushes declared params to parent or settings
# Pushes declared params to parent or settings, then clears @declared_params.
# Clearing here (rather than in initialize) keeps the lifecycle ownership in
# one place: this method both consumes and invalidates the ivar so that
# push_declared_params cannot be called on the frozen scope later.
def configure_declared_params
push_renamed_param(full_path, @element_renamed) if @element_renamed

if nested?
@parent.push_declared_params [element => @declared_params]
@parent.push_declared_params [@element => @declared_params]
else
@api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
end

# params were stored in settings, it can be cleaned from the params scope
ensure
@declared_params = nil
end

Expand Down Expand Up @@ -354,15 +351,15 @@ def validates(attrs, validations)

document_params attrs, validations, coerce_type, values, except_values

opts = derive_validator_options(validations)
opts = derive_validator_options(validations).freeze

# Validate for presence before any other validators
validates_presence(validations, attrs, opts)

# Before we run the rest of the validators, let's handle
# whatever coercion so that we are working with correctly
# type casted values
coerce_type validations, attrs, required, opts
coerce_type validations.extract!(:coerce, :coerce_with, :coerce_message), attrs, required, opts

validations.each do |type, options|
# Don't try to look up validators for documentation params that don't have one.
Expand Down Expand Up @@ -435,17 +432,19 @@ def check_coerce_with(validations)
def coerce_type(validations, attrs, required, opts)
check_coerce_with(validations)

return unless validations.key?(:coerce)
# Falsy check (not key?) is intentional: when a remountable API is first
# evaluated on its base instance (no configuration supplied yet),
# configuration[:some_type] evaluates to nil. Skipping instantiation
# here is correct — the real mounted instance will replay this step with
# the actual type value.
return unless validations[:coerce]

coerce_options = {
type: validations[:coerce],
method: validations[:coerce_with],
message: validations[:coerce_message]
}
validate('coerce', coerce_options, attrs, required, opts)
validations.delete(:coerce_with)
validations.delete(:coerce)
validations.delete(:coerce_message)
end

def guess_coerce_type(coerce_type, *values_list)
Expand All @@ -469,15 +468,15 @@ def check_incompatible_option_values(default, values, except_values)
end

def validate(type, options, attrs, required, opts)
validator_options = {
attributes: attrs,
options: options,
required: required,
params_scope: self,
opts: opts,
validator_class: Validations.require_validator(type)
}
@api.inheritable_setting.namespace_stackable[:validations] = validator_options
validator_class = Validations.require_validator(type)
validator_instance = validator_class.new(
attrs,
options,
required,
self,
opts
)
@api.inheritable_setting.namespace_stackable[:validations] = validator_instance
end

def validate_value_coercion(coerce_type, *values_list)
Expand Down
15 changes: 0 additions & 15 deletions lib/grape/validations/validator_factory.rb

This file was deleted.

9 changes: 7 additions & 2 deletions lib/grape/validations/validators/all_or_none_of_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ module Grape
module Validations
module Validators
class AllOrNoneOfValidator < MultipleParamsBase
def initialize(attrs, options, required, scope, opts)
super
@exception_message = message(:all_or_none)
end

def validate_params!(params)
keys = keys_in_common(params)
return if keys.empty? || keys.length == all_keys.length
return if keys.empty? || keys.length == @attrs.length

raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none))
raise Grape::Exceptions::Validation.new(params: all_keys, message: @exception_message)
end
end
end
Expand Down
15 changes: 10 additions & 5 deletions lib/grape/validations/validators/allow_blank_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ module Grape
module Validations
module Validators
class AllowBlankValidator < Base
def validate_param!(attr_name, params)
return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash)
def initialize(attrs, options, required, scope, opts)
super

@value = option_value
@exception_message = message(:blank)
end

value = params[attr_name]
value = value.scrub if value.respond_to?(:valid_encoding?) && !value.valid_encoding?
def validate_param!(attr_name, params)
return if @value || !hash_like?(params)

value = scrub(params[attr_name])
return if value == false || value.present?

raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank))
validation_error!(attr_name)
end
end
end
Expand Down
9 changes: 7 additions & 2 deletions lib/grape/validations/validators/at_least_one_of_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ module Grape
module Validations
module Validators
class AtLeastOneOfValidator < MultipleParamsBase
def initialize(attrs, options, required, scope, opts)
super
@exception_message = message(:at_least_one)
end

def validate_params!(params)
return unless keys_in_common(params).empty?
return if keys_in_common(params).any?

raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one))
raise Grape::Exceptions::Validation.new(params: all_keys, message: @exception_message)
end
end
end
Expand Down
Loading
Loading