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

Improved coercion: primitives and error handling #200

Merged
merged 12 commits into from
Aug 18, 2014

Conversation

maxlinc
Copy link
Contributor

@maxlinc maxlinc commented Aug 8, 2014

This includes #199 (coerce only when necessary) but also fixes #161 (Boolean coercion?)

Actually, it adds:

  • Integer coercion (via to_i)
  • Float coercion (via to_f)
  • String coercion (via to_s)
  • Symbol coercion (via to_sym)
  • Boolean type-checking, using the pseudo-type :boolean
  • Better coercion error messages

Boolean type checking is done via:

coerce_key :foo, :boolean

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:

  • non-zero is true, 0 is false (!i.zero?)
  • positive is true, otherwise false (i > 0)
  • any value is true, nil is false (!!i)

This PR also adds better error messages for when coercion fails. It will indicate if a type is not coercible:

subject.coerce_key :foo, TrueClass
instance[:foo] = true
#=> TypeError: Cannot coerce property :foo from TrueClass to TrueClass: TrueClass is not a coercable type

Or if a type is coercable under some circumstances, but not from that specific value:

subject.coerce_value Fixnum, Symbol
instance[:foo] = 1
#=> TypeError: Cannot coerce property :hi from Fixnum to Symbol: undefined method `to_sym' for 1:Fixnum

@maxlinc
Copy link
Contributor Author

maxlinc commented Aug 8, 2014

@dblock one question and a couple notes

Notes

I 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.

Question

Since 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 }

@dblock
Copy link
Member

dblock commented Aug 8, 2014

+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
Copy link
Member

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?

Copy link
Contributor Author

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)'

Copy link
Member

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).

@dblock
Copy link
Member

dblock commented Aug 8, 2014

This needs a CHANGELOG entry, too, please, and possibly an UPGRADING note.

@maxlinc
Copy link
Contributor Author

maxlinc commented Aug 8, 2014

Updates:

  • Added support for Complex and Rational. Works fine on Ruby 1.9.3+, if you still pseudo-support older versions might want to add require 'complex' and require 'rational' statements
  • I added support for boolean via a proc, rather than :boolean.
  • Fixed the rbx test issue. It was because of a difference with TrueClass.new. I created a "NotInitializable" class for the test instead.
  • I noticed that nil and false are never coerced, because of unless value && into. This probably makes sense, even though they're both often coercible in theory. I left it as pending for now to confirm this is true on all rubies (rspec 3 tests that pending cases fail).

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.

@dblock
Copy link
Member

dblock commented Aug 8, 2014

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.

@maxlinc
Copy link
Contributor Author

maxlinc commented Aug 14, 2014

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?

@maxlinc
Copy link
Contributor Author

maxlinc commented Aug 14, 2014

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

@dblock
Copy link
Member

dblock commented Aug 15, 2014

I think this can be merged, it needs a CHANGELOG/README/UPGRADING updates.

@dblock
Copy link
Member

dblock commented Aug 17, 2014

Bump @maxlinc, would really like this to go in.

@maxlinc
Copy link
Contributor Author

maxlinc commented Aug 18, 2014

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 [:nested_list, 3, :bar], but I don't see a way to do that with refactoring coercion.rb.

@dblock
Copy link
Member

dblock commented Aug 18, 2014

Perfect, merging as is. Thanks a lot.

dblock added a commit that referenced this pull request Aug 18, 2014
Improved coercion: primitives and error handling
@dblock dblock merged commit dbe8087 into hashie:master Aug 18, 2014
jsonn pushed a commit to jsonn/pkgsrc that referenced this pull request Feb 6, 2015
## 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).
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.

Boolean coercion?
2 participants