From 4bf710c54d3aa2d339525331a9057038abc549b3 Mon Sep 17 00:00:00 2001 From: Michael Herold Date: Tue, 19 Aug 2014 11:44:21 -0500 Subject: [PATCH] Add Hashie::Extensions::MethodOverridingWriter This is part 2 of 3 of the to-do list determined in #198. --- CHANGELOG.md | 1 + README.md | 23 ++++++ lib/hashie/extensions/method_access.rb | 75 ++++++++++++++++++++ spec/hashie/extensions/method_access_spec.rb | 55 ++++++++++++++ 4 files changed, 154 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8772ab93..1d19efec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 246867e3..30f8c489 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/hashie/extensions/method_access.rb b/lib/hashie/extensions/method_access.rb index fe8651ed..bdf2fa7d 100644 --- a/lib/hashie/extensions/method_access.rb +++ b/lib/hashie/extensions/method_access.rb @@ -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 diff --git a/spec/hashie/extensions/method_access_spec.rb b/spec/hashie/extensions/method_access_spec.rb index 6034cef6..4115f9db 100644 --- a/spec/hashie/extensions/method_access_spec.rb +++ b/spec/hashie/extensions/method_access_spec.rb @@ -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