hashie 1.2.0 → 2.0.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.
- data/.gitignore +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +27 -0
- data/Guardfile +1 -1
- data/README.markdown +240 -0
- data/VERSION +1 -0
- data/hashie.gemspec +4 -2
- data/lib/hashie.rb +20 -6
- data/lib/hashie/dash.rb +21 -8
- data/lib/hashie/extensions/coercion.rb +105 -0
- data/lib/hashie/extensions/deep_merge.rb +21 -0
- data/lib/hashie/extensions/indifferent_access.rb +114 -0
- data/lib/hashie/extensions/key_conversion.rb +92 -0
- data/lib/hashie/extensions/merge_initializer.rb +26 -0
- data/lib/hashie/extensions/method_access.rb +124 -0
- data/lib/hashie/extensions/structure.rb +47 -0
- data/lib/hashie/hash.rb +12 -6
- data/lib/hashie/mash.rb +48 -17
- data/lib/hashie/trash.rb +41 -5
- data/lib/hashie/version.rb +1 -1
- data/spec/hashie/dash_spec.rb +25 -0
- data/spec/hashie/extensions/coercion_spec.rb +89 -0
- data/spec/hashie/extensions/deep_merge_spec.rb +20 -0
- data/spec/hashie/extensions/indifferent_access_spec.rb +74 -0
- data/spec/hashie/extensions/key_conversion_spec.rb +102 -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 +2 -12
- data/spec/hashie/mash_spec.rb +105 -17
- data/spec/hashie/trash_spec.rb +75 -0
- metadata +72 -24
- data/Gemfile.lock +0 -34
- data/README.rdoc +0 -120
data/lib/hashie/mash.rb
CHANGED
@@ -14,6 +14,7 @@ module Hashie
|
|
14
14
|
# * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
|
15
15
|
# * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
|
16
16
|
# * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
|
17
|
+
# * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key. Used to test existance in deep Mashes.
|
17
18
|
#
|
18
19
|
# == Basic Example
|
19
20
|
#
|
@@ -42,7 +43,18 @@ module Hashie
|
|
42
43
|
# mash.author!.name = "Michael Bleigh"
|
43
44
|
# mash.author # => <Mash name="Michael Bleigh">
|
44
45
|
#
|
45
|
-
|
46
|
+
# == Under Bang Example
|
47
|
+
#
|
48
|
+
# mash = Mash.new
|
49
|
+
# mash.author # => nil
|
50
|
+
# mash.author_ # => <Mash>
|
51
|
+
# mash.author_.name # => nil
|
52
|
+
#
|
53
|
+
# mash = Mash.new
|
54
|
+
# mash.author_.name = "Michael Bleigh" (assigned to temp object)
|
55
|
+
# mash.author # => <Mash>
|
56
|
+
#
|
57
|
+
class Mash < Hash
|
46
58
|
include Hashie::PrettyInspect
|
47
59
|
alias_method :to_s, :inspect
|
48
60
|
|
@@ -55,20 +67,18 @@ module Hashie
|
|
55
67
|
default ? super(default) : super(&blk)
|
56
68
|
end
|
57
69
|
|
58
|
-
class << self
|
59
|
-
alias [] new
|
60
|
-
|
61
|
-
def subkey_class
|
62
|
-
self
|
63
|
-
end
|
64
|
-
end
|
70
|
+
class << self; alias [] new; end
|
65
71
|
|
66
72
|
def id #:nodoc:
|
67
|
-
|
73
|
+
self["id"]
|
68
74
|
end
|
69
75
|
|
70
76
|
def type #:nodoc:
|
71
|
-
|
77
|
+
self["type"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def object_id #:nodoc:
|
81
|
+
self["object_id"]
|
72
82
|
end
|
73
83
|
|
74
84
|
alias_method :regular_reader, :[]
|
@@ -97,6 +107,21 @@ module Hashie
|
|
97
107
|
regular_reader(ck)
|
98
108
|
end
|
99
109
|
|
110
|
+
# This is the under bang method reader, it will return a temporary new Mash
|
111
|
+
# if there isn't a value already assigned to the key requested.
|
112
|
+
def underbang_reader(key)
|
113
|
+
ck = convert_key(key)
|
114
|
+
if key?(ck)
|
115
|
+
regular_reader(ck)
|
116
|
+
else
|
117
|
+
self.class.new
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def fetch(key, default_value = nil)
|
122
|
+
self[key] || block_given? && yield(key) || default_value || super(key)
|
123
|
+
end
|
124
|
+
|
100
125
|
def delete(key)
|
101
126
|
super(convert_key(key))
|
102
127
|
end
|
@@ -116,20 +141,22 @@ module Hashie
|
|
116
141
|
|
117
142
|
# Performs a deep_update on a duplicate of the
|
118
143
|
# current mash.
|
119
|
-
def deep_merge(other_hash)
|
120
|
-
dup.deep_update(other_hash)
|
144
|
+
def deep_merge(other_hash, &blk)
|
145
|
+
dup.deep_update(other_hash, &blk)
|
121
146
|
end
|
122
147
|
alias_method :merge, :deep_merge
|
123
148
|
|
124
149
|
# Recursively merges this mash with the passed
|
125
150
|
# in hash, merging each hash in the hierarchy.
|
126
|
-
def deep_update(other_hash)
|
151
|
+
def deep_update(other_hash, &blk)
|
127
152
|
other_hash.each_pair do |k,v|
|
128
153
|
key = convert_key(k)
|
129
154
|
if regular_reader(key).is_a?(Mash) and v.is_a?(::Hash)
|
130
|
-
regular_reader(key).deep_update(v)
|
155
|
+
regular_reader(key).deep_update(v, &blk)
|
131
156
|
else
|
132
|
-
|
157
|
+
value = convert_value(v, true)
|
158
|
+
value = blk.call(key, self[k], value) if blk
|
159
|
+
regular_writer(key, value)
|
133
160
|
end
|
134
161
|
end
|
135
162
|
self
|
@@ -161,7 +188,7 @@ module Hashie
|
|
161
188
|
|
162
189
|
def method_missing(method_name, *args, &blk)
|
163
190
|
return self.[](method_name, &blk) if key?(method_name)
|
164
|
-
match = method_name.to_s.match(/(.*?)([?=!]?)$/)
|
191
|
+
match = method_name.to_s.match(/(.*?)([?=!_]?)$/)
|
165
192
|
case match[2]
|
166
193
|
when "="
|
167
194
|
self[match[1]] = args.first
|
@@ -169,6 +196,8 @@ module Hashie
|
|
169
196
|
!!self[match[1]]
|
170
197
|
when "!"
|
171
198
|
initializing_reader(match[1])
|
199
|
+
when "_"
|
200
|
+
underbang_reader(match[1])
|
172
201
|
else
|
173
202
|
default(method_name, *args, &blk)
|
174
203
|
end
|
@@ -184,9 +213,11 @@ module Hashie
|
|
184
213
|
case val
|
185
214
|
when self.class
|
186
215
|
val.dup
|
216
|
+
when Hash
|
217
|
+
duping ? val.dup : val
|
187
218
|
when ::Hash
|
188
219
|
val = val.dup if duping
|
189
|
-
self.class.
|
220
|
+
self.class.new(val)
|
190
221
|
when Array
|
191
222
|
val.collect{ |e| convert_value(e) }
|
192
223
|
else
|
data/lib/hashie/trash.rb
CHANGED
@@ -7,23 +7,39 @@ module Hashie
|
|
7
7
|
# Trashes are useful when you need to read data from another application,
|
8
8
|
# such as a Java api, where the keys are named differently from how we would
|
9
9
|
# in Ruby.
|
10
|
-
class Trash <
|
10
|
+
class Trash < Dash
|
11
11
|
|
12
12
|
# Defines a property on the Trash. Options are as follows:
|
13
13
|
#
|
14
14
|
# * <tt>:default</tt> - Specify a default value for this property, to be
|
15
15
|
# returned before a value is set on the property in a new Dash.
|
16
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.
|
17
20
|
def self.property(property_name, options = {})
|
18
21
|
super
|
19
22
|
|
20
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
|
21
27
|
translations << options[:from].to_sym
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
25
33
|
end
|
26
|
-
|
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]
|
27
43
|
end
|
28
44
|
end
|
29
45
|
|
@@ -32,6 +48,8 @@ module Hashie
|
|
32
48
|
def []=(property, value)
|
33
49
|
if self.class.translations.include? property.to_sym
|
34
50
|
send("#{property}=", value)
|
51
|
+
elsif self.class.transforms.key? property.to_sym
|
52
|
+
super property, self.class.transforms[property.to_sym].call(value)
|
35
53
|
elsif property_exists? property
|
36
54
|
super
|
37
55
|
end
|
@@ -43,6 +61,10 @@ module Hashie
|
|
43
61
|
@translations ||= []
|
44
62
|
end
|
45
63
|
|
64
|
+
def self.transforms
|
65
|
+
@transforms ||= {}
|
66
|
+
end
|
67
|
+
|
46
68
|
# Raises an NoMethodError if the property doesn't exist
|
47
69
|
#
|
48
70
|
def property_exists?(property)
|
@@ -51,5 +73,19 @@ module Hashie
|
|
51
73
|
end
|
52
74
|
true
|
53
75
|
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Deletes any keys that have a translation
|
80
|
+
def initialize_attributes(attributes)
|
81
|
+
return unless attributes
|
82
|
+
attributes_copy = attributes.dup.delete_if do |k,v|
|
83
|
+
if self.class.translations.include?(k.to_sym)
|
84
|
+
self[k] = v
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
super attributes_copy
|
89
|
+
end
|
54
90
|
end
|
55
91
|
end
|
data/lib/hashie/version.rb
CHANGED
data/spec/hashie/dash_spec.rb
CHANGED
@@ -22,6 +22,14 @@ class Subclassed < DashTest
|
|
22
22
|
property :last_name, :required => true
|
23
23
|
end
|
24
24
|
|
25
|
+
class DashDefaultTest < Hashie::Dash
|
26
|
+
property :aliases, :default => ["Snake"]
|
27
|
+
end
|
28
|
+
|
29
|
+
class DeferredTest < Hashie::Dash
|
30
|
+
property :created_at, :default => Proc.new { Time.now }
|
31
|
+
end
|
32
|
+
|
25
33
|
describe DashTest do
|
26
34
|
|
27
35
|
subject { DashTest.new(:first_name => 'Bob', :email => 'bob@example.com') }
|
@@ -96,6 +104,17 @@ describe DashTest do
|
|
96
104
|
end
|
97
105
|
end
|
98
106
|
|
107
|
+
context 'reading from deferred properties' do
|
108
|
+
it 'should evaluate proc after initial read' do
|
109
|
+
DeferredTest.new['created_at'].should be_instance_of(Time)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should not evalute proc after subsequent reads" do
|
113
|
+
deferred = DeferredTest.new
|
114
|
+
deferred['created_at'].object_id.should == deferred['created_at'].object_id
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
99
118
|
describe '.new' do
|
100
119
|
it 'fails with non-existent properties' do
|
101
120
|
lambda { described_class.new(:bork => '') }.should raise_error(NoMethodError)
|
@@ -120,6 +139,12 @@ describe DashTest do
|
|
120
139
|
expect { DashTest.new }.to raise_error(ArgumentError)
|
121
140
|
end
|
122
141
|
|
142
|
+
it "does not overwrite default values" do
|
143
|
+
obj1 = DashDefaultTest.new
|
144
|
+
obj1.aliases << "El Rey"
|
145
|
+
obj2 = DashDefaultTest.new
|
146
|
+
obj2.aliases.should_not include "El Rey"
|
147
|
+
end
|
123
148
|
end
|
124
149
|
|
125
150
|
describe 'properties' do
|
@@ -0,0 +1,89 @@
|
|
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
|
21
|
+
include Hashie::Extensions::Coercion
|
22
|
+
include Hashie::Extensions::MergeInitializer
|
23
|
+
end
|
24
|
+
end
|
25
|
+
subject { ExampleCoercableHash }
|
26
|
+
let(:instance){ subject.new }
|
27
|
+
|
28
|
+
describe '.coerce_key' do
|
29
|
+
it { subject.should be_respond_to(:coerce_key) }
|
30
|
+
|
31
|
+
it 'should run through coerce on a specified key' do
|
32
|
+
subject.coerce_key :foo, Coercable
|
33
|
+
|
34
|
+
instance[:foo] = "bar"
|
35
|
+
instance[:foo].should be_coerced
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should support an array of keys" do
|
39
|
+
subject.coerce_keys :foo, :bar, Coercable
|
40
|
+
|
41
|
+
instance[:foo] = "bar"
|
42
|
+
instance[:bar] = "bax"
|
43
|
+
instance[:foo].should be_coerced
|
44
|
+
instance[:bar].should be_coerced
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should just call #new if no coerce method is available' do
|
48
|
+
subject.coerce_key :foo, Initializable
|
49
|
+
|
50
|
+
instance[:foo] = "bar"
|
51
|
+
instance[:foo].value.should == "String"
|
52
|
+
instance[:foo].should_not be_coerced
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should coerce when the merge initializer is used" do
|
56
|
+
subject.coerce_key :foo, Coercable
|
57
|
+
instance = subject.new(:foo => "bar")
|
58
|
+
|
59
|
+
instance[:foo].should be_coerced
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.coerce_value' do
|
64
|
+
context 'with :strict => true' do
|
65
|
+
it 'should coerce any value of the exact right class' do
|
66
|
+
subject.coerce_value String, Coercable
|
67
|
+
|
68
|
+
instance[:foo] = "bar"
|
69
|
+
instance[:bar] = "bax"
|
70
|
+
instance[:foo].should be_coerced
|
71
|
+
instance[:bar].should be_coerced
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should not coerce superclasses' do
|
75
|
+
klass = Class.new(String)
|
76
|
+
subject.coerce_value klass, Coercable
|
77
|
+
|
78
|
+
instance[:foo] = "bar"
|
79
|
+
instance[:foo].should_not be_kind_of(Coercable)
|
80
|
+
instance[:foo] = klass.new
|
81
|
+
instance[:foo].should be_kind_of(Coercable)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
after(:each) do
|
87
|
+
Object.send(:remove_const, :ExampleCoercableHash)
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hashie::Extensions::DeepMerge do
|
4
|
+
class DeepMergeHash < Hash; include Hashie::Extensions::DeepMerge end
|
5
|
+
|
6
|
+
subject{ DeepMergeHash }
|
7
|
+
|
8
|
+
let(:h1) { subject.new.merge(:a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } }) }
|
9
|
+
let(:h2) { { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } } }
|
10
|
+
let(:expected_hash) { { :a => 1, :b => "b", :c => { :c1 => 2, :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } } }
|
11
|
+
|
12
|
+
it 'should deep merge two hashes' do
|
13
|
+
h1.deep_merge(h2).should == expected_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should deep merge two hashes with bang method' do
|
17
|
+
h1.deep_merge!(h2)
|
18
|
+
h1.should == expected_hash
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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
|
+
let(:h) { subject.new(:foo => 'bar') }
|
42
|
+
|
43
|
+
it 'should find it indifferently' do
|
44
|
+
h.should be_key(:foo)
|
45
|
+
h.should be_key('foo')
|
46
|
+
end
|
47
|
+
|
48
|
+
%w(include? member? has_key?).each do |key_alias|
|
49
|
+
it "should be aliased as #{key_alias}" do
|
50
|
+
h.send(key_alias.to_sym, :foo).should be(true)
|
51
|
+
h.send(key_alias.to_sym, 'foo').should be(true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#update' do
|
57
|
+
subject{ IndifferentHash.new(:foo => 'bar') }
|
58
|
+
it 'should allow keys to be indifferent still' do
|
59
|
+
subject.update(:baz => 'qux')
|
60
|
+
subject['foo'].should == 'bar'
|
61
|
+
subject['baz'].should == 'qux'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should recursively inject indifference into sub-hashes' do
|
65
|
+
subject.update(:baz => {:qux => 'abc'})
|
66
|
+
subject['baz']['qux'].should == 'abc'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should not change the ancestors of the injected object class' do
|
70
|
+
subject.update(:baz => {:qux => 'abc'})
|
71
|
+
Hash.new.should_not be_respond_to(:indifferent_access?)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|