summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Bleigh <michael@intridea.com>2009-09-06 00:27:54 -0400
committerMichael Bleigh <michael@intridea.com>2009-09-06 00:27:54 -0400
commitfc03a24666ee9db6cfb6c85c6b1fa12d3308c586 (patch)
tree380a4beb2195c281adb8a8667a97f6b30ad0b355
parenta7781d369ad474d0df0d5ae6e7cc0c8eabc1f7dc (diff)
downloadhashie-fc03a24666ee9db6cfb6c85c6b1fa12d3308c586.tar.gz
All specs are passing again after fixing a namespacing issue.
-rw-r--r--README.rdoc5
-rw-r--r--lib/hashie.rb3
-rw-r--r--lib/hashie/hash.rb8
-rw-r--r--lib/hashie/hash_extensions.rb31
-rw-r--r--lib/hashie/mash.rb208
-rw-r--r--spec/hashie/hash_spec.rb22
-rw-r--r--spec/hashie/mash_spec.rb116
-rw-r--r--spec/spec.opts2
-rw-r--r--spec/spec_helper.rb1
9 files changed, 394 insertions, 2 deletions
diff --git a/README.rdoc b/README.rdoc
index 9d186f0..fbf75b3 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,6 +1,7 @@
-= hashie
+= Hashie
-Description goes here.
+Hashie is a growing collection of tools that extend Hashes and make
+them more useful.
== Note on Patches/Pull Requests
diff --git a/lib/hashie.rb b/lib/hashie.rb
index e69de29..e24d544 100644
--- a/lib/hashie.rb
+++ b/lib/hashie.rb
@@ -0,0 +1,3 @@
+require 'hashie/hash_extensions'
+require 'hashie/hash'
+require 'hashie/mash' \ No newline at end of file
diff --git a/lib/hashie/hash.rb b/lib/hashie/hash.rb
new file mode 100644
index 0000000..d13a58b
--- /dev/null
+++ b/lib/hashie/hash.rb
@@ -0,0 +1,8 @@
+module Hashie
+ # A Hashie Hash is simply a Hash that has convenience
+ # functions baked in such as stringify_keys that may
+ # not be available in all libraries.
+ class Hash < Hash
+ include Hashie::HashExtensions
+ end
+end \ No newline at end of file
diff --git a/lib/hashie/hash_extensions.rb b/lib/hashie/hash_extensions.rb
new file mode 100644
index 0000000..188c10b
--- /dev/null
+++ b/lib/hashie/hash_extensions.rb
@@ -0,0 +1,31 @@
+module Hashie
+ module HashExtensions
+ def self.included(base)
+ # Don't tread on existing extensions of Hash by
+ # adding methods that are likely to exist.
+ %w(stringify_keys stringify_keys!).each do |hashie_method|
+ base.send :alias_method, hashie_method, "hashie_#{hashie_method}" unless base.instance_methods.include?(hashie_method)
+ end
+ end
+
+ # Destructively convert all of the keys of a Hash
+ # to their string representations.
+ def hashie_stringify_keys!
+ self.keys.each do |k|
+ self[k.to_s] = self.delete(k)
+ end
+ self
+ end
+
+ # Convert all of the keys of a Hash
+ # to their string representations.
+ def hashie_stringify_keys
+ self.dup.stringify_keys!
+ end
+
+ # Convert this hash into a Mash
+ def to_mash
+ Hashie::Mash.new(self)
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/hashie/mash.rb b/lib/hashie/mash.rb
new file mode 100644
index 0000000..39bfd69
--- /dev/null
+++ b/lib/hashie/mash.rb
@@ -0,0 +1,208 @@
+module Hashie
+ # Mash allows you to create pseudo-objects that have method-like
+ # accessors for hash keys. This is useful for such implementations
+ # as an API-accessing library that wants to fake robust objects
+ # without the overhead of actually doing so. Think of it as OpenStruct
+ # with some additional goodies.
+ #
+ # A Mash will look at the methods you pass it and perform operations
+ # based on the following rules:
+ #
+ # * No punctuation: Returns the value of the hash for that key, or nil if none exists.
+ # * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
+ # * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
+ # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
+ #
+ # == Basic Example
+ #
+ # mash = Mash.new
+ # mash.name? # => false
+ # mash.name = "Bob"
+ # mash.name # => "Bob"
+ # mash.name? # => true
+ #
+ # == Hash Conversion Example
+ #
+ # hash = {:a => {:b => 23, :d => {:e => "abc"}}, :f => [{:g => 44, :h => 29}, 12]}
+ # mash = Mash.new(hash)
+ # mash.a.b # => 23
+ # mash.a.d.e # => "abc"
+ # mash.f.first.g # => 44
+ # mash.f.last # => 12
+ #
+ # == Bang Example
+ #
+ # mash = Mash.new
+ # mash.author # => nil
+ # mash.author! # => <Mash>
+ #
+ # mash = Mash.new
+ # mash.author!.name = "Michael Bleigh"
+ # mash.author # => <Mash name="Michael Bleigh">
+ #
+ class Mash < Hashie::Hash
+ # If you pass in an existing hash, it will
+ # convert it to a Mash including recursively
+ # descending into arrays and hashes, converting
+ # them as well.
+ def initialize(source_hash = nil, default = nil, &blk)
+ deep_update(source_hash) if source_hash
+ super default if default
+ super &blk if blk
+ end
+
+ class << self; alias [] new; end
+
+ def id #:nodoc:
+ self["id"] ? self["id"] : super
+ end
+
+ # Borrowed from Merb's Mash object.
+ #
+ # ==== Parameters
+ # key<Object>:: The default value for the mash. Defaults to nil.
+ #
+ # ==== Alternatives
+ # If key is a Symbol and it is a key in the mash, then the default value will
+ # be set to the value matching the key.
+ def default(key = nil)
+ if key.is_a?(Symbol) && key?(key.to_s)
+ self[key]
+ else
+ key ? super : super()
+ end
+ end
+
+ alias_method :regular_reader, :[]
+ alias_method :regular_writer, :[]=
+
+ # Retrieves an attribute set in the Mash. Will convert
+ # any key passed in to a string before retrieving.
+ def [](key)
+ key = convert_key(key)
+ regular_reader(key)
+ end
+
+ # Sets an attribute in the Mash. Key will be converted to
+ # a string before it is set, and Hashes will be converted
+ # into Mashes for nesting purposes.
+ def []=(key,value) #:nodoc:
+ key = convert_key(key)
+ regular_writer(key, convert_value(value))
+ end
+
+ # This is the bang method reader, it will return a new Mash
+ # if there isn't a value already assigned to the key requested.
+ def initializing_reader(key)
+ self[key] = Hashie::Mash.new unless key?(key)
+ self[key]
+ end
+
+ alias_method :regular_dup, :dup
+ # Duplicates the current mash as a new mash.
+ def dup
+ Mash.new(self, self.default)
+ end
+
+ alias_method :picky_key?, :key?
+ def key?(key)
+ picky_key?(convert_key(key))
+ end
+
+ alias_method :regular_inspect, :inspect
+ # Prints out a pretty object-like string of the
+ # defined attributes.
+ def inspect
+ ret = "<#{self.class.to_s}"
+ keys.sort.each do |key|
+ ret << " #{key}=#{self[key].inspect}"
+ end
+ ret << ">"
+ ret
+ end
+ alias_method :to_s, :inspect
+
+ # Performs a deep_update on a duplicate of the
+ # current mash.
+ def deep_merge(other_hash)
+ dup.deep_merge!(other_hash)
+ end
+
+ # Recursively merges this mash with the passed
+ # in hash, merging each hash in the hierarchy.
+ def deep_update(other_hash)
+ other_hash = Hashie::Hash[other_hash].stringify_keys!
+
+ other_hash.each_pair do |k,v|
+ k = convert_key(k)
+ self[k] = Hashie::Mash.new(self[k]).to_mash if self[k].is_a?(Hash) unless self[k].is_a?(Hashie::Mash)
+ if self[k].is_a?(Hashie::Mash) && other_hash[k].is_a?(Hash)
+ self[k] = self[k].deep_merge(other_hash[k])
+ else
+ self[k] = convert_value(other_hash[k],true)
+ end
+ end
+
+ self
+ end
+ alias_method :deep_merge!, :deep_update
+
+ # ==== Parameters
+ # other_hash<Hash>::
+ # A hash to update values in the mash with. Keys will be
+ # stringified and Hashes will be converted to Mashes.
+ #
+ # ==== Returns
+ # Mash:: The updated mash.
+ def update(other_hash)
+ other_hash.each_pair do |key, value|
+ if respond_to?(convert_key(key) + "=")
+ self.send(convert_key(key) + "=", convert_value(value))
+ else
+ regular_writer(convert_key(key), convert_value(value))
+ end
+ end
+ self
+ end
+ alias_method :merge!, :update
+
+ # Converts a mash back to a hash (with stringified keys)
+ def to_hash
+ Hash.new(default).merge(self)
+ end
+
+ def method_missing(method_name, *args) #:nodoc:
+ if (match = method_name.to_s.match(/(.*)=$/)) && args.size == 1
+ self[match[1]] = args.first
+ elsif (match = method_name.to_s.match(/(.*)\?$/)) && args.size == 0
+ key?(match[1])
+ elsif (match = method_name.to_s.match(/(.*)!$/)) && args.size == 0
+ initializing_reader(match[1])
+ elsif key?(method_name)
+ self[method_name]
+ elsif match = method_name.to_s.match(/^([a-z][a-z0-9A-Z_]+)$/)
+ default(method_name)
+ else
+ super
+ end
+ end
+
+ protected
+
+ def convert_key(key) #:nodoc:
+ key.to_s
+ end
+
+ def convert_value(val, duping=false) #:nodoc:
+ case val
+ when ::Hash
+ val = val.dup if duping
+ Hashie::Mash.new(val)
+ when Array
+ val.collect{ |e| convert_value(e) }
+ else
+ val
+ end
+ end
+ end
+end
diff --git a/spec/hashie/hash_spec.rb b/spec/hashie/hash_spec.rb
new file mode 100644
index 0000000..78d44be
--- /dev/null
+++ b/spec/hashie/hash_spec.rb
@@ -0,0 +1,22 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Hash do
+ it "should be convertible to a Hashie::Mash" do
+ mash = Hashie::Hash[:some => "hash"].to_mash
+ mash.is_a?(Hashie::Mash).should be_true
+ mash.some.should == "hash"
+ end
+
+ it "#stringify_keys! should turn all keys into strings" do
+ hash = Hashie::Hash[:a => "hey", 123 => "bob"]
+ hash.stringify_keys!
+ hash.should == Hashie::Hash["a" => "hey", "123" => "bob"]
+ end
+
+ it "#stringify_keys should return a hash with stringified keys" do
+ hash = Hashie::Hash[:a => "hey", 123 => "bob"]
+ stringified_hash = hash.stringify_keys
+ hash.should == Hashie::Hash[:a => "hey", 123 => "bob"]
+ stringified_hash.should == Hashie::Hash["a" => "hey", "123" => "bob"]
+ end
+end \ No newline at end of file
diff --git a/spec/hashie/mash_spec.rb b/spec/hashie/mash_spec.rb
new file mode 100644
index 0000000..7adbe6c
--- /dev/null
+++ b/spec/hashie/mash_spec.rb
@@ -0,0 +1,116 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Hashie::Mash do
+ before(:each) do
+ @mash = Hashie::Mash.new
+ end
+
+ it "should inherit from hash" do
+ @mash.is_a?(Hash).should be_true
+ end
+
+ it "should be able to set hash values through method= calls" do
+ @mash.test = "abc"
+ @mash["test"].should == "abc"
+ end
+
+ it "should be able to retrieve set values through method calls" do
+ @mash["test"] = "abc"
+ @mash.test.should == "abc"
+ end
+
+ it "should test for already set values when passed a ? method" do
+ @mash.test?.should be_false
+ @mash.test = "abc"
+ @mash.test?.should be_true
+ end
+
+ it "should make all [] and []= into strings for consistency" do
+ @mash["abc"] = 123
+ @mash.key?('abc').should be_true
+ @mash["abc"].should == 123
+ end
+
+ it "should have a to_s that is identical to its inspect" do
+ @mash.abc = 123
+ @mash.to_s.should == @mash.inspect
+ end
+
+ it "should return nil instead of raising an error for attribute-esque method calls" do
+ @mash.abc.should be_nil
+ end
+
+ it "should return a Hashie::Mash when passed a bang method to a non-existenct key" do
+ @mash.abc!.is_a?(Hashie::Mash).should be_true
+ end
+
+ it "should return the existing value when passed a bang method for an existing key" do
+ @mash.name = "Bob"
+ @mash.name!.should == "Bob"
+ end
+
+ it "#initializing_reader should return a Hashie::Mash when passed a non-existent key" do
+ @mash.initializing_reader(:abc).is_a?(Hashie::Mash).should be_true
+ end
+
+ it "should allow for multi-level assignment through bang methods" do
+ @mash.author!.name = "Michael Bleigh"
+ @mash.author.should == Hashie::Mash.new(:name => "Michael Bleigh")
+ @mash.author!.website!.url = "http://www.mbleigh.com/"
+ @mash.author.website.should == Hashie::Mash.new(:url => "http://www.mbleigh.com/")
+ end
+
+ it "#deep_update should recursively Hashie::Mash Hashie::Mashes and hashes together" do
+ @mash.first_name = "Michael"
+ @mash.last_name = "Bleigh"
+ @mash.details = Hashie::Hash[:email => "michael@asf.com"].to_mash
+ @mash.deep_update({:details => {:email => "michael@intridea.com"}})
+ @mash.details.email.should == "michael@intridea.com"
+ end
+
+ it "should convert hash assignments into Hashie::Mashes" do
+ @mash.details = {:email => 'randy@asf.com', :address => {:state => 'TX'} }
+ @mash.details.email.should == 'randy@asf.com'
+ @mash.details.address.state.should == 'TX'
+ end
+
+ context "#initialize" do
+ it "should convert an existing hash to a Hashie::Mash" do
+ converted = Hashie::Mash.new({:abc => 123, :name => "Bob"})
+ converted.abc.should == 123
+ converted.name.should == "Bob"
+ end
+
+ it "should convert hashes recursively into Hashie::Mashes" do
+ converted = Hashie::Mash.new({:a => {:b => 1, :c => {:d => 23}}})
+ converted.a.is_a?(Hashie::Mash).should be_true
+ converted.a.b.should == 1
+ converted.a.c.d.should == 23
+ end
+
+ it "should convert hashes in arrays into Hashie::Mashes" do
+ converted = Hashie::Mash.new({:a => [{:b => 12}, 23]})
+ converted.a.first.b.should == 12
+ converted.a.last.should == 23
+ end
+
+ it "should convert an existing Hashie::Mash into a Hashie::Mash" do
+ initial = Hashie::Mash.new(:name => 'randy', :address => {:state => 'TX'})
+ copy = Hashie::Mash.new(initial)
+ initial.name.should == copy.name
+ initial.object_id.should_not == copy.object_id
+ copy.address.state.should == 'TX'
+ copy.address.state = 'MI'
+ initial.address.state.should == 'TX'
+ copy.address.object_id.should_not == initial.address.object_id
+ end
+
+ it "should accept a default block" do
+ initial = Hashie::Mash.new { |h,i| h[i] = []}
+ initial.default_proc.should_not be_nil
+ initial.default.should be_nil
+ initial.test.should == []
+ initial.test?.should be_true
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/spec.opts b/spec/spec.opts
new file mode 100644
index 0000000..44f9a9c
--- /dev/null
+++ b/spec/spec.opts
@@ -0,0 +1,2 @@
+--colour
+--format progress \ No newline at end of file
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index fd23375..5ba09a8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,5 +1,6 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
require 'hashie'
require 'spec'
require 'spec/autorun'