Skip to content

Fix coerce_nil when Array of Type#2040

Merged
dblock merged 2 commits into
ruby-grape:masterfrom
ericproulx:fix_coerce_nil_array
May 3, 2020
Merged

Fix coerce_nil when Array of Type#2040
dblock merged 2 commits into
ruby-grape:masterfrom
ericproulx:fix_coerce_nil_array

Conversation

@ericproulx

@ericproulx ericproulx commented Apr 16, 2020

Copy link
Copy Markdown
Contributor

Since #2019, I found a regression (our tests suite fails with 1.3.2) when params type is an Array of Type and the api is called with a nil value. It should return the default value which is [].

@ericproulx ericproulx force-pushed the fix_coerce_nil_array branch from 433374b to 44e0498 Compare April 16, 2020 23:16
@ericproulx

Copy link
Copy Markdown
Contributor Author

Should it equals NilClass ?

# define default values for structures, the dry-types lib which is used
# for coercion doesn't accept nil as a value, so it would fail
return [] if type == Array
return [] if type == Array || type.is_a?(Array)

@dblock dblock Apr 17, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you love ruby?

2.6.6 :001 > Array.is_a?(Array)
 => false 

This obviously makes sense because a single type is not an array of types, but still funny.

params[:arry]
end

get '/array', arry: nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a default value anywhere here. You're passing a nil, and the API expects an array of integers. Feels like it should fail?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add

default: []

and it still fails on master. I found that since arry is an actual params, the default validator doesn't do anything. Actually it returns immediatly here

Anyway, the default is kind of tricky for structures.
Array, Set and Hash will return a default value even if not explicitly written in the param declaration

So these specs are working

 [[Array, 'Array'], [Set, 'Set'], [Hash, 'ActiveSupport::HashWithIndifferentAccess']].each do |type, expected_class|
          context 'structures default value' do
            it 'Empty array' do
              subject.params do
                requires :arry, type: type
              end
              subject.get '/array' do
                params[:arry].class
              end

              get '/array', arry: nil
              expect(last_response.status).to eq(200)
              expect(last_response.body).to eq(expected_class)
            end
          end
        end

Therefore, I was expecting that an array of types with nil like Array[Integer] as param value would behave like a plain Array.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, please do add those specs and let's make sure to document this behavior?

@dblock

dblock commented Apr 17, 2020

Copy link
Copy Markdown
Member

ruboocop -a ; rubocop --auto-gen-config for build failures

@ericproulx ericproulx force-pushed the fix_coerce_nil_array branch from 44e0498 to aba1f3c Compare April 17, 2020 17:19
expect(last_response.body).to eq('String')
end

it 'respects nil values' do

@dblock dblock Apr 17, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer working? Aren't we breaking something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this spec is actually the opposite and yes it's breaking #2019. But #2019 is why we have this conversation :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be flip-flopping on what we want, let's step back. For this example alone, when I say :a can be either an array of File or a String, and I pass nil, don't I want a nil and not []?

@stanhu stanhu Apr 17, 2020

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this example alone, when I say :a can be either an array of File or a String, and I pass nil, don't I want a nil and not []?

That was the behavior of Grape prior to v1.3.0. v1.3.x broke our tests with that, which is why I filed #2018.

