diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..a4541be --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +-m markdown diff --git a/README.markdown b/README.markdown index d53b568..06993bd 100644 --- a/README.markdown +++ b/README.markdown @@ -65,6 +65,9 @@ counterparts. You can also include just stringify or just symbolize with ### MergeInitializer +The MergeInitializer extension simply makes it possible to initialize a +Hash subclass with another Hash, giving you a quick short-hand. + ### MethodAccess The MethodAccess extension allows you to quickly build method-based @@ -81,17 +84,24 @@ included as individual modules, i.e. `Hashie::Extensions::MethodReader`, h.abc # => 'def' h.abc? # => true +### 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. + +A unique feature of Hashie's IndifferentAccess mixin is that it will +inject itself recursively into subhashes *without* reinitializing the +hash in question. This means you can safely merge together indifferent +and non-indifferent hashes arbitrarily deeply without worrying about +whether you'll be able to `hash[:other][:another]` properly. + ### DeepMerge (Unimplemented) This extension *will* allow you to easily include a recursive merging system to any Hash descendant. -### IndifferentAccess (Unimplemented) - -This extension *will* allow you to easily give a hash rules for -normalizing keys, for instance to allow symbol or string keys both to -reach the intended value. - ## Mash Mash is an extended Hash that gives simple pseudo-object functionality diff --git a/lib/hashie/extensions/indifferent_access.rb b/lib/hashie/extensions/indifferent_access.rb index 6635a1d..50d4081 100644 --- a/lib/hashie/extensions/indifferent_access.rb +++ b/lib/hashie/extensions/indifferent_access.rb @@ -1,7 +1,110 @@ module Hashie module Extensions + # IndifferentAccess gives you the ability to not care + # whether your hash has string or symbol keys. Made famous + # in Rails for accessing query and POST parameters, this + # is a handy tool for making sure your hash has maximum + # utility. + # + # One unique feature of this mixin is that it will recursively + # inject itself into sub-hash instances without modifying + # the actual class of the sub-hash. + # + # @example + # class MyHash < Hash + # include Hashie::Extensions::MergeInitializer + # include Hashie::Extensions::IndifferentAccess + # end + # + # h = MyHash.new(:foo => 'bar', 'baz' => 'blip') + # h['foo'] # => 'bar' + # h[:foo] # => 'bar' + # h[:baz] # => 'blip' + # h['baz'] # => 'blip' + # module IndifferentAccess - # TODO: Implement indifferent access. + def self.included(base) + base.class_eval do + alias_method :regular_writer, :[]= + alias_method :[]=, :indifferent_writer + %w(default update fetch delete key? values_at).each do |m| + alias_method "regular_#{m}", m + alias_method m, "indifferent_#{m}" + end + end + end + + # This will inject indifferent access into an instance of + # a hash without modifying the actual class. This is what + # allows IndifferentAccess to spread to sub-hashes. + def self.inject!(hash) + hash.singleton_class.send :include, Hashie::Extensions::IndifferentAccess + hash.convert! + end + + # Injects indifferent access into a duplicate of the hash + # provided. See #inject! + def self.inject(hash) + inject!(hash.dup) + end + + def convert_key(key) + key.to_s + end + + # Iterates through the keys and values, reconverting them to + # their proper indifferent state. Used when IndifferentAccess + # is injecting itself into member hashes. + def convert! + keys.each do |k| + regular_writer convert_key(k), convert_value(self.regular_delete(k)) + end + self + end + + def convert_value(value) + if hash_lacking_indifference?(value) + Hashie::Extensions::IndifferentAccess.inject(value.dup) + elsif value.is_a?(::Array) + value.dup.replace(value.map { |e| convert_value(e) }) + else + value + end + end + + def indifferent_default(key = nil) + return self[convert_key(key)] if key?(key) + regular_default(key) + end + + def indifferent_update(other_hash) + return regular_update(other_hash) if hash_with_indifference?(other_hash) + other_hash.each_pair do |k,v| + self[k] = v + end + end + + def indifferent_writer(key, value); regular_writer convert_key(key), convert_value(value) end + def indifferent_fetch(key, *args); regular_fetch convert_key(key), *args end + def indifferent_delete(key); regular_delete convert_key(key) end + def indifferent_key?(key); regular_key? convert_key(key) end + def indifferent_values_at(*indices); indices.map{|i| self[i] } end + + def indifferent_access?; true end + + protected + + def hash_lacking_indifference?(other) + other.is_a?(::Hash) && + !(other.respond_to?(:indifferent_access?) && + other.indifferent_access?) + end + + def hash_with_indifference?(other) + other.is_a?(::Hash) && + other.respond_to?(:indifferent_access?) && + other.indifference_access? + end end end end diff --git a/spec/hashie/extensions/indifferent_access_spec.rb b/spec/hashie/extensions/indifferent_access_spec.rb index 12ba044..7c421cb 100644 --- a/spec/hashie/extensions/indifferent_access_spec.rb +++ b/spec/hashie/extensions/indifferent_access_spec.rb @@ -1,5 +1,66 @@ require 'spec_helper' describe Hashie::Extensions::IndifferentAccess do - + class IndifferentHash < Hash + include Hashie::Extensions::MergeInitializer + include Hashie::Extensions::IndifferentAccess + end + subject{ IndifferentHash } + + it 'should be able to access via string or symbol' do + h = subject.new(:abc => 123) + h[:abc].should == 123 + h['abc'].should == 123 + end + + describe '#values_at' do + it 'should indifferently find values' do + h = subject.new(:foo => 'bar', 'baz' => 'qux') + h.values_at('foo', :baz).should == %w(bar qux) + end + end + + describe '#fetch' do + it 'should work like normal fetch, but indifferent' do + h = subject.new(:foo => 'bar') + h.fetch(:foo).should == h.fetch('foo') + h.fetch(:foo).should == 'bar' + end + end + + describe '#delete' do + it 'should delete indifferently' do + h = subject.new(:foo => 'bar', 'baz' => 'qux') + h.delete('foo') + h.delete(:baz) + h.should be_empty + end + end + + describe '#key?' do + it 'should find it indifferently' do + h = subject.new(:foo => 'bar') + h.should be_key(:foo) + h.should be_key('foo') + end + end + + describe '#update' do + subject{ IndifferentHash.new(:foo => 'bar') } + it 'should allow keys to be indifferent still' do + subject.update(:baz => 'qux') + subject['foo'].should == 'bar' + subject['baz'].should == 'qux' + end + + it 'should recursively inject indifference into sub-hashes' do + subject.update(:baz => {:qux => 'abc'}) + subject['baz']['qux'].should == 'abc' + end + + it 'should not change the ancestors of the injected object class' do + subject.update(:baz => {:qux => 'abc'}) + Hash.new.should_not be_respond_to(:indifferent_access?) + end + end end