Skip to content
Closed
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 @@ -30,6 +30,7 @@
* [#2594](https://github.com/ruby-grape/grape/pull/2594): Fix routes memoization - [@ericproulx](https://github.com/ericproulx).
* [#2595](https://github.com/ruby-grape/grape/pull/2595): Keep `within_namespace` as part of our internal api - [@ericproulx](https://github.com/ericproulx).
* [#2596](https://github.com/ruby-grape/grape/pull/2596): Remove `namespace_reverse_stackable_with_hash` from public scope - [@ericproulx](https://github.com/ericproulx).
* [#2611](https://github.com/ruby-grape/grape/pull/2611): Reduce objection allocations from requires and optional dsl - [@nvh0412](https://github.com/nvh0412).
* Your contribution here.

### 2.4.0 (2025-06-18)
Expand Down
37 changes: 24 additions & 13 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,17 @@ def use(*names)
# end
# end
def requires(*attrs, &block)
orig_attrs = attrs.clone

opts = attrs.extract_options!.clone
# Extract options without mutating the original attrs array
attrs_clean, opts = extract_options_non_mutating(attrs)
opts = opts.clone
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)
require_required_and_optional_fields(attrs_clean.first, opts)
else
validate_attributes(attrs, opts, &block)
block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as))
validate_attributes(attrs_clean, opts, &block)
block ? new_scope(attrs, &block) : push_declared_params(attrs_clean, opts.slice(:as))
end
end

Expand All @@ -143,24 +143,23 @@ def requires(*attrs, &block)
# @param (see #requires)
# @option (see #requires)
def optional(*attrs, &block)
orig_attrs = attrs.clone

opts = attrs.extract_options!.clone
# Extract options without mutating the original attrs array
attrs_clean, opts = extract_options_non_mutating(attrs)
type = opts[:type]
opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group

# check type for optional parameter group
if attrs && block
if attrs_clean && block
raise Grape::Exceptions::MissingGroupType if type.nil?
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
end

if opts[:using]
require_optional_fields(attrs.first, opts)
require_optional_fields(attrs_clean.first, opts)
else
validate_attributes(attrs, opts, &block)
validate_attributes(attrs_clean, opts, &block)

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

Expand Down Expand Up @@ -256,6 +255,18 @@ def params(params)

private

# Extract options from an array without mutating the original array.
# This is more memory-efficient than cloning the array and then using extract_options!
# @param attrs [Array] the attributes array that may contain a hash as the last element
# @return [Array] a tuple of [clean_attrs, options_hash]
def extract_options_non_mutating(attrs)
if attrs.last.is_a?(Hash)
[attrs[0...-1], attrs.last]
else
[attrs, {}]
end
end

def first_hash_key_or_param(parameter)
parameter.is_a?(Hash) ? parameter.keys.first : parameter
end
Expand Down
20 changes: 20 additions & 0 deletions spec/grape/dsl/parameters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ def extract_message_option(attrs)
expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }])
expect(subject.push_declared_params_reader).to eq([:id])
end

it 'preserves original attrs array when calling new_scope' do
attrs = [:id, { type: Array, desc: 'Identity.' }]
original_attrs = attrs.dup

expect(subject).to receive(:new_scope).with(attrs)
subject.requires(*attrs) { [] }

expect(attrs).to eq(original_attrs)
end
end

describe '#optional' do
Expand All @@ -108,6 +118,16 @@ def extract_message_option(attrs)
expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }])
expect(subject.push_declared_params_reader).to eq([:id])
end

it 'preserves original attrs array when calling new_scope' do
attrs = [:id, { type: Array, desc: 'Identity.' }]
original_attrs = attrs.dup

expect(subject).to receive(:new_scope).with(attrs, true)
subject.optional(*attrs) { [] }

expect(attrs).to eq(original_attrs)
end
end

describe '#with' do
Expand Down