Comment thread README.md Outdated
end
```

### Structures Implicit Default Value

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this it feels like it's wrong. Yes, it works this way. But if I send a nil, why am I getting a non-nil?

@dblock

dblock commented Apr 17, 2020

Copy link
Copy Markdown
Member

@stanhu @ericproulx

  1. I understand old behavior of Grape was to coerce nil into default array values.
  2. We accidentally fixed it for some scenarios in 1.3.x.
  3. We decided it was a regression, and fixed it again, but not completely.
  4. We are trying to fix it for real in this PR.
  5. I am regretting (1), don't think this was Intentional.

Here's what I think we want:

  1. nil values become nil values for all types, including arrays, sets and hashes
  2. nil values can become default values when default: is specified, users who relied on this behavior will have to follow UPGRADING
  3. We have specs for all the combinations so we are sure not to break this in the future

My question is, why would we not want to do what I am suggesting?

@stanhu

stanhu commented Apr 18, 2020

Copy link
Copy Markdown
Contributor

@dblock That sounds like the right approach to me. I don't see any issues with that at the moment.

@ericproulx

ericproulx commented Apr 18, 2020

Copy link
Copy Markdown
Contributor Author

@dblock sound good to me too. For the default part (2), will need to adjust the default validator because nil doesn't apply with default. It's only applied when the param is missing.
For instance, this spec is failing

context 'integer default value when nil' do
        it 'respects the default value' do
          subject.params do
            optional :int, type: Integer, default: 1
          end
          subject.get '/default_value' do
            params[:int].class
          end

          get '/default_value', int: nil
          expect(last_response.status).to eq(200)
          expect(last_response.body).to eq("Integer")
        end
      end

but this one works

context 'integer default value when nil' do
        it 'respects the default value' do
          subject.params do
            optional :int, type: Integer, default: 1
          end
          subject.get '/default_value' do
            params[:int].class
          end

          get '/default_value'
          expect(last_response.status).to eq(200)
          expect(last_response.body).to eq("Integer")
        end
      end

It's just a matter the presence or not of the param.

So should we adjust the default validator behavior ?

@dblock

dblock commented Apr 18, 2020

Copy link
Copy Markdown
Member

So should we adjust the default validator behavior ?

Yes, I believe so. A nil value is a nil value, not the absence of a parameter.

@ericproulx

Copy link
Copy Markdown
Contributor Author

@dblock @stanhu I've just pushed new commits. Could you validate the behavior ? Ill add the upgrading part after

@dblock dblock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good! Looks like we're not breaking any unrelated existing behavior, and I feel good about the thoroughness of tests.

Comment thread spec/grape/validations/validators/default_spec.rb
Comment thread spec/grape/validations/validators/default_spec.rb
Comment thread spec/grape/validations/validators/default_spec.rb
Comment thread spec/grape/validations/validators/regexp_spec.rb Outdated
@ericproulx ericproulx force-pushed the fix_coerce_nil_array branch from 9b03676 to be04f92 Compare April 20, 2020 11:38
@dblock

dblock commented Apr 20, 2020

Copy link
Copy Markdown
Member

LGTM, needs README and UPGRADING and I think it's good to go. Thank you!

Would appreciate another pair of eyes, maybe from @dnesteryuk, @stanhu.

@stanhu

stanhu commented Apr 20, 2020

Copy link
Copy Markdown
Contributor

Thanks, this is looking good to me too. I ran this branch against our test suite. As expected, I found a few tests that failed because we had Integer and Boolean types with default values, but the tests were passing in nil values expecting the requests to be rejected. I'm not actually sure you pass in a nil value via an HTTP request, so I think the behavior of making them use the default values makes sense. Updating the CHANGELOG, README, and UPGRADING.md is going to be important here. :)

@dblock

dblock commented Apr 21, 2020

Copy link
Copy Markdown
Member

Let's also bump version to 1.4.

@ericproulx

ericproulx commented Apr 21, 2020

Copy link
Copy Markdown
Contributor Author

@dblock I found a real simple case that doesn't work which is Array[Integer] :(

Dry::Types::CoercionError: nil cannot be coerced to array

@ericproulx

Copy link
Copy Markdown
Contributor Author

We'll have to discuss about the current spec failures @dblock

# should actually fail with: children[parents][name] is missing.
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('children[1][parents] is missing')
expect(last_response.body).to eq('children[1][parents] is missing, children[0][parents][1][name] is missing, children[0][parents][1][name] is empty')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, it is something I would like to avoid. If a parent node is missing, why do we need to check children? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put it in "nice to have". Generally, you could see it both ways. If the info is not there, you see an error, then you add a parent, then it tells you that you're still missing children. Get frustrated, repeat.

Comment thread spec/grape/validations/validators/default_spec.rb Outdated
@dnesteryuk

Copy link
Copy Markdown
Member

In general, the idea and PR look right to me, there is only one concern about validating children in a structure when a parent node is missing.

expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing')

But, I know that it is super hard to fix, validations are flat, so validation failure for parents doesn't interrupt validation for children.

We might leave it as it is in this PR.

@ericproulx

Copy link
Copy Markdown
Contributor Author

What about this test that returns 200 instead of 400 ?

  params do
    requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' }
  end
  get 'regexp_with_array' do
  end

  it 'refuses nil instead of array' do
    get '/regexp_with_array', names: nil
    expect(last_response.status).to eq(400)
    expect(last_response.body).to eq('{"error":"names can\'t be nil"}')
  end

There are 3 similar specs that returns 200 now instead of 400

1) Grape::Validations::RegexpValidator custom validation message regexp with array refuses nil instead of array
     Failure/Error: expect(last_response.status).to eq(400)
     
       expected: 400
            got: 200
     
       (compared using ==)
     # ./spec/grape/validations/validators/regexp_spec.rb:103:in `block (4 levels) in <top (required)>'
  2) Grape::Validations::RegexpValidator regexp with array rejects nil instead of array
     Failure/Error: expect(last_response.status).to eq(400)
     
       expected: 400
            got: 200
     
       (compared using ==)
     # ./spec/grape/validations/validators/regexp_spec.rb:159:in `block (3 levels) in <top (required)>'
  3) Grape::Validations::ValuesValidator nil value for a parameter rejects for an optional param with a list of values
     Failure/Error: expect(last_response.status).to eq 400
     
       expected: 400
            got: 200
     
       (compared using ==)
     # ./spec/grape/validations/validators/values_spec.rb:324:in `block (3 levels) in <top (required)>'

