diff options
author | Michael Herold <michael.j.herold@gmail.com> | 2015-04-26 00:07:37 -0500 |
---|---|---|
committer | Michael Herold <michael.j.herold@gmail.com> | 2015-04-26 00:07:37 -0500 |
commit | fa3ba9f9a6808eb56143e5c11aef52dc2c239d98 (patch) | |
tree | 26c7c50c3d37ca8b2826cebf2e7c9472646ebbaa | |
parent | fc4b183eff6ec51270d7ec11e95536f8cf0e304a (diff) | |
download | hashie-circular-coercion-docs.tar.gz |
Add documentation about circular coercioncircular-coercion-docs
The `coerce_key` method in the Coercion extension is a class-level
method. As such, using circular coercion is not possible without using
a coercion proc/lambda, since the coercion in the first type will not
have the second type defined at load time.
This can be worked around through the use of a coercion proc. I have
added docs about this to the README, as well as tests that demonstrate
the behavior.
I don't see this as a bug, since doing this raises a Ruby-level error
that makes sense.
The only way I can think of to make the non-working demonstration work
that I can think of would make the DSL a lot noisier (i.e. make all of
the "intos" use procs/lambdas to yield the class name/structure that
you'd like. Something like `coerce_key :model, to: -> { Model }`, which
then grabs that returned `Model` and is applied. I think this is a
reduction in user experience, so I chose not to pursue it.
Fixes #115
-rw-r--r-- | README.md | 35 | ||||
-rw-r--r-- | spec/hashie/extensions/coercion_spec.rb | 61 |
2 files changed, 96 insertions, 0 deletions
@@ -145,6 +145,41 @@ class Tweet < Hash end ``` +#### A note on circular coercion + +Since `coerce_key` is a class-level method, you cannot have circular coercion without the use of a proc. For example: + +```ruby +class CategoryHash < Hash + include Hashie::Extensions::Coercion + include Hashie::Extensions::MergeInitializer + + coerce_key :products, Array[ProductHash] +end + +class ProductHash < Hash + include Hashie::Extensions::Coercion + include Hashie::Extensions::MergeInitializer + + coerce_key :categories, Array[CategoriesHash] +end +``` + +This will fail with a `NameError` for `CategoryHash::ProductHash` because `ProductHash` is not defined at the point that `coerce_key` is happening for `CategoryHash`. + +To work around this, you can use a coercion proc. For example, you could do: + +```ruby +class CategoryHash < Hash + # ... + coerce_key :products, ->(value) do + return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map) + + ProductHash.new(value) + end +end +``` + ### KeyConversion The KeyConversion extension gives you the convenience methods of `symbolize_keys` and `stringify_keys` along with their bang counterparts. You can also include just stringify or just symbolize with `Hashie::Extensions::StringifyKeys` or `Hashie::Extensions::SymbolizeKeys`. diff --git a/spec/hashie/extensions/coercion_spec.rb b/spec/hashie/extensions/coercion_spec.rb index b190686..2cd5887 100644 --- a/spec/hashie/extensions/coercion_spec.rb +++ b/spec/hashie/extensions/coercion_spec.rb @@ -437,6 +437,67 @@ describe Hashie::Extensions::Coercion do expect(MyOwnBase.key_coercions).to eq({}) end end + + context 'when using circular coercion' do + context 'with a proc on one side' do + class CategoryHash < Hash + include Hashie::Extensions::Coercion + include Hashie::Extensions::MergeInitializer + + coerce_key :products, lambda { |value| + return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map) + + ProductHash.new(v) + } + end + + class ProductHash < Hash + include Hashie::Extensions::Coercion + include Hashie::Extensions::MergeInitializer + + coerce_key :categories, Array[CategoryHash] + end + + let(:category) { CategoryHash.new(type: 'rubygem', products: [Hashie::Mash.new(name: 'Hashie')]) } + let(:product) { ProductHash.new(name: 'Hashie', categories: [Hashie::Mash.new(type: 'rubygem')]) } + + it 'coerces CategoryHash[:products] correctly' do + expected = [ProductHash] + actual = category[:products].map(&:class) + + expect(actual).to eq(expected) + end + + it 'coerces ProductHash[:categories] correctly' do + expected = [CategoryHash] + actual = product[:categories].map(&:class) + + expect(actual).to eq(expected) + end + end + + context 'without a proc on either side' do + it 'fails with a NameError since the other class is not defined yet' do + attempted_code = lambda do + class AnotherCategoryHash < Hash + include Hashie::Extensions::Coercion + include Hashie::Extensions::MergeInitializer + + coerce_key :products, Array[AnotherProductHash] + end + + class AnotherProductHash < Hash + include Hashie::Extensions::Coercion + include Hashie::Extensions::MergeInitializer + + coerce_key :categories, Array[AnotherCategoryHash] + end + end + + expect { attempted_code.call }.to raise_error(NameError) + end + end + end end describe '#coerce_value' do |