Skip to content

Commit

Permalink
Add Hashie::Extensions::MethodOverridingWriter
Browse files Browse the repository at this point in the history
This is part 2 of 3 of the to-do list determined in hashie#198.
  • Loading branch information
michaelherold committed Aug 20, 2014
1 parent 1c4fee0 commit 4bf710c
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
* [#197](https://github.com/intridea/hashie/pull/197): Dont convert keys to string on initalization of mash - [@gregory](https://github.com/gregory).
* [#201](https://github.com/intridea/hashie/pull/201): Hashie::Trash transforms can be inherited - [@fobocaster](https://github.com/fobocaster).
* [#189](https://github.com/intridea/hashie/pull/189): Added Rash#fetch - [@medcat](https://github.com/medcat).
* [#204](https://github.com/intridea/hashie/pull/204): Added Hashie::Extensions::MethodOverridingWriter and Hashie::Extensions::MethodAccessWithOverride - [@michaelherold](https://github.com/michaelherold).
* Your contribution here.

## 3.2.0 (7/10/2014)
Expand Down
23 changes: 23 additions & 0 deletions README.md
Expand Up @@ -119,6 +119,29 @@ h.abc # => 'def'
h.abc? # => true
```

### MethodAccessWithOverride

The MethodAccessWithOverride extension is like the MethodAccess extension, except that it allows you to override Hash methods. It aliases any overridden method with two leading underscores. To include only this overriding functionality, you can include the single module `Hashie::Extensions::MethodOverridingWriter`.

```ruby
class MyHash < Hash
include Hashie::Extensions::MethodAccess
end

class MyOverridingHash < Hash
include Hashie::Extensions::MethodAccessWithOverride
end

non_overriding = MyHash.new
non_overriding.zip = 'a-dee-doo-dah'
non_overriding.zip #=> [[['zip', 'a-dee-doo-dah']]]

overriding = MyHash.new
overriding.zip = 'a-dee-doo-dah'
overriding.zip #=> 'a-dee-doo-dah'
overriding.__zip #=> [[['zip', 'a-dee-doo-dah']]]
```

### IndifferentAccess

This extension can be mixed in to instantly give you indifferent access to your Hash subclass. This works just like the params hash in Rails and other frameworks where whether you provide symbols or strings to access keys, you will get the same results.
Expand Down
75 changes: 75 additions & 0 deletions lib/hashie/extensions/method_access.rb
Expand Up @@ -120,5 +120,80 @@ def self.included(base)
end
end
end

# MethodOverridingWriter gives you #key_name= shortcuts for
# writing to your hash. It allows methods to be overridden by
# #key_name= shortcuts and aliases those methods with two
# leading underscores.
#
# Keys are written as strings. Override #convert_key if you
# would like to have symbols or something else.
#
# Note that MethodOverridingWriter also overrides
# #respond_to_missing? such that any #method_name= will respond
# appropriately as true.
#
# @example
# class MyHash < Hash
# include Hashie::Extensions::MethodOverridingWriter
# end
#
# h = MyHash.new
# h.awesome = 'sauce'
# h['awesome'] # => 'sauce'
# h.zip = 'a-dee-doo-dah'
# h.zip # => 'a-dee-doo-dah'
# h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
#
module MethodOverridingWriter
def convert_key(key)
key.to_s
end

def method_missing(name, *args)
if args.size == 1 && name.to_s =~ /(.*)=$/
key = Regexp.last_match[1]
redefine_method(key) if method?(key) && !already_overridden?(key)
return self[convert_key(key)] = args.first
end

super
end

def respond_to_missing?(name, include_private = false)
return true if name.to_s.end_with?('=')
super
end

protected

def already_overridden?(name)
method?("__#{name}")
end

def method?(name)
methods.map { |m| m.to_s }.include?(name)
end

def redefine_method(method_name)
eigenclass = class << self; self; end
eigenclass.__send__(:alias_method, "__#{method_name}", method_name)
eigenclass.__send__(:define_method, method_name, -> { self[method_name] })
end
end

# A macro module that will automatically include MethodReader,
# MethodOverridingWriter, and MethodQuery, giving you the ability
# to read, write, and query keys in a hash using method call
# shortcuts that can override object methods. Any overridden
# object method is automatically aliased with two leading
# underscores.
module MethodAccessWithOverride
def self.included(base)
[MethodReader, MethodOverridingWriter, MethodQuery].each do |mod|
base.send :include, mod
end
end
end
end
end
55 changes: 55 additions & 0 deletions spec/hashie/extensions/method_access_spec.rb
Expand Up @@ -119,3 +119,58 @@ def initialize(hash = {})
expect((klass.ancestors & [Hashie::Extensions::MethodReader, Hashie::Extensions::MethodWriter, Hashie::Extensions::MethodQuery]).size).to eq 3
end
end

describe Hashie::Extensions::MethodOverridingWriter do
class OverridingHash < Hash
include Hashie::Extensions::MethodOverridingWriter
end

subject { OverridingHash.new }

it 'writes from a method call' do
subject.awesome = 'sauce'
expect(subject['awesome']).to eq 'sauce'
end

it 'convertes the key using the #convert_key method' do
allow(subject).to receive(:convert_key).and_return(:awesome)
subject.awesome = 'sauce'
expect(subject[:awesome]).to eq 'sauce'
end

it 'raises NoMethodError on non equals-ending methods' do
expect { subject.awesome }.to raise_error(NoMethodError)
end

it '#respond_to_missing? correctly' do
expect(subject).to respond_to(:abc=)
expect(subject).not_to respond_to(:abc)
expect(subject.method(:abc=)).not_to be_nil
end

context 'when writing a Hash method' do
before { subject.zip = 'a-dee-doo-dah' }

it 'overrides the original method' do
expect(subject.zip).to eq 'a-dee-doo-dah'
end

it 'aliases the method with two leading underscores' do
expect(subject.__zip).to eq [[%w(zip a-dee-doo-dah)]]
end

it 'does not re-alias when overriding an already overridden method' do
subject.zip = 'test'
expect(subject.zip).to eq 'test'
expect(subject.__zip).to eq [[%w(zip test)]]
end
end
end

describe Hashie::Extensions::MethodAccessWithOverride do
it 'includes all of the other method mixins' do
klass = Class.new(Hash)
klass.send :include, Hashie::Extensions::MethodAccessWithOverride
expect((klass.ancestors & [Hashie::Extensions::MethodReader, Hashie::Extensions::MethodOverridingWriter, Hashie::Extensions::MethodQuery]).size).to eq 3
end
end

0 comments on commit 4bf710c

Please sign in to comment.