hashie-pre 2.0.0.beta
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.
- data/.document +5 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +36 -0
- data/Guardfile +5 -0
- data/LICENSE +20 -0
- data/README.markdown +232 -0
- data/Rakefile +13 -0
- data/VERSION +1 -0
- data/hashie.gemspec +22 -0
- data/lib/hashie.rb +23 -0
- data/lib/hashie/clash.rb +86 -0
- data/lib/hashie/dash.rb +150 -0
- data/lib/hashie/extensions/coercion.rb +101 -0
- data/lib/hashie/extensions/deep_merge.rb +7 -0
- data/lib/hashie/extensions/indifferent_access.rb +110 -0
- data/lib/hashie/extensions/key_conversion.rb +52 -0
- data/lib/hashie/extensions/merge_initializer.rb +24 -0
- data/lib/hashie/extensions/method_access.rb +124 -0
- data/lib/hashie/extensions/structure.rb +47 -0
- data/lib/hashie/hash.rb +31 -0
- data/lib/hashie/hash_extensions.rb +49 -0
- data/lib/hashie/mash.rb +216 -0
- data/lib/hashie/trash.rb +77 -0
- data/lib/hashie/version.rb +3 -0
- data/spec/hashie/clash_spec.rb +42 -0
- data/spec/hashie/dash_spec.rb +215 -0
- data/spec/hashie/extensions/coercion_spec.rb +70 -0
- data/spec/hashie/extensions/indifferent_access_spec.rb +66 -0
- data/spec/hashie/extensions/key_conversion_spec.rb +66 -0
- data/spec/hashie/extensions/merge_initializer_spec.rb +20 -0
- data/spec/hashie/extensions/method_access_spec.rb +112 -0
- data/spec/hashie/hash_spec.rb +22 -0
- data/spec/hashie/mash_spec.rb +305 -0
- data/spec/hashie/trash_spec.rb +139 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +12 -0
- metadata +181 -0
data/lib/hashie/trash.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'hashie/dash'
|
2
|
+
|
3
|
+
module Hashie
|
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 < Hashie::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
|
+
# * <tt>:with</tt> - Specify a lambda to be used to convert value.
|
18
|
+
# * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
|
19
|
+
# without using the :from option. It transform the property itself.
|
20
|
+
def self.property(property_name, options = {})
|
21
|
+
super
|
22
|
+
|
23
|
+
if options[:from]
|
24
|
+
if property_name.to_sym == options[:from].to_sym
|
25
|
+
raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
|
26
|
+
end
|
27
|
+
translations << options[:from].to_sym
|
28
|
+
if options[:with].respond_to? :call
|
29
|
+
class_eval do
|
30
|
+
define_method "#{options[:from]}=" do |val|
|
31
|
+
self[property_name.to_sym] = options[:with].call(val)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else
|
35
|
+
class_eval <<-RUBY
|
36
|
+
def #{options[:from]}=(val)
|
37
|
+
self[:#{property_name}] = val
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
end
|
41
|
+
elsif options[:transform_with].respond_to? :call
|
42
|
+
transforms[property_name.to_sym] = options[:transform_with]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set a value on the Dash in a Hash-like way. Only works
|
47
|
+
# on pre-existing properties.
|
48
|
+
def []=(property, value)
|
49
|
+
if self.class.translations.include? property.to_sym
|
50
|
+
send("#{property}=", value)
|
51
|
+
elsif self.class.transforms.key? property.to_sym
|
52
|
+
super property, self.class.transforms[property.to_sym].call(value)
|
53
|
+
elsif property_exists? property
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def self.translations
|
61
|
+
@translations ||= []
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.transforms
|
65
|
+
@transforms ||= {}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Raises an NoMethodError if the property doesn't exist
|
69
|
+
#
|
70
|
+
def property_exists?(property)
|
71
|
+
unless self.class.property?(property.to_sym)
|
72
|
+
raise NoMethodError, "The property '#{property}' is not defined for this Trash."
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hashie::Clash do
|
4
|
+
before do
|
5
|
+
@c = Hashie::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(Hashie::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
|
+
Hashie::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 < Hashie::Dash
|
10
|
+
property :first_name, :required => true
|
11
|
+
property :email
|
12
|
+
property :count, :default => 0
|
13
|
+
end
|
14
|
+
|
15
|
+
class DashNoRequiredTest < Hashie::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 Hashie::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 Hashie::Dash, 'inheritance' do
|
151
|
+
before do
|
152
|
+
@top = Class.new(Hashie::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
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hashie::Extensions::Coercion do
|
4
|
+
class Initializable
|
5
|
+
def initialize(obj, coerced = false)
|
6
|
+
@coerced = coerced
|
7
|
+
@value = obj.class.to_s
|
8
|
+
end
|
9
|
+
def coerced?; @coerced end
|
10
|
+
attr_reader :value
|
11
|
+
end
|
12
|
+
|
13
|
+
class Coercable < Initializable
|
14
|
+
def self.coerce(obj)
|
15
|
+
new(obj, true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
class ExampleCoercableHash < Hash; include Hashie::Extensions::Coercion end
|
21
|
+
end
|
22
|
+
subject { ExampleCoercableHash }
|
23
|
+
let(:instance){ subject.new }
|
24
|
+
|
25
|
+
describe '.coerce_key' do
|
26
|
+
it { subject.should be_respond_to(:coerce_key) }
|
27
|
+
|
28
|
+
it 'should run through coerce on a specified key' do
|
29
|
+
subject.coerce_key :foo, Coercable
|
30
|
+
|
31
|
+
instance[:foo] = "bar"
|
32
|
+
instance[:foo].should be_coerced
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should just call #new if no coerce method is available' do
|
36
|
+
subject.coerce_key :foo, Initializable
|
37
|
+
|
38
|
+
instance[:foo] = "bar"
|
39
|
+
instance[:foo].value.should == "String"
|
40
|
+
instance[:foo].should_not be_coerced
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.coerce_value' do
|
45
|
+
context 'with :strict => true' do
|
46
|
+
it 'should coerce any value of the exact right class' do
|
47
|
+
subject.coerce_value String, Coercable
|
48
|
+
|
49
|
+
instance[:foo] = "bar"
|
50
|
+
instance[:bar] = "bax"
|
51
|
+
instance[:foo].should be_coerced
|
52
|
+
instance[:bar].should be_coerced
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should not coerce superclasses' do
|
56
|
+
klass = Class.new(String)
|
57
|
+
subject.coerce_value klass, Coercable
|
58
|
+
|
59
|
+
instance[:foo] = "bar"
|
60
|
+
instance[:foo].should_not be_kind_of(Coercable)
|
61
|
+
instance[:foo] = klass.new
|
62
|
+
instance[:foo].should be_kind_of(Coercable)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
after(:each) do
|
68
|
+
Object.send(:remove_const, :ExampleCoercableHash)
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hashie::Extensions::IndifferentAccess do
|
4
|
+
class IndifferentHash < Hash
|
5
|
+
include Hashie::Extensions::MergeInitializer
|
6
|
+
include Hashie::Extensions::IndifferentAccess
|
7
|
+
end
|
8
|
+
subject{ IndifferentHash }
|
9
|
+
|
10
|
+
it 'should be able to access via string or symbol' do
|
11
|
+
h = subject.new(:abc => 123)
|
12
|
+
h[:abc].should == 123
|
13
|
+
h['abc'].should == 123
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#values_at' do
|
17
|
+
it 'should indifferently find values' do
|
18
|
+
h = subject.new(:foo => 'bar', 'baz' => 'qux')
|
19
|
+
h.values_at('foo', :baz).should == %w(bar qux)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#fetch' do
|
24
|
+
it 'should work like normal fetch, but indifferent' do
|
25
|
+
h = subject.new(:foo => 'bar')
|
26
|
+
h.fetch(:foo).should == h.fetch('foo')
|
27
|
+
h.fetch(:foo).should == 'bar'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#delete' do
|
32
|
+
it 'should delete indifferently' do
|
33
|
+
h = subject.new(:foo => 'bar', 'baz' => 'qux')
|
34
|
+
h.delete('foo')
|
35
|
+
h.delete(:baz)
|
36
|
+
h.should be_empty
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#key?' do
|
41
|
+
it 'should find it indifferently' do
|
42
|
+
h = subject.new(:foo => 'bar')
|
43
|
+
h.should be_key(:foo)
|
44
|
+
h.should be_key('foo')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#update' do
|
49
|
+
subject{ IndifferentHash.new(:foo => 'bar') }
|
50
|
+
it 'should allow keys to be indifferent still' do
|
51
|
+
subject.update(:baz => 'qux')
|
52
|
+
subject['foo'].should == 'bar'
|
53
|
+
subject['baz'].should == 'qux'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should recursively inject indifference into sub-hashes' do
|
57
|
+
subject.update(:baz => {:qux => 'abc'})
|
58
|
+
subject['baz']['qux'].should == 'abc'
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should not change the ancestors of the injected object class' do
|
62
|
+
subject.update(:baz => {:qux => 'abc'})
|
63
|
+
Hash.new.should_not be_respond_to(:indifferent_access?)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|