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

Porposition for Issues 176 - coerce collections #177

Merged
merged 1 commit into from Jun 30, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -1,5 +1,6 @@
## Next Release

* [#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).
Expand Down
44 changes: 44 additions & 0 deletions README.md
Expand Up @@ -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`.
Expand Down
22 changes: 16 additions & 6 deletions lib/hashie/extensions/coercion.rb
Expand Up @@ -10,17 +10,27 @@ module InstanceMethods
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
Expand Down
42 changes: 42 additions & 0 deletions spec/hashie/extensions/coercion_spec.rb
Expand Up @@ -50,6 +50,48 @@ class ExampleCoercableHash < Hash
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

Expand Down