summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Bleigh <michael@intridea.com>2010-03-05 12:51:58 -0500
committerMichael Bleigh <michael@intridea.com>2010-03-05 12:51:58 -0500
commite33432cafc372a8864003be5639686a35ca510ba (patch)
treeb8b5385d4d4c2e61501638811e95fa41e4fb71d7
parent6ddc22d4034089f0d4730bb50d74f64f1f69044e (diff)
downloadhashie-e33432cafc372a8864003be5639686a35ca510ba.tar.gz
Adds Clash to the mix.
-rw-r--r--README.rdoc36
-rw-r--r--lib/hashie.rb3
-rw-r--r--lib/hashie/clash.rb86
-rw-r--r--spec/hashie/clash_spec.rb42
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