Skip to content

Commit a28218b

Browse files
authored
Merge pull request #2618 from ruby-grape/ruby3_handling_argument_delegation
Modernize argument delegation for Ruby 3+ compatibility
2 parents 9060dd0 + ad0694b commit a28218b

File tree

17 files changed

+98
-86
lines changed

17 files changed

+98
-86
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* [#2615](https://github.com/ruby-grape/grape/pull/2615): Remove manual toc and tod danger check - [@alexanderadam](https://github.com/alexanderadam).
2424
* [#2612](https://github.com/ruby-grape/grape/pull/2612): Avoid multiple mount pollution - [@alexanderadam](https://github.com/alexanderadam).
2525
* [#2617](https://github.com/ruby-grape/grape/pull/2617): Migrate from `ActiveSupport::Configurable` to `Dry::Configurable` - [@ericproulx](https://github.com/ericproulx).
26+
* [#2618](https://github.com/ruby-grape/grape/pull/2618): Modernize argument delegation for Ruby 3+ compatibility - [@ericproulx](https://github.com/ericproulx).
2627
* Your contribution here.
2728

2829
#### Fixes

UPGRADING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ Upgrading Grape
33

44
### Upgrading to >= 3.0.0
55

6+
#### Ruby 3+ Argument Delegation Modernization
7+
8+
Grape has been modernized to use Ruby 3+'s preferred argument delegation patterns. This change replaces `args.extract_options!` with explicit `**kwargs` parameters throughout the codebase.
9+
10+
- All DSL methods now use explicit keyword arguments (`**kwargs`) instead of extracting options from mixed argument lists
11+
- Method signatures are now more explicit and follow Ruby 3+ best practices
12+
- The `active_support/core_ext/array/extract_options` dependency has been removed
13+
14+
This is a modernization effort that improves code quality while maintaining full backward compatibility.
15+
16+
See [#2618](https://github.com/ruby-grape/grape/pull/2618) for more information.
17+
618
#### Configuration API Migration from ActiveSupport::Configurable to Dry::Configurable
719

820
Grape has migrated from `ActiveSupport::Configurable` to `Dry::Configurable` for its configuration system since its [deprecated](https://github.com/rails/rails/blob/1cdd190a25e483b65f1f25bbd0f13a25d696b461/activesupport/lib/active_support/configurable.rb#L3-L7).

lib/grape.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
require 'active_support/version'
77
require 'active_support/isolated_execution_state'
88
require 'active_support/core_ext/array/conversions'
9-
require 'active_support/core_ext/array/extract_options'
109
require 'active_support/core_ext/array/wrap'
1110
require 'active_support/core_ext/enumerable'
1211
require 'active_support/core_ext/hash/conversions'
1312
require 'active_support/core_ext/hash/deep_merge'
13+
require 'active_support/core_ext/hash/deep_transform_values'
1414
require 'active_support/core_ext/hash/except'
1515
require 'active_support/core_ext/hash/indifferent_access'
1616
require 'active_support/core_ext/hash/keys'

lib/grape/api.rb

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,6 @@ class << self
3030
# NOTE: This will only be called on an API directly mounted on RACK
3131
def_delegators :base_instance, :new, :configuration, :call, :compile!
3232

33-
# When inherited, will create a list of all instances (times the API was mounted)
34-
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
35-
def inherited(api)
36-
super
37-
38-
api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
39-
api.override_all_methods!
40-
end
41-
4233
# Initialize the instance variables on the remountable class, and the base_instance
4334
# an instance that will be used to create the set up but will not be mounted
4435
def initial_setup(base_instance_parent)
@@ -51,8 +42,8 @@ def initial_setup(base_instance_parent)
5142
# Redefines all methods so that are forwarded to add_setup and be recorded
5243
def override_all_methods!
5344
(base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override|
54-
define_singleton_method(method_override) do |*args, &block|
55-
add_setup(method: method_override, args: args, block: block)
45+
define_singleton_method(method_override) do |*args, **kwargs, &block|
46+
add_setup(method: method_override, args: args, kwargs: kwargs, block: block)
5647
end
5748
end
5849
end
@@ -86,6 +77,15 @@ def mount_instance(configuration: nil)
8677

8778
private
8879

80+
# When inherited, will create a list of all instances (times the API was mounted)
81+
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
82+
def inherited(api)
83+
super
84+
85+
api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
86+
api.override_all_methods!
87+
end
88+
8989
# Replays the set up to produce an API as defined in this class, can be called
9090
# on classes that inherit from Grape::API
9191
def replay_setup_on(instance)
@@ -95,7 +95,7 @@ def replay_setup_on(instance)
9595
end
9696

9797
# Adds a new stage to the set up require to get a Grape::API up and running
98-
def add_setup(step)
98+
def add_setup(**step)
9999
@setup << step
100100
last_response = nil
101101
@instances.each do |instance|
@@ -119,22 +119,23 @@ def refresh_mount_step
119119
end
120120
end
121121

122-
def replay_step_on(instance, method:, args:, block:)
123-
return if skip_immediate_run?(instance, args)
122+
def replay_step_on(instance, method:, args:, kwargs:, block:)
123+
return if skip_immediate_run?(instance, args, kwargs)
124124

125125
eval_args = evaluate_arguments(instance.configuration, *args)
126-
response = instance.__send__(method, *eval_args, &block)
127-
if skip_immediate_run?(instance, [response])
126+
eval_kwargs = kwargs.deep_transform_values { |v| evaluate_arguments(instance.configuration, v).first }
127+
response = instance.__send__(method, *eval_args, **eval_kwargs, &block)
128+
if skip_immediate_run?(instance, [response], kwargs)
128129
response
129130
else
130131
evaluate_arguments(instance.configuration, response).first
131132
end
132133
end
133134

134135
# Skips steps that contain arguments to be lazily executed (on re-mount time)
135-
def skip_immediate_run?(instance, args)
136+
def skip_immediate_run?(instance, args, kwargs)
136137
instance.base_instance? &&
137-
(any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) })
138+
(any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) } || any_lazy?(kwargs.values))
138139
end
139140

140141
def any_lazy?(args)

lib/grape/api/instance.rb

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,6 @@ def compile
6161
@instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
6262
end
6363

64-
# Wipe the compiled API so we can recompile after changes were made.
65-
def change!
66-
@instance = nil
67-
end
68-
6964
# This is the interface point between Rack and Grape; it accepts a request
7065
# from Rack and ultimately returns an array of three values: the status,
7166
# the headers, and the body. See [the rack specification]
@@ -101,12 +96,6 @@ def recognize_path(path)
10196

10297
protected
10398

104-
def inherited(subclass)
105-
super
106-
subclass.reset!
107-
subclass.logger logger.clone
108-
end
109-
11099
def inherit_settings(other_settings)
111100
top_level_setting.inherit_from other_settings.point_in_time_copy
112101

@@ -119,6 +108,19 @@ def inherit_settings(other_settings)
119108

120109
reset_routes!
121110
end
111+
112+
# Wipe the compiled API so we can recompile after changes were made.
113+
def change!
114+
@instance = nil
115+
end
116+
117+
private
118+
119+
def inherited(subclass)
120+
super
121+
subclass.reset!
122+
subclass.logger logger.clone
123+
end
122124
end
123125

124126
attr_reader :router

lib/grape/dsl/inside_route.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,7 @@ def stream(value = nil)
337337
# with: API::Entities::User,
338338
# admin: current_user.admin?
339339
# end
340-
def present(*args)
341-
options = args.count > 1 ? args.extract_options! : {}
340+
def present(*args, **options)
342341
key, object = if args.count == 2 && args.first.is_a?(Symbol)
343342
args
344343
else

lib/grape/dsl/parameters.rb

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,8 @@ def build_with(build_with)
5353
# Collection.page(params[:page]).per(params[:per_page])
5454
# end
5555
# end
56-
def use(*names)
56+
def use(*names, **options)
5757
named_params = @api.inheritable_setting.namespace_stackable_with_hash(:named_params) || {}
58-
options = names.extract_options!
5958
names.each do |name|
6059
params_block = named_params.fetch(name) do
6160
raise "Params :#{name} not found!"
@@ -123,29 +122,23 @@ def use(*names)
123122
# requires :name, type: String
124123
# end
125124
# end
126-
def requires(*attrs, &block)
127-
orig_attrs = attrs.clone
128-
129-
opts = attrs.extract_options!.clone
125+
def requires(*attrs, **opts, &block)
130126
opts[:presence] = { value: true, message: opts[:message] }
131127
opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
132128

133129
if opts[:using]
134130
require_required_and_optional_fields(attrs.first, opts)
135131
else
136132
validate_attributes(attrs, opts, &block)
137-
block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as))
133+
block ? new_scope(attrs, opts, &block) : push_declared_params(attrs, opts.slice(:as))
138134
end
139135
end
140136

141137
# Allow, but don't require, one or more parameters for the current
142138
# endpoint.
143139
# @param (see #requires)
144140
# @option (see #requires)
145-
def optional(*attrs, &block)
146-
orig_attrs = attrs.clone
147-
148-
opts = attrs.extract_options!.clone
141+
def optional(*attrs, **opts, &block)
149142
type = opts[:type]
150143
opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
151144

@@ -160,7 +153,7 @@ def optional(*attrs, &block)
160153
else
161154
validate_attributes(attrs, opts, &block)
162155

163-
block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, opts.slice(:as))
156+
block ? new_scope(attrs, opts, true, &block) : push_declared_params(attrs, opts.slice(:as))
164157
end
165158
end
166159

