wahashie 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ require 'wahashie/hash_extensions'
2
+
3
+ module Wahashie
4
+ # A Wahashie Hash is simply a Hash that has convenience
5
+ # functions baked in such as stringify_keys that may
6
+ # not be available in all libraries.
7
+ class Hash < Hash
8
+ include Wahashie::HashExtensions
9
+
10
+ # Converts a mash back to a hash.
11
+ def to_hash(options = {})
12
+ out = {}
13
+ keys.each do |k|
14
+ key = options[:symbolize_keys] ? k.to_sym : k.to_s
15
+ out[key] = Wahashie::Hash === self[k] ? self[k].to_hash : self[k]
16
+ end
17
+ out
18
+ end
19
+
20
+ # The C geneartor for the json gem doesn't like mashies
21
+ def to_json(*args)
22
+ to_hash.to_json(*args)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ module Wahashie
2
+ module HashExtensions
3
+ def self.included(base)
4
+ # Don't tread on existing extensions of Hash by
5
+ # adding methods that are likely to exist.
6
+ %w(stringify_keys stringify_keys!).each do |wahashie_method|
7
+ base.send :alias_method, wahashie_method, "wahashie_#{wahashie_method}" unless base.instance_methods.include?(wahashie_method)
8
+ end
9
+ end
10
+
11
+ # Destructively convert all of the keys of a Hash
12
+ # to their string representations.
13
+ def wahashie_stringify_keys!
14
+ self.keys.each do |k|
15
+ unless String === k
16
+ self[k.to_s] = self.delete(k)
17
+ end
18
+ end
19
+ self
20
+ end
21
+
22
+ # Convert all of the keys of a Hash
23
+ # to their string representations.
24
+ def wahashie_stringify_keys
25
+ self.dup.stringify_keys!
26
+ end
27
+
28
+ # Convert this hash into a Mash
29
+ def to_mash
30
+ ::Wahashie::Mash.new(self)
31
+ end
32
+ end
33
+
34
+ module PrettyInspect
35
+ def self.included(base)
36
+ base.send :alias_method, :hash_inspect, :inspect
37
+ base.send :alias_method, :inspect, :wahashie_inspect
38
+ end
39
+
40
+ def wahashie_inspect
41
+ ret = "#<#{self.class.to_s}"
42
+ stringify_keys.keys.sort.each do |key|
43
+ ret << " #{key}=#{self[key].inspect}"
44
+ end
45
+ ret << ">"
46
+ ret
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,197 @@
1
+ require 'wahashie/hash'
2
+
3
+ module Wahashie
4
+ # Mash allows you to create pseudo-objects that have method-like
5
+ # accessors for hash keys. This is useful for such implementations
6
+ # as an API-accessing library that wants to fake robust objects
7
+ # without the overhead of actually doing so. Think of it as OpenStruct
8
+ # with some additional goodies.
9
+ #
10
+ # A Mash will look at the methods you pass it and perform operations
11
+ # based on the following rules:
12
+ #
13
+ # * No punctuation: Returns the value of the hash for that key, or nil if none exists.
14
+ # * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
15
+ # * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
16
+ # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
17
+ #
18
+ # == Basic Example
19
+ #
20
+ # mash = Mash.new
21
+ # mash.name? # => false
22
+ # mash.name = "Bob"
23
+ # mash.name # => "Bob"
24
+ # mash.name? # => true
25
+ #
26
+ # == Hash Conversion Example
27
+ #
28
+ # hash = {:a => {:b => 23, :d => {:e => "abc"}}, :f => [{:g => 44, :h => 29}, 12]}
29
+ # mash = Mash.new(hash)
30
+ # mash.a.b # => 23
31
+ # mash.a.d.e # => "abc"
32
+ # mash.f.first.g # => 44
33
+ # mash.f.last # => 12
34
+ #
35
+ # == Bang Example
36
+ #
37
+ # mash = Mash.new
38
+ # mash.author # => nil
39
+ # mash.author! # => <Mash>
40
+ #
41
+ # mash = Mash.new
42
+ # mash.author!.name = "Michael Bleigh"
43
+ # mash.author # => <Mash name="Michael Bleigh">
44
+ #
45
+ class Mash < Wahashie::Hash
46
+ include Wahashie::PrettyInspect
47
+ alias_method :to_s, :inspect
48
+
49
+ # If you pass in an existing hash, it will
50
+ # convert it to a Mash including recursively
51
+ # descending into arrays and hashes, converting
52
+ # them as well.
53
+ def initialize(source_hash = nil, default = nil, &blk)
54
+ deep_update(source_hash) if source_hash
55
+ default ? super(default) : super(&blk)
56
+ end
57
+
58
+ class << self
59
+ alias [] new
60
+
61
+ def subkey_class
62
+ self
63
+ end
64
+ end
65
+
66
+ def id #:nodoc:
67
+ key?("id") ? self["id"] : super
68
+ end
69
+
70
+ def type #:nodoc:
71
+ key?("type") ? self["type"] : super
72
+ end
73
+
74
+ alias_method :regular_reader, :[]
75
+ alias_method :regular_writer, :[]=
76
+
77
+ # Retrieves an attribute set in the Mash. Will convert
78
+ # any key passed in to a string before retrieving.
79
+ def [](key)
80
+ value = regular_reader(convert_key(key))
81
+ yield value if block_given?
82
+ value
83
+ end
84
+
85
+ # Sets an attribute in the Mash. Key will be converted to
86
+ # a string before it is set, and Hashes will be converted
87
+ # into Mashes for nesting purposes.
88
+ def []=(key,value) #:nodoc:
89
+ regular_writer(convert_key(key), convert_value(value))
90
+ end
91
+
92
+ # This is the bang method reader, it will return a new Mash
93
+ # if there isn't a value already assigned to the key requested.
94
+ def initializing_reader(key)
95
+ ck = convert_key(key)
96
+ regular_writer(ck, self.class.new) unless key?(ck)
97
+ regular_reader(ck)
98
+ end
99
+
100
+ def delete(key)
101
+ super(convert_key(key))
102
+ end
103
+
104
+ alias_method :regular_dup, :dup
105
+ # Duplicates the current mash as a new mash.
106
+ def dup
107
+ self.class.new(self, self.default)
108
+ end
109
+
110
+ def key?(key)
111
+ super(convert_key(key))
112
+ end
113
+ alias_method :has_key?, :key?
114
+ alias_method :include?, :key?
115
+ alias_method :member?, :key?
116
+
117
+ # Performs a deep_update on a duplicate of the
118
+ # current mash.
119
+ def deep_merge(other_hash)
120
+ dup.deep_update(other_hash)
121
+ end
122
+ alias_method :merge, :deep_merge
123
+
124
+ # Recursively merges this mash with the passed
125
+ # in hash, merging each hash in the hierarchy.
126
+ def deep_update(other_hash)
127
+ other_hash.each_pair do |k,v|
128
+ key = convert_key(k)
129
+ if regular_reader(key).is_a?(Mash) and v.is_a?(::Hash)
130
+ regular_reader(key).deep_update(v)
131
+ else
132
+ regular_writer(key, convert_value(v, true))
133
+ end
134
+ end
135
+ self
136
+ end
137
+ alias_method :deep_merge!, :deep_update
138
+ alias_method :update, :deep_update
139
+ alias_method :merge!, :update
140
+
141
+ # Performs a shallow_update on a duplicate of the current mash
142
+ def shallow_merge(other_hash)
143
+ dup.shallow_update(other_hash)
144
+ end
145
+
146
+ # Merges (non-recursively) the hash from the argument,
147
+ # changing the receiving hash
148
+ def shallow_update(other_hash)
149
+ other_hash.each_pair do |k,v|
150
+ regular_writer(convert_key(k), convert_value(v, true))
151
+ end
152
+ self
153
+ end
154
+
155
+ # Will return true if the Mash has had a key
156
+ # set in addition to normal respond_to? functionality.
157
+ def respond_to?(method_name, include_private=false)
158
+ return true if key?(method_name)
159
+ super
160
+ end
161
+
162
+ def method_missing(method_name, *args, &blk)
163
+ return self.[](method_name, &blk) if key?(method_name)
164
+ match = method_name.to_s.match(/(.*?)([?=!]?)$/)
165
+ case match[2]
166
+ when "="
167
+ self[match[1]] = args.first
168
+ when "?"
169
+ !!self[match[1]]
170
+ when "!"
171
+ initializing_reader(match[1])
172
+ else
173
+ default(method_name, *args, &blk)
174
+ end
175
+ end
176
+
177
+ protected
178
+
179
+ def convert_key(key) #:nodoc:
180
+ key.to_s
181
+ end
182
+
183
+ def convert_value(val, duping=false) #:nodoc:
184
+ case val
185
+ when self.class
186
+ val.dup
187
+ when ::Hash
188
+ val = val.dup if duping
189
+ self.class.subkey_class.new.merge(val)
190
+ when Array
191
+ val.collect{ |e| convert_value(e) }
192
+ else
193
+ val
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,55 @@
1
+ require 'wahashie/dash'
2
+
3
+ module Wahashie
4
+ # A Trash is a 'translated' Dash where the keys can be remapped from a source
5
+ # hash.
6
+ #
7
+ # Trashes are useful when you need to read data from another application,
8
+ # such as a Java api, where the keys are named differently from how we would
9
+ # in Ruby.
10
+ class Trash < Wahashie::Dash
11
+
12
+ # Defines a property on the Trash. Options are as follows:
13
+ #
14
+ # * <tt>:default</tt> - Specify a default value for this property, to be
15
+ # returned before a value is set on the property in a new Dash.
16
+ # * <tt>:from</tt> - Specify the original key name that will be write only.
17
+ def self.property(property_name, options = {})
18
+ super
19
+
20
+ if options[:from]
21
+ translations << options[:from].to_sym
22
+ class_eval <<-RUBY
23
+ def #{options[:from]}=(val)
24
+ self[:#{property_name}] = val
25
+ end
26
+ RUBY
27
+ end
28
+ end
29
+
30
+ # Set a value on the Dash in a Hash-like way. Only works
31
+ # on pre-existing properties.
32
+ def []=(property, value)
33
+ if self.class.translations.include? property.to_sym
34
+ send("#{property}=", value)
35
+ elsif property_exists? property
36
+ super
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def self.translations
43
+ @translations ||= []
44
+ end
45
+
46
+ # Raises an NoMethodError if the property doesn't exist
47
+ #
48
+ def property_exists?(property)
49
+ unless self.class.property?(property.to_sym)
50
+ raise NoMethodError, "The property '#{property}' is not defined for this Trash."
51
+ end
52
+ true
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module Wahashie
2
+ VERSION = '1.2.0'
3
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format progress
3
+ --backtrace
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+
6
+ require 'wahashie'
7
+ require 'rspec'
8
+ require 'rspec/autorun'
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Wahashie::Clash do
4
+ before do
5
+ @c = Wahashie::Clash.new
6
+ end
7
+
8
+ it 'should be able to set an attribute via method_missing' do
9
+ @c.foo('bar')
10
+ @c[:foo].should == 'bar'
11
+ end
12
+
13
+ it 'should be able to set multiple attributes' do
14
+ @c.foo('bar').baz('wok')
15
+ @c.should == {:foo => 'bar', :baz => 'wok'}
16
+ end
17
+
18
+ it 'should convert multiple arguments into an array' do
19
+ @c.foo(1, 2, 3)
20
+ @c[:foo].should == [1,2,3]
21
+ end
22
+
23
+ it 'should be able to use bang notation to create a new Clash on a key' do
24
+ @c.foo!
25
+ @c[:foo].should be_kind_of(Wahashie::Clash)
26
+ end
27
+
28
+ it 'should be able to chain onto the new Clash when using bang notation' do
29
+ @c.foo!.bar('abc').baz(123)
30
+ @c.should == {:foo => {:bar => 'abc', :baz => 123}}
31
+ end
32
+
33
+ it 'should be able to jump back up to the parent in the chain with #_end!' do
34
+ @c.foo!.bar('abc')._end!.baz(123)
35
+ @c.should == {:foo => {:bar => 'abc'}, :baz => 123}
36
+ end
37
+
38
+ it 'should merge rather than replace existing keys' do
39
+ @c.where(:abc => 'def').where(:hgi => 123)
40
+ @c.should == {:where => {:abc => 'def', :hgi => 123}}
41
+ end
42
+ end
@@ -0,0 +1,215 @@
1
+ require 'spec_helper'
2
+
3
+ Wahashie::Hash.class_eval do
4
+ def self.inherited(klass)
5
+ klass.instance_variable_set('@inheritance_test', true)
6
+ end
7
+ end
8
+
9
+ class DashTest < Wahashie::Dash
10
+ property :first_name, :required => true
11
+ property :email
12
+ property :count, :default => 0
13
+ end
14
+
15
+ class DashNoRequiredTest < Wahashie::Dash
16
+ property :first_name
17
+ property :email
18
+ property :count, :default => 0
19
+ end
20
+
21
+ class Subclassed < DashTest
22
+ property :last_name, :required => true
23
+ end
24
+
25
+ describe DashTest do
26
+
27
+ subject { DashTest.new(:first_name => 'Bob', :email => 'bob@example.com') }
28
+
29
+ it('subclasses Wahashie::Hash') { should respond_to(:to_mash) }
30
+
31
+ its(:to_s) { should == '#<DashTest count=0 email="bob@example.com" first_name="Bob">' }
32
+
33
+ it 'lists all set properties in inspect' do
34
+ subject.first_name = 'Bob'
35
+ subject.email = 'bob@example.com'
36
+ subject.inspect.should == '#<DashTest count=0 email="bob@example.com" first_name="Bob">'
37
+ end
38
+
39
+ its(:count) { should be_zero }
40
+
41
+ it { should respond_to(:first_name) }
42
+ it { should respond_to(:first_name=) }
43
+ it { should_not respond_to(:nonexistent) }
44
+
45
+ it 'errors out for a non-existent property' do
46
+ lambda { subject['nonexistent'] }.should raise_error(NoMethodError)
47
+ end
48
+
49
+ it 'errors out when attempting to set a required property to nil' do
50
+ lambda { subject.first_name = nil }.should raise_error(ArgumentError)
51
+ end
52
+
53
+ context 'writing to properties' do
54
+
55
+ it 'fails writing a required property to nil' do
56
+ lambda { subject.first_name = nil }.should raise_error(ArgumentError)
57
+ end
58
+
59
+ it 'fails writing a required property to nil using []=' do
60
+ lambda { subject['first_name'] = nil }.should raise_error(ArgumentError)
61
+ end
62
+
63
+ it 'fails writing to a non-existent property using []=' do
64
+ lambda { subject['nonexistent'] = 123 }.should raise_error(NoMethodError)
65
+ end
66
+
67
+ it 'works for an existing property using []=' do
68
+ subject['first_name'] = 'Bob'
69
+ subject['first_name'].should == 'Bob'
70
+ subject[:first_name].should == 'Bob'
71
+ end
72
+
73
+ it 'works for an existing property using a method call' do
74
+ subject.first_name = 'Franklin'
75
+ subject.first_name.should == 'Franklin'
76
+ end
77
+ end
78
+
79
+ context 'reading from properties' do
80
+ it 'fails reading from a non-existent property using []' do
81
+ lambda { subject['nonexistent'] }.should raise_error(NoMethodError)
82
+ end
83
+
84
+ it "should be able to retrieve properties through blocks" do
85
+ subject["first_name"] = "Aiden"
86
+ value = nil
87
+ subject.[]("first_name") { |v| value = v }
88
+ value.should == "Aiden"
89
+ end
90
+
91
+ it "should be able to retrieve properties through blocks with method calls" do
92
+ subject["first_name"] = "Frodo"
93
+ value = nil
94
+ subject.first_name { |v| value = v }
95
+ value.should == "Frodo"
96
+ end
97
+ end
98
+
99
+ describe '.new' do
100
+ it 'fails with non-existent properties' do
101
+ lambda { described_class.new(:bork => '') }.should raise_error(NoMethodError)
102
+ end
103
+
104
+ it 'should set properties that it is able to' do
105
+ obj = described_class.new :first_name => 'Michael'
106
+ obj.first_name.should == 'Michael'
107
+ end
108
+
109
+ it 'accepts nil' do
110
+ lambda { DashNoRequiredTest.new(nil) }.should_not raise_error
111
+ end
112
+
113
+ it 'accepts block to define a global default' do
114
+ obj = described_class.new { |hash, key| key.to_s.upcase }
115
+ obj.first_name.should == 'FIRST_NAME'
116
+ obj.count.should be_zero
117
+ end
118
+
119
+ it "fails when required values are missing" do
120
+ expect { DashTest.new }.to raise_error(ArgumentError)
121
+ end
122
+
123
+ end
124
+
125
+ describe 'properties' do
126
+ it 'lists defined properties' do
127
+ described_class.properties.should == Set.new([:first_name, :email, :count])
128
+ end
129
+
130
+ it 'checks if a property exists' do
131
+ described_class.property?('first_name').should be_true
132
+ described_class.property?(:first_name).should be_true
133
+ end
134
+
135
+ it 'checks if a property is required' do
136
+ described_class.required?('first_name').should be_true
137
+ described_class.required?(:first_name).should be_true
138
+ end
139
+
140
+ it 'doesnt include property from subclass' do
141
+ described_class.property?(:last_name).should be_false
142
+ end
143
+
144
+ it 'lists declared defaults' do
145
+ described_class.defaults.should == { :count => 0 }
146
+ end
147
+ end
148
+ end
149
+
150
+ describe Wahashie::Dash, 'inheritance' do
151
+ before do
152
+ @top = Class.new(Wahashie::Dash)
153
+ @middle = Class.new(@top)
154
+ @bottom = Class.new(@middle)
155
+ end
156
+
157
+ it 'reports empty properties when nothing defined' do
158
+ @top.properties.should be_empty
159
+ @top.defaults.should be_empty
160
+ end
161
+
162
+ it 'inherits properties downwards' do
163
+ @top.property :echo
164
+ @middle.properties.should include(:echo)
165
+ @bottom.properties.should include(:echo)
166
+ end
167
+
168
+ it 'doesnt inherit properties upwards' do
169
+ @middle.property :echo
170
+ @top.properties.should_not include(:echo)
171
+ @bottom.properties.should include(:echo)
172
+ end
173
+
174
+ it 'allows overriding a default on an existing property' do
175
+ @top.property :echo
176
+ @middle.property :echo, :default => 123
177
+ @bottom.properties.to_a.should == [:echo]
178
+ @bottom.new.echo.should == 123
179
+ end
180
+
181
+ it 'allows clearing an existing default' do
182
+ @top.property :echo
183
+ @middle.property :echo, :default => 123
184
+ @bottom.property :echo
185
+ @bottom.properties.to_a.should == [:echo]
186
+ @bottom.new.echo.should be_nil
187
+ end
188
+
189
+ it 'should allow nil defaults' do
190
+ @bottom.property :echo, :default => nil
191
+ @bottom.new.should have_key('echo')
192
+ end
193
+
194
+ end
195
+
196
+ describe Subclassed do
197
+
198
+ subject { Subclassed.new(:first_name => 'Bob', :last_name => 'McNob', :email => 'bob@example.com') }
199
+
200
+ its(:count) { should be_zero }
201
+
202
+ it { should respond_to(:first_name) }
203
+ it { should respond_to(:first_name=) }
204
+ it { should respond_to(:last_name) }
205
+ it { should respond_to(:last_name=) }
206
+
207
+ it 'has one additional property' do
208
+ described_class.property?(:last_name).should be_true
209
+ end
210
+
211
+ it "didn't override superclass inheritance logic" do
212
+ described_class.instance_variable_get('@inheritance_test').should be_true
213
+ end
214
+
215
+ end