-
Notifications
You must be signed in to change notification settings - Fork 311
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
Improved coercion: primitives and error handling #200
Conversation
@dblock one question and a couple notes NotesI created a new PR rather than updating #199 because that PR just covers lazy coercion and has fewer open questions. This covers several coercion corner cases, but has some open questions and surprising behavior to review. So I left #199 for now, but if it looks like this will be merged #199 can be closed. This might fix #115 but I need a test case to confirm. QuestionSince there are many ways to coerce to a boolean, what if hashie accepted a proc directly? Rather than: coerce_value Integer, :boolean What about: coerce_value Integer, ->(v) { !v.zero? }
# or coerce_value Integer, ->(v) { v > 0 }
# or coerce_value Integer, ->(v) { !!v } |
+1 on the Proc idea, however don't feel too compelled to do this at the same time. There's a failure on Rubinius that doesn't look like a fluke, lets at least understand it? |
expect { instance[:hi] = 1 }.to raise_error(TypeError, /Cannot coerce property :hi from Fixnum to Symbol/) | ||
end | ||
|
||
pending 'coerces Integer to String' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be un-pendingfied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left it there to catch you attention :)
The specs for coerce_value are:
- 'coerces any value of the exact right class'
- 'does not coerce superclasses'
So this does not work:
subject.coerce_value Integer, String
But you can do this:
subject.coerce_value Fixnum, String
subject.coerce_value Bignum, String
Is that an acceptable limitation or would you prefer to handle Integer and Numeric? I suppose core types are already a special case, so maybe the spec can be 'does not coerce superclasses (except for core numeric types)'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most people are going to be using Integer and Numeric, so I think we should handle those, even explicitly. Down with what you propose with the spec (does not coerce superclasses except for numeric types).
This needs a CHANGELOG entry, too, please, and possibly an UPGRADING note. |
Updates:
FYI - Pending tests do run (and must fail) on rspec 3, so the pending tests check the behavior is not working on any platform. @dblock Can you review the changes so far and decide what the behavior should be for the pending scenarios? Once that's done I'll work on the final updates and documentation. |
This is excellent work. LMK if my comment is not enough. As always, I welcome a more opinionated thought on this from your side because you've had your hands in the coercion for the last few - if anyone is best qualified to make a call is yourself. |
Yeah, your comments were enough. I'll target the pareto principal: the 20% of coercions that will be used 80% of the time. Right now I'm probably covering 80% - but the 80% that are easiest to implement rather than most used. It would be good to test this on some real world projects that have deeply nested coercible objects before saying its ready to merge. Do you know any real-world projects that are good candidates for regression testing coercion? |
This might be done! The tests cover all the behavior I think makes sense. The error messages when coercion fails are definitely better, but it might be possible to improve them further. I won't be touching this next week because I'm on vacation, but if you can review and test this PR on some real-world projects it'd be helpful. There are significant changes here - not just coercion types that were previously not coercible, but it also now avoids unnecessary coercion (as discussed earlier), coerces false (but still not nil), and raises different errors. Here's a diff of the rspec documentation for your convenience: @@ -2,6 +2,7 @@
#coerce_key
should be respond to :coerce_key
runs through coerce on a specified key
+ skips unnecessary coercions
supports an array of keys
supports coercion for Array
supports coercion for Set
@@ -10,6 +11,31 @@
supports coercion for Hash with primitive as value
calls #new if no coerce method is available
coerces when the merge initializer is used
+ nesting
+ coeces nested objects
+ coeces nested arrays
+ coeces nested hashes
+ coercing core types
+ coerces from String to Integer via to_i
+ coerces from Integer to Integer via to_i
+ coerces from Rational to Integer via to_i
+ coerces from String to Float via to_f
+ coerces from Integer to Float via to_f
+ coerces from Rational to Float via to_f
+ coerces from String to String via to_s
+ coerces from Integer to String via to_s
+ coerces from Rational to String via to_s
+ coerces from String to String via to_s
+ coerces from Symbol to String via to_s
+ coerces from String to Symbol via to_sym
+ coerces from Symbol to Symbol via to_sym
+ can coerce String to Rational when possible
+ can coerce String to Complex when possible
+ coerces collections with core types
+ can coerce booleans via a proc
+ raises errors for non-coercable types
+ can coerce false
+ does not coerce nil
when #replace is used
coerces relevant keys
sets correct values
@@ -29,3 +55,9 @@
coerces any value of the exact right class
coerces values from a #replace call
does not coerce superclasses
+ core types
+ coerces String to Integer when possible
+ coerces non-numeric from String to Integer
+ raises a CoercionError when coercion is not possible
+ coerces Integer to String
+ coerces Numeric to String |
I think this can be merged, it needs a CHANGELOG/README/UPGRADING updates. |
Bump @maxlinc, would really like this to go in. |
Done. I'm on vacation this week, so if only minor edits are needed feel free to take what I have and refactor or rephrase whatever you want. Otherwise I'll respond to any questions next week. Also, I was hoping to get it to track the "chain" for deeper coercion errors, but it looks like something that may need to wait for a future PR. It would be something like this: expect do
instance[:nested_list] = [
{ foo: 123, bar: '456' },
{ foo: 234, bar: '567' },
{ foo: 345, bar: :xyz }
]
end.to raise_error(Hashie::CoercionError, /Cannot coerce property nested_list[3].bar/) Right now it just says "Cannot coerce property :bar" so tracking down the exact class and/or object that failed can be tricky. I wanted to push the current key and/or array index to a stack during coercion, so at the end you have |
Perfect, merging as is. Thanks a lot. |
Improved coercion: primitives and error handling
## 3.4.0 (02/02/2014) * [#271](hashie/hashie#271): Added ability to define defaults based on current hash - [@gregory](https://github.com/gregory). * [#247](hashie/hashie#247): Fixed #stringify_keys and #symbolize_keys collision with ActiveSupport - [@bartoszkopinski](https://github.com/bartoszkopinski). * [#249](hashie/hashie#249): SafeAssignment will now also protect hash-style assignments - [@jrochkind](https://github.com/jrochkind). * [#251](hashie/hashie#251): Added block support to indifferent access #fetch - [@jgraichen](https://github.com/jgraichen). * [#252](https://github.com/intridia/hashie/pull/252): Added support for conditionally required Hashie::Dash attributes - [@ccashwell](https://github.com/ccashwell). * [#256](https://github.com/intridia/hashie/pull/256): Inherit key coercions - [@Erol](https://github.com/Erol). * [#259](https://github.com/intridia/hashie/pull/259): Fixed handling of default proc values in Mash - [@Erol](https://github.com/Erol). * [#260](https://github.com/intridia/hashie/pull/260): Added block support to Extensions::DeepMerge - [@Galathius](https://github.com/galathius). * [#254](hashie/hashie#254): Added public utility methods for stringify and symbolize keys - [@maxlinc](https://github.com/maxlinc). * [#261](hashie/hashie#261): Fixed bug where Dash.property modifies argument object - [@d_tw](https://github.com/d_tw). * [#264](hashie/hashie#264): Methods such as abc? return true/false with Hashie::Extensions::MethodReader - [@Zloy](https://github.com/Zloy). * [#269](hashie/hashie#269): Add #extractable_options? so ActiveSupport Array#extract_options! can extract it - [@ridiculous](https://github.com/ridiculous). * Your contribution here. ## 3.3.2 (11/26/2014) * [#233](hashie/hashie#233): Custom error messages for required properties in Hashie::Dash subclasses - [@Joss](https://github.com/joss). * [#231](hashie/hashie#231): Added support for coercion on class type that inherit from Hash - [@gregory](https://github.com/gregory). * [#228](hashie/hashie#228): Made Hashie::Extensions::Parsers::YamlErbParser pass template filename to ERB - [@jperville](https://github.com/jperville). * [#224](hashie/hashie#224): Merging Hashie::Mash now correctly only calls the block on duplicate values - [@amysutedja](https://github.com/amysutedja). * [#221](hashie/hashie#221): Reduce amount of allocated objects on calls with suffixes in Hashie::Mash - [@kubum](https://github.com/kubum). * [#245](hashie/hashie#245): Added Hashie::Extensions::MethodAccessWithOverride to autoloads - [@Fritzinger](https://github.com/Fritzinger). ## 3.3.1 (8/26/2014) * [#183](hashie/hashie#183): Added Mash#load with YAML file support - [@gregory](https://github.com/gregory). * [#195](hashie/hashie#195): Ensure that the same object is returned after injecting IndifferentAccess - [@michaelherold](https://github.com/michaelherold). * [#201](hashie/hashie#201): Hashie::Trash transforms can be inherited - [@FoboCasteR](https://github.com/fobocaster). * [#189](hashie/hashie#189): Added Rash#fetch - [@medcat](https://github.com/medcat). * [#200](hashie/hashie#200): Improved coercion: primitives and error handling - [@maxlinc](https://github.com/maxlinc). * [#204](hashie/hashie#204): Added Hashie::Extensions::MethodOverridingWriter and Hashie::Extensions::MethodAccessWithOverride - [@michaelherold](https://github.com/michaelherold). * [#205](http://github.com/intridea/hashie/pull/205): Added Hashie::Extensions::Mash::SafeAssignment - [@michaelherold](https://github.com/michaelherold). * [#206](http://github.com/intridea/hashie/pull/206): Fixed stack overflow from repetitively including coercion in subclasses - [@michaelherold](https://github.com/michaelherold). * [#207](http://github.com/intridea/hashie/pull/207): Fixed inheritance of transformations in Trash - [@FoboCasteR](https://github.com/fobocaster). * [#209](http://github.com/intridea/hashie/pull/209): Added Hashie::Extensions::DeepFind - [@michaelherold](https://github.com/michaelherold). * [#69](hashie/hashie#69): Fixed regression in assigning multiple properties in Hashie::Trash - [@michaelherold](https://github.com/michaelherold), [@einzige](https://github.com/einzige), [@dblock](https://github.com/dblock). ## 3.2.0 (7/10/2014) * [#164](hashie/hashie#164), [#165](hashie/hashie#165), [#166](hashie/hashie#166): Fixed stack overflow when coercing mashes that contain ActiveSupport::HashWithIndifferentAccess values - [@numinit](https://github.com/numinit), [@kgrz](https://github.com/kgrz). * [#177](hashie/hashie#177): Added support for coercing enumerables and collections - [@gregory](https://github.com/gregory). * [#179](hashie/hashie#179): Mash#values_at will convert each key before doing the lookup - [@nahiluhmot](https://github.com/nahiluhmot). * [#184](hashie/hashie#184): Allow ranges on Rash to match all Numeric types - [@medcat](https://github.com/medcat). * [#187](hashie/hashie#187): Automatically require version - [@medcat](https://github.com/medcat). * [#190](hashie/hashie#190): Fixed `coerce_key` with `from` Trash feature and Coercion extension - [@gregory](https://github.com/gregory). * [#192](hashie/hashie#192): Fixed StringifyKeys#stringify_keys! to recursively stringify keys of embedded ::Hash types - [@dblock](https://github.com/dblock). ## 3.1.0 (6/25/2014) * [#169](hashie/hashie#169): Hash#to_hash will also convert nested objects that implement to_hash - [@gregory](https://github.com/gregory). * [#171](hashie/hashie#171): Include Trash and Dash class name when raising `NoMethodError` - [@gregory](https://github.com/gregory). * [#172](hashie/hashie#172): Added Dash and Trash#update_attributes! - [@gregory](https://github.com/gregory). * [#173](hashie/hashie#173): Auto include Dash::IndifferentAccess when IndiferentAccess is included in Dash - [@gregory](https://github.com/gregory). * [#174](hashie/hashie#174): Fixed `from` and `transform_with` Trash features when IndifferentAccess is included - [@gregory](https://github.com/gregory). ## 3.0.0 (6/3/2014) **Note:** This version introduces several backward incompatible API changes. See [UPGRADING](UPGRADING.md) for details. * [#150](hashie/hashie#159): Handle nil intermediate object on deep fetch - [@stephenaument](https://github.com/stephenaument). * [#146](hashie/hashie#146): Mash#respond_to? inconsistent with #method_missing and does not respond to #permitted? - [@dblock](https://github.com/dblock). * [#152](hashie/hashie#152): Do not convert keys to String in Hashie::Dash and Hashie::Trash, use Hashie::Extensions::Dash::IndifferentAccess to achieve backward compatible behavior - [@dblock](https://github.com/dblock). * [#152](hashie/hashie#152): Do not automatically stringify keys in Hashie::Hash#to_hash, pass `:stringify_keys` to achieve backward compatible behavior - [@dblock](https://github.com/dblock). * [#148](hashie/hashie#148): Consolidated Hashie::Hash#stringify_keys implementation - [@dblock](https://github.com/dblock). * [#149](hashie/hashie#149): Allow IgnoreUndeclared and DeepMerge to be used with undeclared properties - [@jhaesus](https://github.com/jhaesus).
This includes #199 (coerce only when necessary) but also fixes #161 (Boolean coercion?)
Actually, it adds:
:boolean
Boolean type checking is done via:
This will check that foo is a boolean. It would be possible to coerce foo to a boolean, but there isn't a standard convention so we'd need to make a choice. For example, integers can be converted to a boolean as:
!i.zero?
)i > 0
)!!i
)This PR also adds better error messages for when coercion fails. It will indicate if a type is not coercible:
Or if a type is coercable under some circumstances, but not from that specific value: