Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hi. Please don't define Hash#method_missing #44

Closed
robotdana opened this issue Nov 22, 2019 · 7 comments · Fixed by #45
Closed

Hi. Please don't define Hash#method_missing #44

robotdana opened this issue Nov 22, 2019 · 7 comments · Fixed by #45

Comments

@robotdana
Copy link
Contributor

NoMethodError is a wonderful thing to have, and subverting that by responding to literally every possible method depending on the hash keys is a potential source of issues.

Please either use [] and .fetch depending if you want it to raise, or if you must, use ruby stdlib ostruct instead of hashes, or if you really really must, use refinements, so that your Hash#method_missing doesn't affect every codebase using your gem.

Thank you

I really wasn't expecting this to happen.

> {a: 1}.a
NoMethodError
> require 'adyen-ruby-api-library'
> {a: 1}.a
1
@crrood
Copy link
Contributor

crrood commented Nov 22, 2019

Hi Dana,

Thank you for your input. Can you please elaborate on the use case for relying on Hash NoMethodError's? We've gotten good feedback on this functionality from other users, so I'd like to understand your side of the argument better before making a decision.

Best,
Colin

@robotdana
Copy link
Contributor Author

I don't expect to use NoMethodErrors, they're a way to tell me i've done something wrong.

This changes the contract of every single ruby hash in a codebase. it might respond to a method, or not, there's no way of knowing because it depends on the data, also respond_to_missing? wasn't defined so respond_to? is no use.

This affect on all hashes doesn't seem to be documented anywhere, and is unavoidable if you're using this gem for the straightforward reason of interacting with the adyen api.

If one wanted this 'all hashes to have accessors' behaviour in their codebase they should directly use a gem that adds this to hash, for example the http://github.com/hashie/hashie gem (which is extremely configurable, and won't do more than you want it to), and not as a side effect of trying to interact with a payment gateway, which I would never expect to modify anything about hashes at all.

If the good feedback you've got is just adyen api responses to have accessors for every item of data deeply then the adyen api responses should use a different class than Hash that perhaps has your method_missing (and adds respond_to_missing?) or OpenStruct or something.

--

Another potential issue with defining Hash#method_missing for this is that anything else that adds methods to hashes would win over accessing data, whether it's new ruby versions or other gems. This is probably not something desired.

--

A third reason to do it is you have to play nice with other code. you don't call super from your Hash#method_missing, instead directly raising, so any mixin that's supposed to be calling method_missing can't do its own thing.

--

As a general rule, unless a gem is explicitly and documentedly about adding methods to core ruby classes, it shouldn't add methods to core ruby classes. (including your to_camel_case, though that isn't as egregious)

@crrood
Copy link
Contributor

crrood commented Nov 22, 2019

Hi Dana,

Well said, it's a valid point that we may have overstepped our bounds by changing behavior of the core Hash class, and that such changes should be left to dedicated libraries.

Are you willing to submit a pull request to the /lib/adyen/util.rb file with one of your suggestions to change the implementation of AdyenResponse so it doesn't overwrite Hash while maintaining the dot-notation functionality?

Best,
Colin

robotdana added a commit to robotdana/adyen-ruby-api-library that referenced this issue Nov 24, 2019
WHY
===

Previously, monkey-patching Hash changes the contract of every single
ruby hash in a codebase. it might respond to a method, or not, there's
no way of knowing because it depends on the data, also
respond_to_missing? wasn't defined so respond_to? is no use.

Another potential issue with defining Hash#method_missing for this was
that anything else that adds methods to hashes would win over accessing
data, whether a new ruby versions or other gems. This was probably not
something desired.

A third reason not to do it is you have to carefully play nice with
other code. This Hash#method_missing wasn't call super, instead directly
raising, so any mixin that's supposed to be calling method_missing
can't do its own thing.

---

This affect on all hashes doesn't seem to be documented anywhere, and
was unavoidable if you were using this gem for the straightforward
reason of interacting with the adyen api.

If one wants this 'all hashes to have accessors' behaviour in their
codebase they should directly use a gem that adds this to hash,
for example the https://github.com/hashie/hashie gem (which is extremely
configurable, and won't do more than you want it to), and not as a side
effect of trying to interact with a payment gateway, which I would never
expect to modify anything about hashes at all.

As a general rule, unless a gem is explicitly and documentedly about
adding methods to core ruby classes, it shouldn't add methods to core
ruby classes.

How
===

JSON.parse conveniently lets us chose a different class for objects than
the default hash Hash, so we can simply use our own class, one that
inherits from Hash and define the method_missing there.

While i was making changes I added `ArgumentError` for when arguments
are passed unexpectedly, and `respond_to_missing?` so `respond_to?` and
`method`/`instance_method` work as expected, and super, so anyone
intentionally defining `Hash#method_missing` can still see their desired
behaviour.

There are also specs for this behaviour now. (because when i deleted the
Hash#method_missing all the specs still passed).

fixes: Adyen#44
robotdana added a commit to robotdana/adyen-ruby-api-library that referenced this issue Nov 24, 2019
WHY
===

Previously, monkey-patching Hash changes the contract of every single
ruby hash in a codebase. it might respond to a method, or not, there's
no way of knowing because it depends on the data, also
respond_to_missing? wasn't defined so respond_to? is no use.

Another potential issue with defining Hash#method_missing for this was
that anything else that adds methods to hashes would win over accessing
data, whether a new ruby versions or other gems. This was probably not
something desired.

A third reason not to do it is you have to carefully play nice with
other code. This Hash#method_missing wasn't call super, instead directly
raising, so any mixin that's supposed to be calling method_missing
can't do its own thing.

---

This affect on all hashes doesn't seem to be documented anywhere, and
was unavoidable if you were using this gem for the straightforward
reason of interacting with the adyen api.

If one wants this 'all hashes to have accessors' behaviour in their
codebase they should directly use a gem that adds this to hash,
for example the https://github.com/hashie/hashie gem (which is extremely
configurable, and won't do more than you want it to), and not as a side
effect of trying to interact with a payment gateway, which I would never
expect to modify anything about hashes at all.

As a general rule, unless a gem is explicitly and documentedly about
adding methods to core ruby classes, it shouldn't add methods to core
ruby classes.

How
===

JSON.parse conveniently lets us chose a different class for objects than
the default hash Hash, so we can simply use our own class, one that
inherits from Hash and define the method_missing there.

While i was making changes I added `ArgumentError` for when arguments
are passed unexpectedly, and `respond_to_missing?` so `respond_to?` and
`method`/`instance_method` work as expected, and super, so anyone
intentionally defining `Hash#method_missing` can still see their desired
behaviour.

There are also specs for this behaviour now. (because when i deleted the
Hash#method_missing all the specs still passed).

fixes: Adyen#44
@robotdana
Copy link
Contributor Author

done #45

robotdana added a commit to robotdana/adyen-ruby-api-library that referenced this issue Nov 24, 2019
WHY
===

Previously, monkey-patching Hash changes the contract of every single
ruby hash in a codebase. it might respond to a method, or not, there's
no way of knowing because it depends on the data, also
respond_to_missing? wasn't defined so respond_to? is no use.

Another potential issue with defining Hash#method_missing for this was
that anything else that adds methods to hashes would win over accessing
data, whether a new ruby versions or other gems. This was probably not
something desired.

A third reason not to do it is you have to carefully play nice with
other code. This Hash#method_missing wasn't call super, instead directly
raising, so any mixin that's supposed to be calling method_missing
can't do its own thing.

---

This affect on all hashes doesn't seem to be documented anywhere, and
was unavoidable if you were using this gem for the straightforward
reason of interacting with the adyen api.

If one wants this 'all hashes to have accessors' behaviour in their
codebase they should directly use a gem that adds this to hash,
for example the https://github.com/hashie/hashie gem (which is extremely
configurable, and won't do more than you want it to), and not as a side
effect of trying to interact with a payment gateway, which I would never
expect to modify anything about hashes at all.

As a general rule, unless a gem is explicitly and documentedly about
adding methods to core ruby classes, it shouldn't add methods to core
ruby classes.

How
===

JSON.parse conveniently lets us chose a different class for objects than
the default hash Hash, so we can simply use our own class, one that
inherits from Hash and define the method_missing there.

While i was making changes I added `ArgumentError` for when arguments
are passed unexpectedly, and `respond_to_missing?` so `respond_to?` and
`method`/`instance_method` work as expected, and super, so anyone
intentionally defining `Hash#method_missing` can still see their desired
behaviour.

There are also specs for this behaviour now. (because when i deleted the
Hash#method_missing all the specs still passed).

fixes: Adyen#44
@crrood
Copy link
Contributor

crrood commented Nov 25, 2019

HI Dana,

Thank you for this, it's a very thorough PR - I see you removed the to_camel_case override as well.

After a first read-through I don't see any issues, but given its size I'd like to sync with the team internally before merging.

I'll keep you posted.

Best,
Colin

@crrood
Copy link
Contributor

crrood commented Dec 4, 2019

Hi Dana,

Just wanted to let you know we haven't forgotten about this, but are still in the process of reviewing. We'll keep you posted.

Best,
Colin

@crrood
Copy link
Contributor

crrood commented Dec 6, 2019

Hi Dana,

Thanks for your patience - we've passed this around the team and all agree it looks great.

I'll merge and release now.

Best,
Colin

@crrood crrood closed this as completed Dec 6, 2019
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 a pull request may close this issue.

2 participants