lib/grape/dsl/request_response.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,13 @@ def default_error_status(new_status = nil)
9393
# @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
9494
# @param [Proc] handler Execution proc to handle the given exception as an
9595
# alternative to passing a block.
96-
def rescue_from(*args, &block)
96+
def rescue_from(*args, **options, &block)
9797
if args.last.is_a?(Proc)
9898
handler = args.pop
9999
elsif block
100100
handler = block
101101
end
102102

103-
options = args.extract_options!
104103
raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with)
105104

106105
handler ||= extract_with(options)

lib/grape/dsl/routing.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ module Routing
2222
# end
2323
# end
2424
#
25-
def version(*args, &block)
25+
def version(*args, **options, &block)
2626
if args.any?
27-
options = args.extract_options!
2827
options = options.reverse_merge(using: :path)
2928
requested_versions = args.flatten.map(&:to_s)
3029

@@ -165,8 +164,7 @@ def route(methods, paths = ['/'], route_options = {}, &block)
165164
end
166165

167166
Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
168-
define_method supported_method.downcase do |*args, &block|
169-
options = args.extract_options!
167+
define_method supported_method.downcase do |*args, **options, &block|
170168
paths = args.first || ['/']
171169
route(supported_method, paths, options, &block)
172170
end

lib/grape/error_formatter/base.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def format_structured_message(_structured_message)
5858
raise NotImplementedError
5959
end
6060

61+
private
62+
6163
def inherited(klass)
6264
super
6365
ErrorFormatter.register(klass)

0 commit comments

Comments
 (0)