@dblock

dblock commented Apr 22, 2020

Copy link
Copy Markdown
Member

What about this test that returns 200 instead of 400 ?

In these specs we are saying that the value has to be an Array[String], where each string matches /^[a-z]+$/. Feels like the expectation is correct, by passing a nil value for the Array, Grape should reject it because there's no default value?

Add CHANGELOG.md

Add documentation in README.md
Add specs for future proof

return nil instead of InvalidValue

Remove Structures Implicit Default Value section

Not returning when param's value is nil

Remove default value for structures

Add specs for nil values and default values

Add non-empty value spec when params is nil

Typo change

Rubocop

Array[Integer] case doesn't work with default

rejects

Fix Array[Integer]

Fix specs where nil is now a valid value

Replaced non-empty-json by non-empty-string
@ericproulx ericproulx force-pushed the fix_coerce_nil_array branch from 72541d9 to 3bcdc29 Compare April 23, 2020 11:27
@ericproulx

Copy link
Copy Markdown
Contributor Author

So a fixed the remaining specs based on your last comments. I guess we're up to CHANGELOG, README, and UPGRADING.md :)

@dblock

dblock commented Apr 23, 2020

Copy link
Copy Markdown
Member

Awesome work @ericproulx. Yes please on README/UPGRADING.

@ZachWileman

Copy link
Copy Markdown

By chance, do you guys have any time estimate on when a new Grape version will be released with this fix in it? It looks like the change is mostly done besides a couple doc updates.

@ericproulx

ericproulx commented Apr 24, 2020 via email

Copy link
Copy Markdown
Contributor Author

@ericproulx

Copy link
Copy Markdown
Contributor Author

I’ll work the doc this weekend

On Apr 24, 2020, at 22:27, Zachary Wileman @.***> wrote: By chance, do you guys have any time estimate on when a new Grape version will be released with this fix in it? It looks like the change is mostly done besides a couple doc updates. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#2040 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACAHJI543DOQATCT5AIHX4DROHY4PANCNFSM4MKJFCAA.

Unfortunately, I didnt do it. If someone wants to write the DOC that would be great. I'm very busy at work right now probably like you.

@dblock dblock merged commit 01ca08c into ruby-grape:master May 3, 2020
@dblock

dblock commented May 3, 2020

Copy link
Copy Markdown
Member

This is a tremendous job @ericproulx. Thank you.

@ericproulx

ericproulx commented May 3, 2020 via email

Copy link
Copy Markdown
Contributor Author

@ZachWileman

Copy link
Copy Markdown

Does anyone know when a new version of Grape is going to be released that includes this fix?

@magni- magni- mentioned this pull request May 21, 2020
@ghiculescu

Copy link
Copy Markdown
Contributor

@ericproulx just FYI this is causing us a bit of grief - not sure if it's an error you've seen too #1717 (comment)

@ericproulx

ericproulx commented May 26, 2020

Copy link
Copy Markdown
Contributor Author

@ericproulx just FYI this is causing us a bit of grief - not sure if it's an error you've seen too #1717 (comment)

Sorry about that, I'll investigate. First thought : default is applied when param is nil or when param is missing. So missing and nil are equivalent but should it ? @dblock

@ghiculescu

Copy link
Copy Markdown
Contributor

That sounds right. https://github.com/ruby-grape/grape#parameter-validation-and-coercion doesn't specifically clarify this I assumed default is only used if the value is provided, but nil.

@ghiculescu

Copy link
Copy Markdown
Contributor

Hmmm, actually, this implies the behaviour I'm seeing, since mutually_exclusive is also a validation:

image

If we decide to keep this behaviour, I'll make a PR to make the docs more explicit.

@dblock

dblock commented May 27, 2020

Copy link
Copy Markdown
Member

This is the correct behavior. I particularly find the mutually_exclusive example in #1717 (comment) interesting as a confirmation that this is the correct design with predictable, future-proof results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants