wahashie 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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