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 @@ -5,6 +5,7 @@
* [#2629](https://github.com/ruby-grape/grape/pull/2629): Refactor Router Architecture - [@ericproulx](https://github.com/ericproulx).
* [#2633](https://github.com/ruby-grape/grape/pull/2633): Refactor API::Instance and reorganize DSL modules - [@ericproulx](https://github.com/ericproulx).
* [#2636](https://github.com/ruby-grape/grape/pull/2636): Refactor router to simplify method signatures and reduce duplication - [@ericproulx](https://github.com/ericproulx).
* [#2637](https://github.com/ruby-grape/grape/pull/2637): Refactor declared method - [@ericproulx](https://github.com/ericproulx).
* [#2639](https://github.com/ruby-grape/grape/pull/2639): Refactor mime_types_for - [@ericproulx](https://github.com/ericproulx).
* [#2638](https://github.com/ruby-grape/grape/pull/2638): Remove unnecessary path string duplication - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.
Expand Down
134 changes: 134 additions & 0 deletions lib/grape/dsl/declared.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# frozen_string_literal: true

module Grape
module DSL
module Declared
# Denotes a situation where a DSL method has been invoked in a
# filter which it should not yet be available in
class MethodNotYetAvailable < StandardError
def initialize(msg = '#declared is not available prior to parameter validation')
super
end
end

# A filtering method that will return a hash
# consisting only of keys that have been declared by a
# `params` statement against the current/target endpoint or parent
# namespaces.
# @param params [Hash] The initial hash to filter. Usually this will just be `params`
# @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
# options. `:include_parent_namespaces` defaults to true, hence must be set to false if
# you want only to return params declared against the current/target endpoint.
def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
raise MethodNotYetAvailable unless before_filter_passed

options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
declared_params ||= optioned_declared_params(options[:include_parent_namespaces])

res = if passed_params.is_a?(Array)
declared_array(passed_params, options, declared_params, params_nested_path)
else
declared_hash(passed_params, options, declared_params, params_nested_path)
end

if (key_maps = inheritable_setting.namespace_stackable[:contract_key_map])
key_maps.each { |key_map| key_map.write(passed_params, res) }
end

res
end

private

def declared_array(passed_params, options, declared_params, params_nested_path)
passed_params.map do |passed_param|
declared(passed_param || {}, options, declared_params, params_nested_path)
end
end

def declared_hash(passed_params, options, declared_params, params_nested_path)
declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params)

declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo)
end
end

def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo)
renamed_params = inheritable_setting.route[:renamed_params] || {}
if declared_param.is_a?(Hash)
declared_param.each_pair do |declared_parent_param, declared_children_params|
params_nested_path_dup = params_nested_path.dup
params_nested_path_dup << declared_parent_param.to_s
next unless options[:include_missing] || passed_params.key?(declared_parent_param)

rename_path = params_nested_path + [declared_parent_param.to_s]
renamed_param_name = renamed_params[rename_path]

memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new

memo[memo_key] = handle_passed_param(params_nested_path_dup, has_passed_children: passed_children_params.any?) do
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
end
end
else
# If it is not a Hash then it does not have children.
# Find its value or set it to nil.
return unless options[:include_missing] || passed_params.try(:key?, declared_param)

rename_path = params_nested_path + [declared_param.to_s]
renamed_param_name = renamed_params[rename_path]

memo_key = optioned_param_key(renamed_param_name || declared_param, options)
passed_param = passed_params[declared_param]

params_nested_path_dup = params_nested_path.dup
params_nested_path_dup << declared_param.to_s
memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
passed_param
end
end
end

def handle_passed_param(params_nested_path, has_passed_children: false, &_block)
return yield if has_passed_children

key = params_nested_path[0]
key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1

route_options_params = options[:route_options][:params] || {}
type = route_options_params.dig(key, :type)
has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }

if type == 'Hash' && !has_children
{}
elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
[]
elsif type == 'Set' || type&.start_with?('#<Set')
Set.new
else
yield
end
end

def optioned_param_key(declared_param, options)
options[:stringify] ? declared_param.to_s : declared_param.to_sym
end

def optioned_declared_params(include_parent_namespaces)
declared_params = if include_parent_namespaces
# Declared params including parent namespaces
inheritable_setting.route[:declared_params]
else
# Declared params at current namespace
inheritable_setting.namespace_stackable[:declared_params].last || []
end

raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params

declared_params
end
end
end
end
144 changes: 3 additions & 141 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,148 +3,10 @@
module Grape
module DSL
module InsideRoute
# Denotes a situation where a DSL method has been invoked in a
# filter which it should not yet be available in
class MethodNotYetAvailable < StandardError; end
include Declared

# @param type [Symbol] The type of filter for which evaluation has been
# completed
# @return [Module] A module containing method overrides suitable for the
# position in the filter evaluation sequence denoted by +type+. This
# defaults to an empty module if no overrides are defined for the given
# filter +type+.
def self.post_filter_methods(type)
@post_filter_modules ||= { before: PostBeforeFilter }
@post_filter_modules[type]
end

# Methods which should not be available in filters until the before filter
# has completed
module PostBeforeFilter
def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
declared_params ||= optioned_declared_params(options[:include_parent_namespaces])

res = if passed_params.is_a?(Array)
declared_array(passed_params, options, declared_params, params_nested_path)
else
declared_hash(passed_params, options, declared_params, params_nested_path)
end

if (key_maps = inheritable_setting.namespace_stackable[:contract_key_map])
key_maps.each { |key_map| key_map.write(passed_params, res) }
end

res
end

private

def declared_array(passed_params, options, declared_params, params_nested_path)
passed_params.map do |passed_param|
declared(passed_param || {}, options, declared_params, params_nested_path)
end
end

def declared_hash(passed_params, options, declared_params, params_nested_path)
declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params)

declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo)
end
end

