diff options
author | Michael Bleigh <michael@intridea.com> | 2010-03-05 12:51:58 -0500 |
---|---|---|
committer | Michael Bleigh <michael@intridea.com> | 2010-03-05 12:51:58 -0500 |
commit | e33432cafc372a8864003be5639686a35ca510ba (patch) | |
tree | b8b5385d4d4c2e61501638811e95fa41e4fb71d7 | |
parent | 6ddc22d4034089f0d4730bb50d74f64f1f69044e (diff) | |
download | hashie-e33432cafc372a8864003be5639686a35ca510ba.tar.gz |
Adds Clash to the mix.
-rw-r--r-- | README.rdoc | 36 | ||||
-rw-r--r-- | lib/hashie.rb | 3 | ||||
-rw-r--r-- | lib/hashie/clash.rb | 86 | ||||
-rw-r--r-- | spec/hashie/clash_spec.rb | 42 |
4 files changed, 158 insertions, 9 deletions
diff --git a/README.rdoc b/README.rdoc index 2fdf033..a8b8d10 100644 --- a/README.rdoc +++ b/README.rdoc @@ -4,14 +4,8 @@ Hashie is a growing collection of tools that extend Hashes and make them more useful. == Installation - -Hashie is a gem and is available on Gemcutter. If you don't have Gemcutter, -install it: - - gem install gemcutter - gem tumble -Then you can install Hashie: +Hashie is available as a RubyGem: gem install hashie @@ -62,7 +56,33 @@ can set defaults for each property. p = Person.new(:name => "Bob") p.name # => 'Bob' p.occupation # => 'Rubyist' - + +== Clash + +Clash is a Chainable Lazy Hash that allows you to easily construct +complex hashes using method notation chaining. This will allow you +to use a more action-oriented approach to building options hashes. + +Essentially, a Clash is a generalized way to provide much of the same +kind of "chainability" that libraries like Arel or Rails 2.x's named_scopes +provide. + +=== Example + + c = Hashie::Clash.new + c.where(:abc => 'def').order(:created_at) + c # => {:where => {:abc => 'def}, :order => :created_at} + + # You can also use bang notation to chain into sub-hashes, + # jumping back up the chain with _end! + c = Hashie::Clash.new + c.where!.abc('def').ghi(123)._end!.order(:created_at) + c # => {:where => {:abc => 'def', :ghi => 123}, :order => :created_at} + + # Multiple hashes are merged automatically + c = Hashie::Clash.new + c.where(:abc => 'def').where(:hgi => 123) + c # => {:where => {:abc => 'def', :hgi => 123}} == Note on Patches/Pull Requests diff --git a/lib/hashie.rb b/lib/hashie.rb index 87c8d45..06c238a 100644 --- a/lib/hashie.rb +++ b/lib/hashie.rb @@ -1,4 +1,5 @@ require 'hashie/hash_extensions' require 'hashie/hash' require 'hashie/mash' -require 'hashie/dash'
\ No newline at end of file +require 'hashie/dash' +require 'hashie/clash'
\ No newline at end of file diff --git a/lib/hashie/clash.rb b/lib/hashie/clash.rb new file mode 100644 index 0000000..8a8ed7b --- /dev/null +++ b/lib/hashie/clash.rb @@ -0,0 +1,86 @@ +require 'hashie/hash' + +module Hashie + # + # A Clash is a "Chainable Lazy Hash". Inspired by libraries such as Arel, + # a Clash allows you to chain together method arguments to build a + # hash, something that's especially useful if you're doing something + # like constructing a complex options hash. Here's a basic example: + # + # c = Hashie::Clash.new.conditions(:foo => 'bar').order(:created_at) + # c # => {:conditions => {:foo => 'bar'}, :order => :created_at} + # + # Clash provides another way to create sub-hashes by using bang notation. + # You can dive into a sub-hash by providing a key with a bang and dive + # back out again with the _end! method. Example: + # + # c = Hashie::Clash.new.conditions!.foo('bar').baz(123)._end!.order(:created_at) + # c # => {:conditions => {:foo => 'bar', :baz => 123}, :order => :created_at} + # + # Because the primary functionality of Clash is to build options objects, + # all keys are converted to symbols since many libraries expect symbols explicitly + # for keys. + # + class Clash < ::Hash + class ChainError < ::StandardError; end + # The parent Clash if this Clash was created via chaining. + attr_reader :_parent + + # Initialize a new clash by passing in a Hash to + # convert and, optionally, the parent to which this + # Clash is chained. + def initialize(other_hash = {}, parent = nil) + @_parent = parent + other_hash.each_pair do |k, v| + self[k.to_sym] = v + end + end + + # Jump back up a level if you are using bang method + # chaining. For example: + # + # c = Hashie::Clash.new.foo('bar') + # c.baz!.foo(123) # => c[:baz] + # c.baz!._end! # => c + def _end! + self._parent + end + + def id(*args) #:nodoc: + method_missing(:id, *args) + end + + def merge_store(key, *args) #:nodoc: + case args.length + when 1 + val = args.first + val = self[key].merge(val) if self[key].is_a?(::Hash) && val.is_a?(::Hash) + else + val = args + end + + self[key.to_sym] = val + self + end + + def method_missing(name, *args) #:nodoc: + name = name.to_s + if name.match(/!$/) && args.empty? + key = name[0...-1].to_sym + + if self[key].nil? + self[key] = Clash.new({}, self) + elsif self[key].is_a?(::Hash) && !self[key].is_a?(Clash) + self[key] = Clash.new(self[key], self) + else + raise ChainError, "Tried to chain into a non-hash key." + end + + self[key] + elsif args.any? + key = name.to_sym + self.merge_store(key, *args) + end + end + end +end
\ No newline at end of file diff --git a/spec/hashie/clash_spec.rb b/spec/hashie/clash_spec.rb new file mode 100644 index 0000000..6363d5d --- /dev/null +++ b/spec/hashie/clash_spec.rb @@ -0,0 +1,42 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe Hashie::Clash do + before do + @c = Hashie::Clash.new + end + + it 'should be able to set an attribute via method_missing' do + @c.foo('bar') + @c[:foo].should == 'bar' + end + + it 'should be able to set multiple attributes' do + @c.foo('bar').baz('wok') + @c.should == {:foo => 'bar', :baz => 'wok'} + end + + it 'should convert multiple arguments into an array' do + @c.foo(1, 2, 3) + @c[:foo].should == [1,2,3] + end + + it 'should be able to use bang notation to create a new Clash on a key' do + @c.foo! + @c[:foo].should be_kind_of(Hashie::Clash) + end + + it 'should be able to chain onto the new Clash when using bang notation' do + @c.foo!.bar('abc').baz(123) + @c.should == {:foo => {:bar => 'abc', :baz => 123}} + end + + it 'should be able to jump back up to the parent in the chain with #_end!' do + @c.foo!.bar('abc')._end!.baz(123) + @c.should == {:foo => {:bar => 'abc'}, :baz => 123} + end + + it 'should merge rather than replace existing keys' do + @c.where(:abc => 'def').where(:hgi => 123) + @c.should == {:where => {:abc => 'def', :hgi => 123}} + end +end |