hashie 2.1.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +3 -2
- data/CHANGELOG.md +11 -3
- data/Gemfile +1 -1
- data/README.md +72 -79
- data/UPGRADING.md +93 -0
- data/hashie.gemspec +8 -8
- data/lib/hashie.rb +18 -11
- data/lib/hashie/dash.rb +8 -9
- data/lib/hashie/extensions/coercion.rb +1 -1
- data/lib/hashie/extensions/dash/indifferent_access.rb +21 -0
- data/lib/hashie/extensions/deep_fetch.rb +1 -1
- data/lib/hashie/extensions/ignore_undeclared.rb +5 -1
- data/lib/hashie/extensions/key_conversion.rb +0 -82
- data/lib/hashie/extensions/pretty_inspect.rb +19 -0
- data/lib/hashie/extensions/stringify_keys.rb +44 -0
- data/lib/hashie/extensions/symbolize_keys.rb +44 -0
- data/lib/hashie/hash.rb +17 -5
- data/lib/hashie/mash.rb +24 -13
- data/lib/hashie/rash.rb +1 -1
- data/lib/hashie/trash.rb +9 -10
- data/lib/hashie/version.rb +1 -1
- data/spec/hashie/dash_spec.rb +69 -24
- data/spec/hashie/extensions/dash/indifferent_access_spec.rb +58 -0
- data/spec/hashie/extensions/deep_fetch_spec.rb +27 -0
- data/spec/hashie/extensions/ignore_undeclared_spec.rb +36 -13
- data/spec/hashie/extensions/indifferent_access_spec.rb +2 -2
- data/spec/hashie/hash_spec.rb +15 -23
- data/spec/hashie/mash_spec.rb +41 -29
- data/spec/hashie/trash_spec.rb +5 -2
- data/spec/spec_helper.rb +0 -1
- metadata +20 -28
- data/lib/hashie/hash_extensions.rb +0 -47
- data/spec/spec.opts +0 -3
data/lib/hashie/rash.rb
CHANGED
@@ -111,7 +111,7 @@ module Hashie
|
|
111
111
|
|
112
112
|
def optimize_if_necessary!
|
113
113
|
if (@lookups += 1) >= @optimize_every
|
114
|
-
@regexes = @regex_counts.sort_by { |
|
114
|
+
@regexes = @regex_counts.sort_by { |_, count| -count }.map { |regex, _| regex }
|
115
115
|
@lookups = 0
|
116
116
|
end
|
117
117
|
end
|
data/lib/hashie/trash.rb
CHANGED
@@ -19,23 +19,22 @@ module Hashie
|
|
19
19
|
def self.property(property_name, options = {})
|
20
20
|
super
|
21
21
|
|
22
|
-
options[:from] = options[:from]
|
23
|
-
property_name = property_name.to_sym
|
22
|
+
options[:from] = options[:from] if options[:from]
|
24
23
|
|
25
24
|
if options[:from]
|
26
25
|
if property_name == options[:from]
|
27
26
|
fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
|
28
27
|
end
|
29
28
|
|
30
|
-
translations[options[:from]
|
29
|
+
translations[options[:from]] = property_name
|
31
30
|
|
32
31
|
define_method "#{options[:from]}=" do |val|
|
33
32
|
with = options[:with] || options[:transform_with]
|
34
|
-
self[property_name
|
33
|
+
self[property_name] = with.respond_to?(:call) ? with.call(val) : val
|
35
34
|
end
|
36
35
|
else
|
37
36
|
if options[:transform_with].respond_to? :call
|
38
|
-
transforms[property_name
|
37
|
+
transforms[property_name] = options[:transform_with]
|
39
38
|
end
|
40
39
|
end
|
41
40
|
end
|
@@ -43,10 +42,10 @@ module Hashie
|
|
43
42
|
# Set a value on the Dash in a Hash-like way. Only works
|
44
43
|
# on pre-existing properties.
|
45
44
|
def []=(property, value)
|
46
|
-
if self.class.translations.key? property
|
45
|
+
if self.class.translations.key? property
|
47
46
|
send("#{property}=", value)
|
48
|
-
elsif self.class.transforms.key? property
|
49
|
-
super property, self.class.transforms[property
|
47
|
+
elsif self.class.transforms.key? property
|
48
|
+
super property, self.class.transforms[property].call(value)
|
50
49
|
elsif property_exists? property
|
51
50
|
super
|
52
51
|
end
|
@@ -77,7 +76,7 @@ module Hashie
|
|
77
76
|
# Raises an NoMethodError if the property doesn't exist
|
78
77
|
#
|
79
78
|
def property_exists?(property)
|
80
|
-
unless self.class.property?(property
|
79
|
+
unless self.class.property?(property)
|
81
80
|
fail NoMethodError, "The property '#{property}' is not defined for this Trash."
|
82
81
|
end
|
83
82
|
true
|
@@ -89,7 +88,7 @@ module Hashie
|
|
89
88
|
def initialize_attributes(attributes)
|
90
89
|
return unless attributes
|
91
90
|
attributes_copy = attributes.dup.delete_if do |k, v|
|
92
|
-
if self.class.translations.include?(k
|
91
|
+
if self.class.translations.include?(k)
|
93
92
|
self[k] = v
|
94
93
|
true
|
95
94
|
end
|
data/lib/hashie/version.rb
CHANGED
data/spec/hashie/dash_spec.rb
CHANGED
@@ -22,7 +22,7 @@ class PropertyBangTest < Hashie::Dash
|
|
22
22
|
property :important!
|
23
23
|
end
|
24
24
|
|
25
|
-
class
|
25
|
+
class SubclassedTest < DashTest
|
26
26
|
property :last_name, required: true
|
27
27
|
end
|
28
28
|
|
@@ -35,7 +35,6 @@ class DeferredTest < Hashie::Dash
|
|
35
35
|
end
|
36
36
|
|
37
37
|
describe DashTest do
|
38
|
-
|
39
38
|
subject { DashTest.new(first_name: 'Bob', email: 'bob@example.com') }
|
40
39
|
|
41
40
|
it('subclasses Hashie::Hash') { should respond_to(:to_mash) }
|
@@ -74,7 +73,7 @@ describe DashTest do
|
|
74
73
|
end
|
75
74
|
|
76
75
|
it 'fails writing a required property to nil using []=' do
|
77
|
-
expect { subject[
|
76
|
+
expect { subject[:first_name] = nil }.to raise_error(ArgumentError)
|
78
77
|
end
|
79
78
|
|
80
79
|
it 'fails writing to a non-existent property using []=' do
|
@@ -82,9 +81,9 @@ describe DashTest do
|
|
82
81
|
end
|
83
82
|
|
84
83
|
it 'works for an existing property using []=' do
|
85
|
-
subject[
|
86
|
-
expect(subject['first_name']).to eq 'Bob'
|
84
|
+
subject[:first_name] = 'Bob'
|
87
85
|
expect(subject[:first_name]).to eq 'Bob'
|
86
|
+
expect { subject['first_name'] }.to raise_error(NoMethodError)
|
88
87
|
end
|
89
88
|
|
90
89
|
it 'works for an existing property using a method call' do
|
@@ -99,14 +98,14 @@ describe DashTest do
|
|
99
98
|
end
|
100
99
|
|
101
100
|
it 'is able to retrieve properties through blocks' do
|
102
|
-
subject[
|
101
|
+
subject[:first_name] = 'Aiden'
|
103
102
|
value = nil
|
104
|
-
subject.[](
|
103
|
+
subject.[](:first_name) { |v| value = v }
|
105
104
|
expect(value).to eq 'Aiden'
|
106
105
|
end
|
107
106
|
|
108
107
|
it 'is able to retrieve properties through blocks with method calls' do
|
109
|
-
subject[
|
108
|
+
subject[:first_name] = 'Frodo'
|
110
109
|
value = nil
|
111
110
|
subject.first_name { |v| value = v }
|
112
111
|
expect(value).to eq 'Frodo'
|
@@ -115,12 +114,12 @@ describe DashTest do
|
|
115
114
|
|
116
115
|
context 'reading from deferred properties' do
|
117
116
|
it 'evaluates proc after initial read' do
|
118
|
-
expect(DeferredTest.new[
|
117
|
+
expect(DeferredTest.new[:created_at]).to be_instance_of(Time)
|
119
118
|
end
|
120
119
|
|
121
120
|
it 'does not evalute proc after subsequent reads' do
|
122
121
|
deferred = DeferredTest.new
|
123
|
-
expect(deferred[
|
122
|
+
expect(deferred[:created_at].object_id).to eq deferred[:created_at].object_id
|
124
123
|
end
|
125
124
|
end
|
126
125
|
|
@@ -139,7 +138,7 @@ describe DashTest do
|
|
139
138
|
end
|
140
139
|
|
141
140
|
it 'accepts block to define a global default' do
|
142
|
-
obj = described_class.new { |
|
141
|
+
obj = described_class.new { |_, key| key.to_s.upcase }
|
143
142
|
expect(obj.first_name).to eq 'FIRST_NAME'
|
144
143
|
expect(obj.count).to be_zero
|
145
144
|
end
|
@@ -220,17 +219,17 @@ describe DashTest do
|
|
220
219
|
end
|
221
220
|
|
222
221
|
it 'checks if a property exists' do
|
223
|
-
expect(described_class.property?(
|
224
|
-
expect(described_class.property?(
|
222
|
+
expect(described_class.property?(:first_name)).to be_truthy
|
223
|
+
expect(described_class.property?('first_name')).to be_falsy
|
225
224
|
end
|
226
225
|
|
227
226
|
it 'checks if a property is required' do
|
228
|
-
expect(described_class.required?(
|
229
|
-
expect(described_class.required?(
|
227
|
+
expect(described_class.required?(:first_name)).to be_truthy
|
228
|
+
expect(described_class.required?('first_name')).to be_falsy
|
230
229
|
end
|
231
230
|
|
232
231
|
it 'doesnt include property from subclass' do
|
233
|
-
expect(described_class.property?(:last_name)).to
|
232
|
+
expect(described_class.property?(:last_name)).to be_falsy
|
234
233
|
end
|
235
234
|
|
236
235
|
it 'lists declared defaults' do
|
@@ -238,7 +237,7 @@ describe DashTest do
|
|
238
237
|
end
|
239
238
|
|
240
239
|
it 'allows properties that end in bang' do
|
241
|
-
expect(PropertyBangTest.property?(:important!)).to
|
240
|
+
expect(PropertyBangTest.property?(:important!)).to be_truthy
|
242
241
|
end
|
243
242
|
end
|
244
243
|
|
@@ -246,7 +245,7 @@ describe DashTest do
|
|
246
245
|
before { subject.replace(first_name: 'Cain') }
|
247
246
|
|
248
247
|
it 'return self' do
|
249
|
-
expect(subject.replace(email: 'bar').to_hash).to eq(
|
248
|
+
expect(subject.replace(email: 'bar').to_hash).to eq(email: 'bar', count: 0)
|
250
249
|
end
|
251
250
|
|
252
251
|
it 'sets all specified keys to their corresponding values' do
|
@@ -254,7 +253,7 @@ describe DashTest do
|
|
254
253
|
end
|
255
254
|
|
256
255
|
it 'leaves only specified keys and keys with default values' do
|
257
|
-
expect(subject.keys.
|
256
|
+
expect(subject.keys.sort_by { |key| key.to_s }).to eq [:count, :first_name]
|
258
257
|
expect(subject.email).to be_nil
|
259
258
|
expect(subject.count).to eq 0
|
260
259
|
end
|
@@ -310,13 +309,14 @@ describe Hashie::Dash, 'inheritance' do
|
|
310
309
|
|
311
310
|
it 'allows nil defaults' do
|
312
311
|
@bottom.property :echo, default: nil
|
313
|
-
expect(@bottom.new).to have_key(
|
312
|
+
expect(@bottom.new).to have_key(:echo)
|
313
|
+
expect(@bottom.new).to_not have_key('echo')
|
314
314
|
end
|
315
315
|
|
316
316
|
end
|
317
317
|
|
318
|
-
describe
|
319
|
-
subject {
|
318
|
+
describe SubclassedTest do
|
319
|
+
subject { SubclassedTest.new(first_name: 'Bob', last_name: 'McNob', email: 'bob@example.com') }
|
320
320
|
|
321
321
|
describe '#count' do
|
322
322
|
subject { super().count }
|
@@ -329,10 +329,55 @@ describe Subclassed do
|
|
329
329
|
it { should respond_to(:last_name=) }
|
330
330
|
|
331
331
|
it 'has one additional property' do
|
332
|
-
expect(described_class.property?(:last_name)).to
|
332
|
+
expect(described_class.property?(:last_name)).to be_truthy
|
333
333
|
end
|
334
334
|
|
335
335
|
it "didn't override superclass inheritance logic" do
|
336
|
-
expect(described_class.instance_variable_get('@inheritance_test')).to
|
336
|
+
expect(described_class.instance_variable_get('@inheritance_test')).to be_truthy
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
class MixedPropertiesTest < Hashie::Dash
|
341
|
+
property :symbol
|
342
|
+
property 'string'
|
343
|
+
end
|
344
|
+
|
345
|
+
describe MixedPropertiesTest do
|
346
|
+
subject { MixedPropertiesTest.new('string' => 'string', symbol: 'symbol') }
|
347
|
+
|
348
|
+
it { should respond_to('string') }
|
349
|
+
it { should respond_to(:symbol) }
|
350
|
+
|
351
|
+
it 'property?' do
|
352
|
+
expect(described_class.property?('string')).to be_truthy
|
353
|
+
expect(described_class.property?(:symbol)).to be_truthy
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'fetch' do
|
357
|
+
expect(subject['string']).to eq('string')
|
358
|
+
expect { subject[:string] }.to raise_error(NoMethodError)
|
359
|
+
expect(subject[:symbol]).to eq('symbol')
|
360
|
+
expect { subject['symbol'] }.to raise_error(NoMethodError)
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'double define' do
|
364
|
+
klass = Class.new(MixedPropertiesTest) do
|
365
|
+
property 'symbol'
|
366
|
+
end
|
367
|
+
instance = klass.new(symbol: 'one', 'symbol' => 'two')
|
368
|
+
expect(instance[:symbol]).to eq('one')
|
369
|
+
expect(instance['symbol']).to eq('two')
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'assign' do
|
373
|
+
subject['string'] = 'updated'
|
374
|
+
expect(subject['string']).to eq('updated')
|
375
|
+
|
376
|
+
expect { subject[:string] = 'updated' }.to raise_error(NoMethodError)
|
377
|
+
|
378
|
+
subject[:symbol] = 'updated'
|
379
|
+
expect(subject[:symbol]).to eq('updated')
|
380
|
+
|
381
|
+
expect { subject['symbol'] = 'updated' }.to raise_error(NoMethodError)
|
337
382
|
end
|
338
383
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hashie::Extensions::Dash::IndifferentAccess do
|
4
|
+
class DashWithIndifferentAccess < Hashie::Dash
|
5
|
+
include Hashie::Extensions::Dash::IndifferentAccess
|
6
|
+
property :name
|
7
|
+
end
|
8
|
+
|
9
|
+
context 'initialized with' do
|
10
|
+
it 'string' do
|
11
|
+
instance = DashWithIndifferentAccess.new('name' => 'Name')
|
12
|
+
expect(instance.name).to eq('Name')
|
13
|
+
expect(instance['name']).to eq('Name')
|
14
|
+
expect(instance[:name]).to eq('Name')
|
15
|
+
expect(instance.inspect).to eq('#<DashWithIndifferentAccess name="Name">')
|
16
|
+
expect(instance.to_hash).to eq('name' => 'Name')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'key' do
|
20
|
+
instance = DashWithIndifferentAccess.new(name: 'Name')
|
21
|
+
expect(instance.name).to eq('Name')
|
22
|
+
expect(instance['name']).to eq('Name')
|
23
|
+
expect(instance[:name]).to eq('Name')
|
24
|
+
expect(instance.inspect).to eq('#<DashWithIndifferentAccess name="Name">')
|
25
|
+
expect(instance.to_hash).to eq('name' => 'Name')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'updates' do
|
30
|
+
instance = DashWithIndifferentAccess.new
|
31
|
+
instance['name'] = 'Updated String'
|
32
|
+
expect(instance.name).to eq('Updated String')
|
33
|
+
instance[:name] = 'Updated Symbol'
|
34
|
+
expect(instance.name).to eq('Updated Symbol')
|
35
|
+
instance.name = 'Updated Method'
|
36
|
+
expect(instance.name).to eq('Updated Method')
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'initialized with both prefers last assignment' do
|
40
|
+
it 'string, then symbol' do
|
41
|
+
instance = DashWithIndifferentAccess.new('name' => 'First', name: 'Last')
|
42
|
+
expect(instance.name).to eq('Last')
|
43
|
+
expect(instance['name']).to eq('Last')
|
44
|
+
expect(instance[:name]).to eq('Last')
|
45
|
+
expect(instance.inspect).to eq('#<DashWithIndifferentAccess name="Last">')
|
46
|
+
expect(instance.to_hash).to eq('name' => 'Last')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'symbol then string' do
|
50
|
+
instance = DashWithIndifferentAccess.new(name: 'Last', 'name' => 'First')
|
51
|
+
expect(instance.name).to eq('First')
|
52
|
+
expect(instance['name']).to eq('First')
|
53
|
+
expect(instance[:name]).to eq('First')
|
54
|
+
expect(instance.inspect).to eq('#<DashWithIndifferentAccess name="First">')
|
55
|
+
expect(instance.to_hash).to eq('name' => 'First')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -11,6 +11,7 @@ module Hashie
|
|
11
11
|
{ title: 'Call of the Wild' },
|
12
12
|
{ title: 'Moby Dick' }
|
13
13
|
],
|
14
|
+
shelves: nil,
|
14
15
|
location: {
|
15
16
|
address: '123 Library St.'
|
16
17
|
}
|
@@ -62,6 +63,32 @@ module Hashie
|
|
62
63
|
)
|
63
64
|
end
|
64
65
|
end
|
66
|
+
|
67
|
+
context 'when the nested object is missing' do
|
68
|
+
it 'raises an UndefinedPathError' do
|
69
|
+
expect do
|
70
|
+
instance.deep_fetch(:library, :unknown_key, :books)
|
71
|
+
end.to(
|
72
|
+
raise_error(
|
73
|
+
DeepFetch::UndefinedPathError,
|
74
|
+
'Could not fetch path (library > unknown_key > books) at unknown_key'
|
75
|
+
)
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'when the nested object is nil' do
|
81
|
+
it 'raises an UndefinedPathError' do
|
82
|
+
expect do
|
83
|
+
instance.deep_fetch(:library, :shelves, :address)
|
84
|
+
end.to(
|
85
|
+
raise_error(
|
86
|
+
DeepFetch::UndefinedPathError,
|
87
|
+
'Could not fetch path (library > shelves > address) at address'
|
88
|
+
)
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
65
92
|
end
|
66
93
|
end
|
67
94
|
end
|
@@ -1,23 +1,46 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Hashie::Extensions::IgnoreUndeclared do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
context 'included in Trash' do
|
5
|
+
class ForgivingTrash < Hashie::Trash
|
6
|
+
include Hashie::Extensions::IgnoreUndeclared
|
7
|
+
property :city
|
8
|
+
property :state, from: :provence
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
+
subject { ForgivingTrash }
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
it 'silently ignores undeclared properties on initialization' do
|
14
|
+
expect { subject.new(city: 'Toronto', provence: 'ON', country: 'Canada') }.to_not raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'works with translated properties (with symbol keys)' do
|
18
|
+
expect(subject.new(provence: 'Ontario').state).to eq('Ontario')
|
19
|
+
end
|
15
20
|
|
16
|
-
|
17
|
-
|
21
|
+
it 'works with translated properties (with string keys)' do
|
22
|
+
expect(subject.new(provence: 'Ontario').state).to eq('Ontario')
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'requires properties to be declared on assignment' do
|
26
|
+
hash = subject.new(city: 'Toronto')
|
27
|
+
expect { hash.country = 'Canada' }.to raise_error(NoMethodError)
|
28
|
+
end
|
18
29
|
end
|
19
30
|
|
20
|
-
|
21
|
-
|
31
|
+
context 'combined with DeepMerge' do
|
32
|
+
class ForgivingTrashWithMerge < Hashie::Trash
|
33
|
+
include Hashie::Extensions::DeepMerge
|
34
|
+
include Hashie::Extensions::IgnoreUndeclared
|
35
|
+
property :some_key
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'deep merges' do
|
39
|
+
class ForgivingTrashWithMergeAndProperty < ForgivingTrashWithMerge
|
40
|
+
property :some_other_key
|
41
|
+
end
|
42
|
+
hash = ForgivingTrashWithMergeAndProperty.new(some_ignored_key: 17, some_key: 12)
|
43
|
+
expect(hash.deep_merge(some_other_key: 55, some_ignored_key: 18)).to eq(some_key: 12, some_other_key: 55)
|
44
|
+
end
|
22
45
|
end
|
23
46
|
end
|
@@ -103,12 +103,12 @@ describe Hashie::Extensions::IndifferentAccess do
|
|
103
103
|
it 'removes old keys' do
|
104
104
|
[:foo, 'foo'].each do |k|
|
105
105
|
expect(h[k]).to be_nil
|
106
|
-
expect(h.key?(k)).to
|
106
|
+
expect(h.key?(k)).to be_falsy
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
110
|
it 'creates new keys with indifferent access' do
|
111
|
-
[:bar, 'bar', :hi, 'hi'].each { |k| expect(h.key?(k)).to
|
111
|
+
[:bar, 'bar', :hi, 'hi'].each { |k| expect(h.key?(k)).to be_truthy }
|
112
112
|
expect(h[:bar]).to eq 'baz'
|
113
113
|
expect(h['bar']).to eq 'baz'
|
114
114
|
expect(h[:hi]).to eq 'bye'
|