diff options
author | Daniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org> | 2014-06-30 16:13:32 -0400 |
---|---|---|
committer | Daniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org> | 2014-06-30 16:13:32 -0400 |
commit | c1fad3ba4bb897dfc3a106f85602b1c00a0bfab9 (patch) | |
tree | f6250beb6975a119bb473fd7b658678d10ec63d0 | |
parent | 2b2cebba7830f13525ccc74b8a7a74ab3bca6b6b (diff) | |
parent | f09c5f68f907659a68fa759e7a7dcd225b7c2078 (diff) | |
download | hashie-c1fad3ba4bb897dfc3a106f85602b1c00a0bfab9.tar.gz |
Merge pull request #177 from gregory/issues_176_coercing_collection
Porposition for Issues 176 - coerce collections
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | README.md | 44 | ||||
-rw-r--r-- | lib/hashie/extensions/coercion.rb | 22 | ||||
-rw-r--r-- | spec/hashie/extensions/coercion_spec.rb | 42 |
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). @@ -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 |