Skip to content
Merged
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 @@ -23,6 +23,7 @@
* [#2615](https://github.com/ruby-grape/grape/pull/2615): Remove manual toc and tod danger check - [@alexanderadam](https://github.com/alexanderadam).
* [#2612](https://github.com/ruby-grape/grape/pull/2612): Avoid multiple mount pollution - [@alexanderadam](https://github.com/alexanderadam).
* [#2617](https://github.com/ruby-grape/grape/pull/2617): Migrate from `ActiveSupport::Configurable` to `Dry::Configurable` - [@ericproulx](https://github.com/ericproulx).
* [#2618](https://github.com/ruby-grape/grape/pull/2618): Modernize argument delegation for Ruby 3+ compatibility - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
12 changes: 12 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ Upgrading Grape

### Upgrading to >= 3.0.0

#### Ruby 3+ Argument Delegation Modernization

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.

- All DSL methods now use explicit keyword arguments (`**kwargs`) instead of extracting options from mixed argument lists
- Method signatures are now more explicit and follow Ruby 3+ best practices
- The `active_support/core_ext/array/extract_options` dependency has been removed

This is a modernization effort that improves code quality while maintaining full backward compatibility.

See [#2618](https://github.com/ruby-grape/grape/pull/2618) for more information.

#### Configuration API Migration from ActiveSupport::Configurable to Dry::Configurable

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).
Expand Down
2 changes: 1 addition & 1 deletion lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
require 'active_support/version'
require 'active_support/isolated_execution_state'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/deep_transform_values'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/keys'
Expand Down
37 changes: 19 additions & 18 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,6 @@ class << self
# NOTE: This will only be called on an API directly mounted on RACK
def_delegators :base_instance, :new, :configuration, :call, :compile!

# When inherited, will create a list of all instances (times the API was mounted)
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
def inherited(api)
super

api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
api.override_all_methods!
end

# Initialize the instance variables on the remountable class, and the base_instance
# an instance that will be used to create the set up but will not be mounted
def initial_setup(base_instance_parent)
Expand All @@ -51,8 +42,8 @@ def initial_setup(base_instance_parent)
# Redefines all methods so that are forwarded to add_setup and be recorded
def override_all_methods!
(base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override|
define_singleton_method(method_override) do |*args, &block|
add_setup(method: method_override, args: args, block: block)
define_singleton_method(method_override) do |*args, **kwargs, &block|
add_setup(method: method_override, args: args, kwargs: kwargs, block: block)
end
end
end
Expand Down Expand Up @@ -86,6 +77,15 @@ def mount_instance(configuration: nil)

private

# When inherited, will create a list of all instances (times the API was mounted)
# It will listen to the setup required to mount that endpoint, and replicate it on any new instance
def inherited(api)
super

api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
api.override_all_methods!
end

# Replays the set up to produce an API as defined in this class, can be called
# on classes that inherit from Grape::API
def replay_setup_on(instance)
Expand All @@ -95,7 +95,7 @@ def replay_setup_on(instance)
end

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

def replay_step_on(instance, method:, args:, block:)
return if skip_immediate_run?(instance, args)
def replay_step_on(instance, method:, args:, kwargs:, block:)
return if skip_immediate_run?(instance, args, kwargs)

eval_args = evaluate_arguments(instance.configuration, *args)
response = instance.__send__(method, *eval_args, &block)
if skip_immediate_run?(instance, [response])
eval_kwargs = kwargs.deep_transform_values { |v| evaluate_arguments(instance.configuration, v).first }
response = instance.__send__(method, *eval_args, **eval_kwargs, &block)
if skip_immediate_run?(instance, [response], kwargs)
response
else
evaluate_arguments(instance.configuration, response).first
end
end

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

def any_lazy?(args)
Expand Down
24 changes: 13 additions & 11 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ def compile
@instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
end

# Wipe the compiled API so we can recompile after changes were made.
def change!
@instance = nil
end

# This is the interface point between Rack and Grape; it accepts a request
# from Rack and ultimately returns an array of three values: the status,
# the headers, and the body. See [the rack specification]
Expand Down Expand Up @@ -101,12 +96,6 @@ def recognize_path(path)

protected

def inherited(subclass)
super
subclass.reset!
subclass.logger logger.clone
end

def inherit_settings(other_settings)
top_level_setting.inherit_from other_settings.point_in_time_copy

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

reset_routes!
end

# Wipe the compiled API so we can recompile after changes were made.
def change!
@instance = nil
end

private

def inherited(subclass)
super
subclass.reset!
subclass.logger logger.clone
end
end

attr_reader :router
Expand Down
3 changes: 1 addition & 2 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,7 @@ def stream(value = nil)
# with: API::Entities::User,
# admin: current_user.admin?
# end
def present(*args)
options = args.count > 1 ? args.extract_options! : {}
def present(*args, **options)
key, object = if args.count == 2 && args.first.is_a?(Symbol)
args
else
Expand Down
17 changes: 5 additions & 12 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ def build_with(build_with)
# Collection.page(params[:page]).per(params[:per_page])
# end
# end
def use(*names)
def use(*names, **options)
named_params = @api.inheritable_setting.namespace_stackable_with_hash(:named_params) || {}
options = names.extract_options!
names.each do |name|
params_block = named_params.fetch(name) do
raise "Params :#{name} not found!"
Expand Down Expand Up @@ -123,29 +122,23 @@ def use(*names)
# requires :name, type: String
# end
# end
def requires(*attrs, &block)
orig_attrs = attrs.clone

opts = attrs.extract_options!.clone
def requires(*attrs, **opts, &block)
opts[:presence] = { value: true, message: opts[:message] }
opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group

if opts[:using]
require_required_and_optional_fields(attrs.first, opts)
else
validate_attributes(attrs, opts, &block)
block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as))
block ? new_scope(attrs, opts, &block) : push_declared_params(attrs, opts.slice(:as))
end
end

# Allow, but don't require, one or more parameters for the current
# endpoint.
# @param (see #requires)
# @option (see #requires)
def optional(*attrs, &block)
orig_attrs = attrs.clone

opts = attrs.extract_options!.clone
def optional(*attrs, **opts, &block)
type = opts[:type]
opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group

Expand All @@ -160,7 +153,7 @@ def optional(*attrs, &block)
else
validate_attributes(attrs, opts, &block)

block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, opts.slice(:as))
block ? new_scope(attrs, opts, true, &block) : push_declared_params(attrs, opts.slice(:as))
end
end

Expand Down
3 changes: 1 addition & 2 deletions lib/grape/dsl/request_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,13 @@ def default_error_status(new_status = nil)
# @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
# @param [Proc] handler Execution proc to handle the given exception as an
# alternative to passing a block.
def rescue_from(*args, &block)
def rescue_from(*args, **options, &block)
if args.last.is_a?(Proc)
handler = args.pop
elsif block
handler = block
end

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

handler ||= extract_with(options)
Expand Down
6 changes: 2 additions & 4 deletions lib/grape/dsl/routing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ module Routing
# end
# end
#
def version(*args, &block)
def version(*args, **options, &block)
if args.any?
options = args.extract_options!
options = options.reverse_merge(using: :path)
requested_versions = args.flatten.map(&:to_s)

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

Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
define_method supported_method.downcase do |*args, &block|
options = args.extract_options!
define_method supported_method.downcase do |*args, **options, &block|
paths = args.first || ['/']
route(supported_method, paths, options, &block)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/grape/error_formatter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def format_structured_message(_structured_message)
raise NotImplementedError
end

private

def inherited(klass)
super
ErrorFormatter.register(klass)
Expand Down
2 changes: 2 additions & 0 deletions lib/grape/params_builder/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ def call(_params)
raise NotImplementedError
end

private

def inherited(klass)
super
ParamsBuilder.register(klass)
Expand Down
23 changes: 13 additions & 10 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,22 +218,25 @@ def push_renamed_param(path, new_name)
end

def require_required_and_optional_fields(context, opts)
except_fields = Array.wrap(opts[:except])
using_fields = opts[:using].keys.delete_if { |f| except_fields.include?(f) }

if context == :all
optional_fields = Array.wrap(opts[:except])
required_fields = opts[:using].keys.delete_if { |f| optional_fields.include?(f) }
optional_fields = except_fields
required_fields = using_fields
else # context == :none
required_fields = Array.wrap(opts[:except])
optional_fields = opts[:using].keys.delete_if { |f| required_fields.include?(f) }
required_fields = except_fields
optional_fields = using_fields
end
required_fields.each do |field|
field_opts = opts[:using][field]
raise ArgumentError, "required field not exist: #{field}" unless field_opts

requires(field, field_opts)
requires(field, **field_opts)
end
optional_fields.each do |field|
field_opts = opts[:using][field]
optional(field, field_opts) if field_opts
optional(field, **field_opts) if field_opts
end
end

Expand All @@ -245,7 +248,7 @@ def require_optional_fields(context, opts)
end
optional_fields.each do |field|
field_opts = opts[:using][field]
optional(field, field_opts) if field_opts
optional(field, **field_opts) if field_opts
end
end

Expand All @@ -262,9 +265,9 @@ def validate_attributes(attrs, opts, &block)
# @param optional [Boolean] whether the parameter this are nested under
# is optional or not (and hence, whether this block's params will be).
# @yield parameter scope
def new_scope(attrs, optional = false, &block)
def new_scope(attrs, opts, optional = false, &block)
# if required params are grouped and no type or unsupported type is provided, raise an error
type = attrs[1] ? attrs[1][:type] : nil
type = opts[:type]
if attrs.first && !optional
raise Grape::Exceptions::MissingGroupType if type.nil?
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
Expand All @@ -273,7 +276,7 @@ def new_scope(attrs, optional = false, &block)
self.class.new(
api: @api,
element: attrs.first,
element_renamed: attrs[1][:as],
element_renamed: opts[:as],
parent: self,
optional: optional,
type: type || Array,
Expand Down
4 changes: 2 additions & 2 deletions spec/grape/dsl/parameters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def validates_reader
@validates
end

def new_scope(args, _, &block)
def new_scope(args, _opts, _, &block)
nested_scope = self.class.new
nested_scope.new_group_scope(args, &block)
nested_scope
Expand Down Expand Up @@ -71,7 +71,7 @@ def extract_message_option(attrs)
subject.api = Class.new { include Grape::DSL::Settings }.new
subject.api.inheritable_setting.namespace_stackable[:named_params] = named_params
expect(subject).to receive(:instance_exec).with(options).and_yield
subject.use :params_group, options
subject.use :params_group, **options
end

it 'raises error when non-existent named param is called' do
Expand Down
Loading