summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org>2014-06-30 16:13:32 -0400
committerDaniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org>2014-06-30 16:13:32 -0400
commitc1fad3ba4bb897dfc3a106f85602b1c00a0bfab9 (patch)
treef6250beb6975a119bb473fd7b658678d10ec63d0
parent2b2cebba7830f13525ccc74b8a7a74ab3bca6b6b (diff)
parentf09c5f68f907659a68fa759e7a7dcd225b7c2078 (diff)
downloadhashie-c1fad3ba4bb897dfc3a106f85602b1c00a0bfab9.tar.gz
Merge pull request #177 from gregory/issues_176_coercing_collection
Porposition for Issues 176 - coerce collections
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md44
-rw-r--r--lib/hashie/extensions/coercion.rb22
-rw-r--r--spec/hashie/extensions/coercion_spec.rb42
4 files changed, 103 insertions, 6 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e46b4e9..9d66c74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
## 3.1 (6/25/2014)
+* [#177](https://github.com/intridea/hashie/pull/177): Added support for coercing enumerables and collections - [@gregory](https://github.com/gregory).
* [#169](https://github.com/intridea/hashie/pull/169): Hash#to_hash will also convert nested objects that implement to_hash - [@gregory](https://github.com/gregory).
* [#171](https://github.com/intridea/hashie/pull/171): Include Trash and Dash class name when raising `NoMethodError` - [@gregory](https://github.com/gregory).
* [#172](https://github.com/intridea/hashie/pull/172): Added Dash and Trash#update_attributes! - [@gregory](https://github.com/gregory).
diff --git a/README.md b/README.md
index 5279f7b..e2d22e6 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,50 @@ class SpecialHash < Hash
end
```
+### Coercing Collections
+
+```ruby
+class Tweet < Hash
+ include Hashie::Extensions::Coercion
+ coerce_key :mentions, Array[User]
+ coerce_key :friends, Set[User]
+end
+
+user_hash = { name: "Bob" }
+mentions_hash= [user_hash, user_hash]
+friends_hash = [user_hash]
+tweet = Tweet.new(mentions: mentions_hash, friends: friends_hash)
+# => automatically calls User.coerce(user_hash) or
+# User.new(user_hash) if that isn't present on each element of the array
+
+tweet.mentions.map(&:class) # => [User, User]
+tweet.friends.class # => Set
+```
+
+### Hash attributes Coercion
+
+```ruby
+class Relation
+ def initialize(string)
+ @relation = string
+ end
+end
+
+class Tweet < Hash
+ include Hashie::Extensions::Coercion
+ coerce_key :relations, Hash[User => Relation]
+end
+
+user_hash = { name: "Bob" }
+relations_hash= { user_hash => "father", user_hash => "friend" }
+tweet = Tweet.new(relations: relations_hash)
+tweet.relations.map { |k,v| [k.class, v.class] } # => [[User, Relation], [User, Relation]]
+tweet.relations.class # => Hash
+
+# => automatically calls User.coerce(user_hash) on each key
+# and Relation.new on each value since Relation doesn't define the `coerce` class method
+```
+
### 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/lib/hashie/extensions/coercion.rb b/lib/hashie/extensions/coercion.rb
index f335481..4486ae9 100644
--- a/lib/hashie/extensions/coercion.rb
+++ b/lib/hashie/extensions/coercion.rb
@@ -10,17 +10,27 @@ module Hashie
def []=(key, value)
into = self.class.key_coercion(key) || self.class.value_coercion(value)
- if value && into
- if into.respond_to?(:coerce)
- value = into.coerce(value)
- else
- value = into.new(value)
- end
+ return super(key, value) unless value && into
+ return super(key, coerce_or_init(into).call(value)) unless into.is_a?(Enumerable)
+
+ if into.class >= Hash
+ key_coerce = coerce_or_init(into.flatten[0])
+ value_coerce = coerce_or_init(into.flatten[-1])
+ value = Hash[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
+ else # Enumerable but not Hash: Array, Set
+ value_coerce = coerce_or_init(into.first)
+ value = into.class.new(value.map { |v| value_coerce.call(v) })
end
super(key, value)
end
+ def coerce_or_init(type)
+ type.respond_to?(:coerce) ? ->(v) { type.coerce(v) } : ->(v) { type.new(v) }
+ end
+
+ private :coerce_or_init
+
def custom_writer(key, value, _convert = true)
self[key] = value
end
diff --git a/spec/hashie/extensions/coercion_spec.rb b/spec/hashie/extensions/coercion_spec.rb
index 4aab298..d73e884 100644
--- a/spec/hashie/extensions/coercion_spec.rb
+++ b/spec/hashie/extensions/coercion_spec.rb
@@ -50,6 +50,48 @@ describe Hashie::Extensions::Coercion do
expect(instance[:bar]).to be_coerced
end
+ it 'supports coercion for Array' do
+ subject.coerce_key :foo, Array[Coercable]
+
+ instance[:foo] = %w('bar', 'bar2')
+ expect(instance[:foo]).to all(be_coerced)
+ expect(instance[:foo]).to be_a(Array)
+ end
+
+ it 'supports coercion for Set' do
+ subject.coerce_key :foo, Set[Coercable]
+
+ instance[:foo] = Set.new(%w('bar', 'bar2'))
+ expect(instance[:foo]).to all(be_coerced)
+ expect(instance[:foo]).to be_a(Set)
+ end
+
+ it 'supports coercion for Set of primitive' do
+ subject.coerce_key :foo, Set[Initializable]
+
+ instance[:foo] = %w('bar', 'bar2')
+ expect(instance[:foo].map(&:value)).to all(eq 'String')
+ expect(instance[:foo]).to be_none { |v| v.coerced? }
+ expect(instance[:foo]).to be_a(Set)
+ end
+
+ it 'supports coercion for Hash' do
+ subject.coerce_key :foo, Hash[Coercable => Coercable]
+
+ instance[:foo] = { 'bar_key' => 'bar_value', 'bar2_key' => 'bar2_value' }
+ expect(instance[:foo].keys).to all(be_coerced)
+ expect(instance[:foo].values).to all(be_coerced)
+ expect(instance[:foo]).to be_a(Hash)
+ end
+
+ it 'supports coercion for Hash with primitive as value' do
+ subject.coerce_key :foo, Hash[Coercable => Initializable]
+
+ instance[:foo] = { 'bar_key' => '1', 'bar2_key' => '2' }
+ expect(instance[:foo].values.map(&:value)).to all(eq 'String')
+ expect(instance[:foo].keys).to all(be_coerced)
+ end
+
it 'calls #new if no coerce method is available' do
subject.coerce_key :foo, Initializable