def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo)
renamed_params = inheritable_setting.route[:renamed_params] || {}
if declared_param.is_a?(Hash)
declared_param.each_pair do |declared_parent_param, declared_children_params|
params_nested_path_dup = params_nested_path.dup
params_nested_path_dup << declared_parent_param.to_s
next unless options[:include_missing] || passed_params.key?(declared_parent_param)

rename_path = params_nested_path + [declared_parent_param.to_s]
renamed_param_name = renamed_params[rename_path]

memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new

memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
end
end
else
# If it is not a Hash then it does not have children.
# Find its value or set it to nil.
return unless options[:include_missing] || passed_params.try(:key?, declared_param)

rename_path = params_nested_path + [declared_param.to_s]
renamed_param_name = renamed_params[rename_path]

memo_key = optioned_param_key(renamed_param_name || declared_param, options)
passed_param = passed_params[declared_param]

params_nested_path_dup = params_nested_path.dup
params_nested_path_dup << declared_param.to_s
memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
passed_param
end
end
end

def handle_passed_param(params_nested_path, has_passed_children = false, &_block)
return yield if has_passed_children

key = params_nested_path[0]
key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1

route_options_params = options[:route_options][:params] || {}
type = route_options_params.dig(key, :type)
has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }

if type == 'Hash' && !has_children
{}
elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
[]
elsif type == 'Set' || type&.start_with?('#<Set')
Set.new
else
yield
end
end

def optioned_param_key(declared_param, options)
options[:stringify] ? declared_param.to_s : declared_param.to_sym
end

def optioned_declared_params(include_parent_namespaces)
declared_params = if include_parent_namespaces
# Declared params including parent namespaces
inheritable_setting.route[:declared_params]
else
# Declared params at current namespace
inheritable_setting.namespace_stackable[:declared_params].last || []
end

raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params

declared_params
end
end

# A filtering method that will return a hash
# consisting only of keys that have been declared by a
# `params` statement against the current/target endpoint or parent
# namespaces.
#
# @see +PostBeforeFilter#declared+
#
# @param params [Hash] The initial hash to filter. Usually this will just be `params`
# @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
# options. `:include_parent_namespaces` defaults to true, hence must be set to false if
# you want only to return params declared against the current/target endpoint.
def declared(*)
raise MethodNotYetAvailable, '#declared is not available prior to parameter validation.'
end
# Backward compatibility: alias exception class to previous location
MethodNotYetAvailable = Declared::MethodNotYetAvailable

# The API version as specified in the URL.
def version
Expand Down
6 changes: 4 additions & 2 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def initialize(new_settings, **options, &block)
@stream = nil
@body = nil
@source = block
@before_filter_passed = false
end

# Update our settings from a given set of stackable parameters. Used when
Expand Down Expand Up @@ -153,6 +154,7 @@ def run
begin
self.class.run_before_each(self)
run_filters befores, :before
@before_filter_passed = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be tracking filters than ran more generically in run_filters?

Copy link
Contributor Author

@ericproulx ericproulx Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is tracked through instrumentation but feels over engineered for 1 case

def run_filters(filters, type = :other)
  ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
    filters&.each { |filter| instance_eval(&filter) }
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still I don't like this function and I will revisit it. There's some pluralization and dynamic define_method. I think it could be simplified but I'll do it after this merge if you don't mind


if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS)
header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
Expand Down Expand Up @@ -229,8 +231,6 @@ def run_filters(filters, type = :other)
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
filters&.each { |filter| instance_eval(&filter) }
end
post_extension = DSL::InsideRoute.post_filter_methods(type)
extend post_extension if post_extension
end

%i[befores before_validations after_validations afters finallies].each do |method|
Expand All @@ -256,6 +256,8 @@ def options?

private

attr_reader :before_filter_passed

def to_routes
route_options = options[:route_options]
default_route_options = prepare_default_route_attributes(route_options)
Expand Down
12 changes: 11 additions & 1 deletion spec/grape/dsl/inside_route_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,17 @@ def header(key = nil, val = nil)
end

describe '#declared' do
# see endpoint_spec.rb#declared for spec coverage
let(:dummy_class) do
Class.new do
include Grape::DSL::Declared

attr_reader :before_filter_passed

def initialize
@before_filter_passed = false
end
end
end

it 'is not available by default' do
expect { subject.declared({}) }.to raise_error(
Expand Down
Loading