summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Herold <michael.j.herold@gmail.com>2015-04-26 00:07:37 -0500
committerMichael Herold <michael.j.herold@gmail.com>2015-04-26 00:07:37 -0500
commitfa3ba9f9a6808eb56143e5c11aef52dc2c239d98 (patch)
tree26c7c50c3d37ca8b2826cebf2e7c9472646ebbaa
parentfc4b183eff6ec51270d7ec11e95536f8cf0e304a (diff)
downloadhashie-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.md35
-rw-r--r--spec/hashie/extensions/coercion_spec.rb61
2 files changed, 96 insertions, 0 deletions
diff --git a/README.md b/README.md
index b8df22e..dcc462c 100644
--- a/README.md
+++ b/README.md
@@ -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