Skip to content

Commit

Permalink
Deep Merge for group parameter attributes (#2432)
Browse files Browse the repository at this point in the history
  • Loading branch information
numbata authored Apr 26, 2024
1 parent 9e68e46 commit 8e2d2fb
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#### Features

* [#2432](https://github.com/ruby-grape/grape/pull/2432): Deep merge for group parameter attributes - [@numbata](https://github.com/numbata).
* [#2419](https://github.com/ruby-grape/grape/pull/2419): Add the `contract` DSL - [@dgutov](https://github.com/dgutov).
* [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia).
* [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia).
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1542,23 +1542,26 @@ Note: param in `given` should be the renamed one. In the example, it should be `

### Group Options

Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters. The example below presents a typical case when parameters share common options.
Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters.
Within these groups, individual parameters can extend or selectively override the common settings, allowing you to maintain the defaults at the group level while still applying parameter-specific rules where necessary.

The example below presents a typical case when parameters share common options.

```ruby
params do
requires :first_name, type: String, regexp: /w+/, desc: 'First name'
requires :middle_name, type: String, regexp: /w+/, desc: 'Middle name'
requires :last_name, type: String, regexp: /w+/, desc: 'Last name'
requires :first_name, type: String, regexp: /w+/, desc: 'First name', documentation: { in: 'body' }
optional :middle_name, type: String, regexp: /w+/, desc: 'Middle name', documentation: { in: 'body', x: { nullable: true } }
requires :last_name, type: String, regexp: /w+/, desc: 'Last name', documentation: { in: 'body' }
end
```

Grape allows you to present the same logic through the `with` method in your parameters block, like so:

```ruby
params do
with(type: String, regexp: /w+/) do
with(type: String, regexp: /w+/, documentation: { in: 'body' }) do
requires :first_name, desc: 'First name'
requires :middle_name, desc: 'Middle name'
optional :middle_name, desc: 'Middle name', documentation: { x: { nullable: true } }
requires :last_name, desc: 'Last name'
end
end
Expand Down
29 changes: 28 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@ Upgrading Grape

### Upgrading to >= 2.1.0

#### Deep Merging of Parameter Attributes

Grape now uses `deep_merge` to combine parameter attributes within the `with` method. Previously, attributes defined at the parameter level would override those defined at the group level.
With deep merge, attributes are now combined, allowing for more detailed and nuanced API specifications.

For example:

```ruby
with(documentation: { in: 'body' }) do
optional :vault, documentation: { default: 33 }
end
```

Before it was equivalent to:

```ruby
optional :vault, documentation: { default: 33 }
```

After it is an equivalent of:

```ruby
optional :vault, documentation: { in: 'body', default: 33 }
```

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

#### Zeitwerk

Grape's autoloader has been updated and it's now based on [Zeitwerk](https://github.com/fxn/zeitwerk).
Expand Down Expand Up @@ -179,7 +206,7 @@ If you are using Rack 3 in your application then the headers will be set to:
{ "content-type" => "application/json", "secret-password" => "foo"}
```

This means if you are checking for header values in your application, you would need to change your code to use downcased keys.
This means if you are checking for header values in your application, you would need to change your code to use downcased keys.

```ruby
get do
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def requires(*attrs, &block)

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

if opts[:using]
require_required_and_optional_fields(attrs.first, opts)
Expand All @@ -149,7 +149,7 @@ def optional(*attrs, &block)

opts = attrs.extract_options!.clone
type = opts[:type]
opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group

# check type for optional parameter group
if attrs && block
Expand Down
65 changes: 64 additions & 1 deletion spec/grape/dsl/parameters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ class Dummy
include Grape::DSL::Parameters
attr_accessor :api, :element, :parent

def initialize
@validate_attributes = []
end

def validate_attributes(*args)
@validate_attributes = *args
@validate_attributes.push(*args)
end

def validate_attributes_reader
Expand Down Expand Up @@ -106,6 +110,65 @@ 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 'merges the group attributes' do
subject.with(documentation: { in: 'body' }) { subject.optional :vault, documentation: { default: 33 } }

expect(subject.validate_attributes_reader).to eq([[:vault], { documentation: { in: 'body', default: 33 } }])
expect(subject.push_declared_params_reader).to eq([:vault])
end

it 'overrides the group attribute when values not mergable' do
subject.with(type: Integer, documentation: { in: 'body', default: 33 }) do
subject.optional :vault
subject.optional :allowed_vaults, type: [Integer], documentation: { default: [31, 32, 33], is_array: true }
end

expect(subject.validate_attributes_reader).to eq(
[
[:vault], { type: Integer, documentation: { in: 'body', default: 33 } },
[:allowed_vaults], { type: [Integer], documentation: { in: 'body', default: [31, 32, 33], is_array: true } }
]
)
end

it 'allows a primitive type attribite to overwrite a complex type group attribute' do
subject.with(documentation: { x: { nullable: true } }) do
subject.optional :vault, type: Integer, documentation: { x: nil }
end

expect(subject.validate_attributes_reader).to eq(
[
[:vault], { type: Integer, documentation: { x: nil } }
]
)
end

it 'does not nest primitives inside existing complex types erroneously' do
subject.with(type: Hash, documentation: { default: { vault: '33' } }) do
subject.optional :info
subject.optional :role, type: String, documentation: { default: 'resident' }
end

expect(subject.validate_attributes_reader).to eq(
[
[:info], { type: Hash, documentation: { default: { vault: '33' } } },
[:role], { type: String, documentation: { default: 'resident' } }
]
)
end

it 'merges deeply nested attributes' do
subject.with(documentation: { details: { in: 'body', hidden: false } }) do
subject.optional :vault, documentation: { details: { desc: 'The vault number' } }
end

expect(subject.validate_attributes_reader).to eq(
[
[:vault], { documentation: { details: { in: 'body', hidden: false, desc: 'The vault number' } } }
]
)
end
end

describe '#mutually_exclusive' do
Expand Down

0 comments on commit 8e2d2fb

Please sign in to comment.