Skip to content

Commit 7ce67e5

Browse files
authored
Merge pull request #2637 from ruby-grape/refactor/declared_availability
Refactor: Extract declared method into separate DSL module
2 parents ea7bbb7 + 9cfac04 commit 7ce67e5

File tree

5 files changed

+153
-144
lines changed

5 files changed

+153
-144
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [#2629](https://github.com/ruby-grape/grape/pull/2629): Refactor Router Architecture - [@ericproulx](https://github.com/ericproulx).
66
* [#2633](https://github.com/ruby-grape/grape/pull/2633): Refactor API::Instance and reorganize DSL modules - [@ericproulx](https://github.com/ericproulx).
77
* [#2636](https://github.com/ruby-grape/grape/pull/2636): Refactor router to simplify method signatures and reduce duplication - [@ericproulx](https://github.com/ericproulx).
8+
* [#2637](https://github.com/ruby-grape/grape/pull/2637): Refactor declared method - [@ericproulx](https://github.com/ericproulx).
89
* [#2639](https://github.com/ruby-grape/grape/pull/2639): Refactor mime_types_for - [@ericproulx](https://github.com/ericproulx).
910
* [#2638](https://github.com/ruby-grape/grape/pull/2638): Remove unnecessary path string duplication - [@ericproulx](https://github.com/ericproulx).
1011
* Your contribution here.

lib/grape/dsl/declared.rb

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module DSL
5+
module Declared
6+
# Denotes a situation where a DSL method has been invoked in a
7+
# filter which it should not yet be available in
8+
class MethodNotYetAvailable < StandardError
9+
def initialize(msg = '#declared is not available prior to parameter validation')
10+
super
11+
end
12+
end
13+
14+
# A filtering method that will return a hash
15+
# consisting only of keys that have been declared by a
16+
# `params` statement against the current/target endpoint or parent
17+
# namespaces.
18+
# @param params [Hash] The initial hash to filter. Usually this will just be `params`
19+
# @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
20+
# options. `:include_parent_namespaces` defaults to true, hence must be set to false if
21+
# you want only to return params declared against the current/target endpoint.
22+
def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
23+
raise MethodNotYetAvailable unless before_filter_passed
24+
25+
options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
26+
declared_params ||= optioned_declared_params(options[:include_parent_namespaces])
27+
28+
res = if passed_params.is_a?(Array)
29+
declared_array(passed_params, options, declared_params, params_nested_path)
30+
else
31+
declared_hash(passed_params, options, declared_params, params_nested_path)
32+
end
33+
34+
if (key_maps = inheritable_setting.namespace_stackable[:contract_key_map])
35+
key_maps.each { |key_map| key_map.write(passed_params, res) }
36+
end
37+
38+
res
39+
end
40+
41+
private
42+
43+
def declared_array(passed_params, options, declared_params, params_nested_path)
44+
passed_params.map do |passed_param|
45+
declared(passed_param || {}, options, declared_params, params_nested_path)
46+
end
47+
end
48+
49+
def declared_hash(passed_params, options, declared_params, params_nested_path)
50+
declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
51+
next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
52+
53+
declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo)
54+
end
55+
end
56+
57+
def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo)
58+
renamed_params = inheritable_setting.route[:renamed_params] || {}
59+
if declared_param.is_a?(Hash)
60+
declared_param.each_pair do |declared_parent_param, declared_children_params|
61+
params_nested_path_dup = params_nested_path.dup
62+
params_nested_path_dup << declared_parent_param.to_s
63+
next unless options[:include_missing] || passed_params.key?(declared_parent_param)
64+
65+
rename_path = params_nested_path + [declared_parent_param.to_s]
66+
renamed_param_name = renamed_params[rename_path]
67+
68+
memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
69+
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
70+
71+
memo[memo_key] = handle_passed_param(params_nested_path_dup, has_passed_children: passed_children_params.any?) do
72+
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
73+
end
74+
end
75+
else
76+
# If it is not a Hash then it does not have children.
77+
# Find its value or set it to nil.
78+
return unless options[:include_missing] || passed_params.try(:key?, declared_param)
79+
80+
rename_path = params_nested_path + [declared_param.to_s]
81+
renamed_param_name = renamed_params[rename_path]
82+
83+
memo_key = optioned_param_key(renamed_param_name || declared_param, options)
84+
passed_param = passed_params[declared_param]
85+
86+
params_nested_path_dup = params_nested_path.dup
87+
params_nested_path_dup << declared_param.to_s
88+
memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
89+
passed_param
90+
end
91+
end
92+
end
93+
94+
def handle_passed_param(params_nested_path, has_passed_children: false, &_block)
95+
return yield if has_passed_children
96+
97+
key = params_nested_path[0]
98+
key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
99+
100+
route_options_params = options[:route_options][:params] || {}
101+
type = route_options_params.dig(key, :type)
102+
has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103+
104+
if type == 'Hash' && !has_children
105+
{}
106+
elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
107+
[]
108+
elsif type == 'Set' || type&.start_with?('#<Set')
109+
Set.new
110+
else
111+
yield
112+
end
113+
end
114+
115+
def optioned_param_key(declared_param, options)
116+
options[:stringify] ? declared_param.to_s : declared_param.to_sym
117+
end
118+
119+
def optioned_declared_params(include_parent_namespaces)
120+
declared_params = if include_parent_namespaces
121+
# Declared params including parent namespaces
122+
inheritable_setting.route[:declared_params]
123+
else
124+
# Declared params at current namespace
125+
inheritable_setting.namespace_stackable[:declared_params].last || []
126+
end
127+
128+
raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params
129+
130+
declared_params
131+
end
132+
end
133+
end
134+
end

lib/grape/dsl/inside_route.rb

Lines changed: 3 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -3,148 +3,10 @@
33
module Grape
44
module DSL
55
module InsideRoute
6-
# Denotes a situation where a DSL method has been invoked in a
7-
# filter which it should not yet be available in
8-
class MethodNotYetAvailable < StandardError; end
6+
include Declared
97

10-
# @param type [Symbol] The type of filter for which evaluation has been
11-
# completed
12-
# @return [Module] A module containing method overrides suitable for the
13-
# position in the filter evaluation sequence denoted by +type+. This
14-
# defaults to an empty module if no overrides are defined for the given
15-
# filter +type+.
16-
def self.post_filter_methods(type)
17-
@post_filter_modules ||= { before: PostBeforeFilter }
18-
@post_filter_modules[type]
19-
end
20-
21-
# Methods which should not be available in filters until the before filter
22-
# has completed
23-
module PostBeforeFilter
24-
def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
25-
options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
26-
declared_params ||= optioned_declared_params(options[:include_parent_namespaces])
27-
28-
res = if passed_params.is_a?(Array)
29-
declared_array(passed_params, options, declared_params, params_nested_path)
30-
else
31-
declared_hash(passed_params, options, declared_params, params_nested_path)
32-
end
33-
34-
if (key_maps = inheritable_setting.namespace_stackable[:contract_key_map])
35-
key_maps.each { |key_map| key_map.write(passed_params, res) }
36-
end
37-
38-
res
39-
end
40-
41-
private
42-
43-
def declared_array(passed_params, options, declared_params, params_nested_path)
44-
passed_params.map do |passed_param|
45-
declared(passed_param || {}, options, declared_params, params_nested_path)
46-
end
47-
end
48-
49-
def declared_hash(passed_params, options, declared_params, params_nested_path)
50-
declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
51-
next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
52-
53-
declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo)
54-
end
55-
end
56-
57-
def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo)
58-
renamed_params = inheritable_setting.route[:renamed_params] || {}
59-
if declared_param.is_a?(Hash)
60-
declared_param.each_pair do |declared_parent_param, declared_children_params|
61-
params_nested_path_dup = params_nested_path.dup
62-
params_nested_path_dup << declared_parent_param.to_s
63-
next unless options[:include_missing] || passed_params.key?(declared_parent_param)
64-
65-
rename_path = params_nested_path + [declared_parent_param.to_s]
66-
renamed_param_name = renamed_params[rename_path]
67-
68-
memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
69-
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
70-
71-
memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do
72-
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
73-
end
74-
end
75-
else
76-
# If it is not a Hash then it does not have children.
77-
# Find its value or set it to nil.
78-
return unless options[:include_missing] || passed_params.try(:key?, declared_param)
79-
80-
rename_path = params_nested_path + [declared_param.to_s]
81-
renamed_param_name = renamed_params[rename_path]
82-
83-
memo_key = optioned_param_key(renamed_param_name || declared_param, options)
84-
passed_param = passed_params[declared_param]
85-
86-
params_nested_path_dup = params_nested_path.dup
87-
params_nested_path_dup << declared_param.to_s
88-
memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
89-
passed_param
90-
end
91-
end
92-
end
93-
94-
def handle_passed_param(params_nested_path, has_passed_children = false, &_block)
95-
return yield if has_passed_children
96-
97-
key = params_nested_path[0]
98-
key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
99-
100-
route_options_params = options[:route_options][:params] || {}
101-
type = route_options_params.dig(key, :type)
102-
has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103-
104-
if type == 'Hash' && !has_children
105-
{}
106-
elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
107-
[]
108-
elsif type == 'Set' || type&.start_with?('#<Set')
109-
Set.new
110-
else
111-
yield
112-
end
113-
end
114-
115-
def optioned_param_key(declared_param, options)
116-
options[:stringify] ? declared_param.to_s : declared_param.to_sym
117-
end
118-
119-
def optioned_declared_params(include_parent_namespaces)
120-
declared_params = if include_parent_namespaces
121-
# Declared params including parent namespaces
122-
inheritable_setting.route[:declared_params]
123-
else
124-
# Declared params at current namespace
125-
inheritable_setting.namespace_stackable[:declared_params].last || []
126-
end
127-
128-
raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params
129-
130-
declared_params
131-
end
132-
end
133-
134-
# A filtering method that will return a hash
135-
# consisting only of keys that have been declared by a
136-
# `params` statement against the current/target endpoint or parent
137-
# namespaces.
138-
#
139-
# @see +PostBeforeFilter#declared+
140-
#
141-
# @param params [Hash] The initial hash to filter. Usually this will just be `params`
142-
# @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
143-
# options. `:include_parent_namespaces` defaults to true, hence must be set to false if
144-
# you want only to return params declared against the current/target endpoint.
145-
def declared(*)
146-
raise MethodNotYetAvailable, '#declared is not available prior to parameter validation.'
147-
end
8+
# Backward compatibility: alias exception class to previous location
9+
MethodNotYetAvailable = Declared::MethodNotYetAvailable
14810

14911
# The API version as specified in the URL.
15012
def version

lib/grape/endpoint.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def initialize(new_settings, **options, &block)
7171
@stream = nil
7272
@body = nil
7373
@source = block
74+
@before_filter_passed = false
7475
end
7576

7677
# Update our settings from a given set of stackable parameters. Used when
@@ -153,6 +154,7 @@ def run
153154
begin
154155
self.class.run_before_each(self)
155156
run_filters befores, :before
157+
@before_filter_passed = true
156158

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

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

257257
private
258258

259+
attr_reader :before_filter_passed
260+
259261
def to_routes
260262
route_options = options[:route_options]
261263
default_route_options = prepare_default_route_attributes(route_options)

spec/grape/dsl/inside_route_spec.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,17 @@ def header(key = nil, val = nil)
404404
end
405405

406406
describe '#declared' do
407-
# see endpoint_spec.rb#declared for spec coverage
407+
let(:dummy_class) do
408+
Class.new do
409+
include Grape::DSL::Declared
410+
411+
attr_reader :before_filter_passed
412+
413+
def initialize
414+
@before_filter_passed = false
415+
end
416+
end
417+
end
408418

409419
it 'is not available by default' do
410420
expect { subject.declared({}) }.to raise_error(

0 commit comments

Comments
 (0)