representable 2.0.4 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +17 -0
- data/README.md +20 -1
- data/lib/representable.rb +2 -1
- data/lib/representable/binding.rb +115 -59
- data/lib/representable/config.rb +8 -0
- data/lib/representable/definition.rb +10 -14
- data/lib/representable/deserializer.rb +64 -25
- data/lib/representable/hash.rb +3 -3
- data/lib/representable/hash/binding.rb +40 -0
- data/lib/representable/hash/collection.rb +3 -2
- data/lib/representable/hash_methods.rb +4 -2
- data/lib/representable/mapper.rb +1 -1
- data/lib/representable/populator.rb +59 -0
- data/lib/representable/serializer.rb +24 -13
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +3 -3
- data/lib/representable/xml/binding.rb +171 -0
- data/lib/representable/yaml.rb +3 -3
- data/lib/representable/yaml/binding.rb +48 -0
- data/representable.gemspec +1 -1
- data/test/benchmarking.rb +83 -0
- data/test/binding_test.rb +46 -0
- data/test/definition_test.rb +5 -58
- data/test/exec_context_test.rb +4 -4
- data/test/hash_bindings_test.rb +4 -52
- data/test/hash_test.rb +6 -6
- data/test/json_test.rb +8 -8
- data/test/lonely_test.rb +1 -1
- data/test/realistic_benchmark.rb +83 -0
- data/test/skip_test.rb +28 -0
- data/test/xml_bindings_test.rb +2 -109
- data/test/xml_test.rb +61 -23
- data/test/yaml_test.rb +5 -8
- metadata +19 -11
- data/lib/representable/bindings/hash_bindings.rb +0 -64
- data/lib/representable/bindings/xml_bindings.rb +0 -172
- data/lib/representable/bindings/yaml_bindings.rb +0 -49
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'representable/hash/binding'
|
2
|
+
|
3
|
+
module Representable
|
4
|
+
module YAML
|
5
|
+
class Binding < Representable::Hash::Binding
|
6
|
+
def self.build_for(definition, *args)
|
7
|
+
return Collection.new(definition, *args) if definition.array?
|
8
|
+
new(definition, *args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def write(map, fragment)
|
12
|
+
map.children << Psych::Nodes::Scalar.new(as)
|
13
|
+
map.children << node_for(fragment) # FIXME: should be serialize.
|
14
|
+
end
|
15
|
+
# private
|
16
|
+
|
17
|
+
def node_for(fragment)
|
18
|
+
write_scalar(fragment)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_scalar(value)
|
22
|
+
return value if typed?
|
23
|
+
|
24
|
+
Psych::Nodes::Scalar.new(value.to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize_method
|
28
|
+
:to_ast
|
29
|
+
end
|
30
|
+
|
31
|
+
def deserialize_method
|
32
|
+
:from_hash
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
class Collection < self
|
37
|
+
include Representable::Binding::Collection
|
38
|
+
|
39
|
+
def node_for(fragments)
|
40
|
+
Psych::Nodes::Sequence.new.tap do |seq|
|
41
|
+
seq.style = Psych::Nodes::Sequence::FLOW if self[:style] == :flow
|
42
|
+
fragments.each { |frag| seq.children << write_scalar(frag) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/representable.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
|
26
26
|
s.add_development_dependency "rake"
|
27
27
|
s.add_development_dependency "test_xml", ">= 0.1.6"
|
28
|
-
s.add_development_dependency "minitest", "
|
28
|
+
s.add_development_dependency "minitest", ">= 5.4.1"
|
29
29
|
s.add_development_dependency "mocha", ">= 0.13.0"
|
30
30
|
s.add_development_dependency "mongoid"
|
31
31
|
s.add_development_dependency "virtus"
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
SONG_PROPERTIES = 1000.times.collect do |i|
|
5
|
+
"property_#{i}"
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
module SongRepresenter
|
10
|
+
include Representable::JSON
|
11
|
+
|
12
|
+
SONG_PROPERTIES.each { |p| property p }
|
13
|
+
end
|
14
|
+
|
15
|
+
class SongDecorator < Representable::Decorator
|
16
|
+
include Representable::JSON
|
17
|
+
|
18
|
+
SONG_PROPERTIES.each { |p| property p }
|
19
|
+
end
|
20
|
+
|
21
|
+
module AlbumRepresenter
|
22
|
+
include Representable::JSON
|
23
|
+
|
24
|
+
# collection :songs, extend: SongRepresenter
|
25
|
+
collection :songs, extend: SongDecorator
|
26
|
+
end
|
27
|
+
|
28
|
+
def random_song
|
29
|
+
attrs = Hash[SONG_PROPERTIES.collect { |n| [n,n] }]
|
30
|
+
OpenStruct.new(attrs)
|
31
|
+
end
|
32
|
+
|
33
|
+
times = []
|
34
|
+
|
35
|
+
3.times.each do
|
36
|
+
album = OpenStruct.new(songs: 100.times.collect { random_song })
|
37
|
+
|
38
|
+
times << Benchmark.measure do
|
39
|
+
puts "================ next!"
|
40
|
+
album.extend(AlbumRepresenter).to_json
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
puts times.join("")
|
45
|
+
|
46
|
+
# 100 songs, 100 attrs
|
47
|
+
# 0.050000 0.000000 0.050000 ( 0.093157)
|
48
|
+
|
49
|
+
## 100 songs, 1000 attrs
|
50
|
+
# 0.470000 0.010000 0.480000 ( 0.483708)
|
51
|
+
|
52
|
+
|
53
|
+
### without binding cache:
|
54
|
+
# 2.790000 0.030000 2.820000 ( 2.820190)
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
### with extend: on Song, with binding cache>
|
59
|
+
# 2.490000 0.030000 2.520000 ( 2.517433) 2.4-3.0
|
60
|
+
### without skip?
|
61
|
+
# 2.030000 0.020000 2.050000 ( 2.050796) 2.1-2.3
|
62
|
+
|
63
|
+
### without :writer
|
64
|
+
# 2.270000 0.010000 2.280000 ( 2.284530 1.9-2.2
|
65
|
+
### without :render_filter
|
66
|
+
# 2.020000 0.000000 2.020000 ( 2.030234) 1.5-2.0
|
67
|
+
###without default_for and skipable?
|
68
|
+
# 1.730000 0.010000 1.740000 ( 1.735597 1.4-1.7
|
69
|
+
### without :serialize
|
70
|
+
# 1.780000 0.010000 1.790000 ( 1.786791) 1.4-1.7
|
71
|
+
### using decorator
|
72
|
+
# 1.400000 0.030000 1.430000 ( 1.434206) 1.4-1.6
|
73
|
+
### with prepare AFTER representable?
|
74
|
+
# 1.330000 0.010000 1.340000 ( 1.335900) 1.1-1.3
|
75
|
+
|
76
|
+
|
77
|
+
# representable 2.0
|
78
|
+
# 3.000000 0.020000 3.020000 ( 3.013031) 2.7-3.0
|
79
|
+
|
80
|
+
# no method missing
|
81
|
+
# 2.280000 0.030000 2.310000 ( 2.313522) 2.2-2.5
|
82
|
+
# no def_delegator in Definition
|
83
|
+
# 2.130000 0.010000 2.140000 ( 2.136115) 1.7-2.1
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BindingTest < MiniTest::Spec
|
4
|
+
Binding = Representable::Binding
|
5
|
+
let (:render_nil_definition) { Representable::Definition.new(:song, :render_nil => true) }
|
6
|
+
|
7
|
+
describe "#skipable_empty_value?" do
|
8
|
+
let (:binding) { Binding.new(render_nil_definition, nil, nil) }
|
9
|
+
|
10
|
+
# don't skip when present.
|
11
|
+
it { binding.skipable_empty_value?("Disconnect, Disconnect").must_equal false }
|
12
|
+
|
13
|
+
# don't skip when it's nil and render_nil: true
|
14
|
+
it { binding.skipable_empty_value?(nil).must_equal false }
|
15
|
+
|
16
|
+
# skip when nil and :render_nil undefined.
|
17
|
+
it { Binding.new(Representable::Definition.new(:song), nil, nil).skipable_empty_value?(nil).must_equal true }
|
18
|
+
|
19
|
+
# don't skip when nil and :render_nil undefined.
|
20
|
+
it { Binding.new(Representable::Definition.new(:song), nil, nil).skipable_empty_value?("Fatal Flu").must_equal false }
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
describe "#default_for" do
|
25
|
+
let (:definition) { Representable::Definition.new(:song, :default => "Insider") }
|
26
|
+
let (:binding) { Binding.new(definition, nil, nil) }
|
27
|
+
|
28
|
+
# return value when value present.
|
29
|
+
it { binding.default_for("Black And Blue").must_equal "Black And Blue" }
|
30
|
+
|
31
|
+
# return false when value false.
|
32
|
+
it { binding.default_for(false).must_equal false }
|
33
|
+
|
34
|
+
# return default when value nil.
|
35
|
+
it { binding.default_for(nil).must_equal "Insider" }
|
36
|
+
|
37
|
+
# return nil when value nil and render_nil: true.
|
38
|
+
it { Binding.new(render_nil_definition, nil, nil).default_for(nil).must_equal nil }
|
39
|
+
|
40
|
+
# return nil when value nil and render_nil: true, even when :default is set" do
|
41
|
+
it { Binding.new(Representable::Definition.new(:song, :render_nil => true, :default => "The Quest"), nil, nil).default_for(nil).must_equal nil }
|
42
|
+
|
43
|
+
# return nil if no :default
|
44
|
+
it { Binding.new(Representable::Definition.new(:song), nil, nil).default_for(nil).must_equal nil }
|
45
|
+
end
|
46
|
+
end
|
data/test/definition_test.rb
CHANGED
@@ -88,6 +88,11 @@ class DefinitionTest < MiniTest::Spec
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
+
# #inspect
|
92
|
+
describe "#inspect" do
|
93
|
+
it { Definition.new(:songs).inspect.must_equal "#<Representable::Definition ==>songs @options={:parse_filter=>[], :render_filter=>[], :as=>\"songs\"}>" }
|
94
|
+
end
|
95
|
+
|
91
96
|
|
92
97
|
describe "generic API" do
|
93
98
|
before do
|
@@ -186,64 +191,6 @@ class DefinitionTest < MiniTest::Spec
|
|
186
191
|
end
|
187
192
|
|
188
193
|
|
189
|
-
describe "#skipable_empty_value?" do
|
190
|
-
# default if skipable_empty_value?
|
191
|
-
before do
|
192
|
-
@def = Representable::Definition.new(:song, :render_nil => true)
|
193
|
-
end
|
194
|
-
|
195
|
-
it "returns false when not nil" do
|
196
|
-
assert_equal false, @def.skipable_empty_value?("Disconnect, Disconnect")
|
197
|
-
end
|
198
|
-
|
199
|
-
it "returns false when nil and :render_nil => true" do
|
200
|
-
assert_equal false, @def.skipable_empty_value?(nil)
|
201
|
-
end
|
202
|
-
|
203
|
-
it "returns true when nil and :render_nil => false" do
|
204
|
-
assert_equal true, Representable::Definition.new(:song).skipable_empty_value?(nil)
|
205
|
-
end
|
206
|
-
|
207
|
-
it "returns false when not nil and :render_nil => false" do
|
208
|
-
assert_equal false, Representable::Definition.new(:song).skipable_empty_value?("Fatal Flu")
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
|
213
|
-
describe "#default_for" do
|
214
|
-
before do
|
215
|
-
@def = Representable::Definition.new(:song, :default => "Insider")
|
216
|
-
end
|
217
|
-
|
218
|
-
it "always returns value when value not nil" do
|
219
|
-
assert_equal "Black And Blue", @def.default_for("Black And Blue")
|
220
|
-
end
|
221
|
-
|
222
|
-
it "returns false when value false" do
|
223
|
-
assert_equal false, @def.default_for(false)
|
224
|
-
end
|
225
|
-
|
226
|
-
it "returns default when value nil" do
|
227
|
-
assert_equal "Insider", @def.default_for(nil)
|
228
|
-
end
|
229
|
-
|
230
|
-
it "returns nil when value nil and :render_nil true" do
|
231
|
-
@def = Representable::Definition.new(:song, :render_nil => true)
|
232
|
-
assert_equal nil, @def.default_for(nil)
|
233
|
-
end
|
234
|
-
|
235
|
-
it "returns nil when value nil and :render_nil true even when :default is set" do
|
236
|
-
@def = Representable::Definition.new(:song, :render_nil => true, :default => "The Quest")
|
237
|
-
assert_equal nil, @def.default_for(nil)
|
238
|
-
end
|
239
|
-
|
240
|
-
it "returns nil if no :default" do
|
241
|
-
@def = Representable::Definition.new(:song)
|
242
|
-
assert_equal nil, @def.default_for(nil)
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
|
247
194
|
describe "#binding" do
|
248
195
|
it "returns true when :binding is set" do
|
249
196
|
assert Representable::Definition.new(:songs, :binding => Object)[:binding]
|
data/test/exec_context_test.rb
CHANGED
@@ -40,8 +40,8 @@ class ExecContextTest < MiniTest::Spec
|
|
40
40
|
}
|
41
41
|
end
|
42
42
|
|
43
|
-
it { render(song).must_equal_document({Representable::Hash::
|
44
|
-
it { parse(song, {Representable::Hash::
|
43
|
+
it { render(song).must_equal_document({Representable::Hash::Binding => "name"}) }
|
44
|
+
it { parse(song, {Representable::Hash::Binding => "Rebel Fate"}).name.must_equal "Rebel Fate" }
|
45
45
|
end
|
46
46
|
|
47
47
|
|
@@ -85,8 +85,8 @@ class ExecContextTest < MiniTest::Spec
|
|
85
85
|
}
|
86
86
|
end
|
87
87
|
|
88
|
-
it { render(song).must_equal_document({Representable::Hash::
|
89
|
-
it { parse(song, {Representable::Hash::
|
88
|
+
it { render(song).must_equal_document({Representable::Hash::Binding => "name"}) }
|
89
|
+
it { parse(song, {Representable::Hash::Binding => "Rebel Fate"}).name.must_equal "Rebel Fate" }
|
90
90
|
end
|
91
91
|
end
|
92
92
|
end
|
data/test/hash_bindings_test.rb
CHANGED
@@ -15,7 +15,7 @@ class HashBindingTest < MiniTest::Spec
|
|
15
15
|
describe "PropertyBinding" do
|
16
16
|
describe "#read" do
|
17
17
|
before do
|
18
|
-
@property = Representable::Hash::
|
18
|
+
@property = Representable::Hash::Binding.new(Representable::Definition.new(:song), nil, nil)
|
19
19
|
end
|
20
20
|
|
21
21
|
it "returns fragment if present" do
|
@@ -29,61 +29,13 @@ class HashBindingTest < MiniTest::Spec
|
|
29
29
|
end
|
30
30
|
|
31
31
|
end
|
32
|
-
|
33
|
-
describe "with plain text" do
|
34
|
-
before do
|
35
|
-
@property = Representable::Hash::PropertyBinding.new(Representable::Definition.new(:song), nil, nil)
|
36
|
-
end
|
37
|
-
|
38
|
-
it "extracts with #read" do
|
39
|
-
assert_equal "Thinning the Herd", @property.read("song" => "Thinning the Herd")
|
40
|
-
end
|
41
|
-
|
42
|
-
it "inserts with #write" do
|
43
|
-
doc = {}
|
44
|
-
assert_equal("Thinning the Herd", @property.write(doc,"Thinning the Herd"))
|
45
|
-
assert_equal({"song"=>"Thinning the Herd"}, doc)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
describe "with an object" do
|
50
|
-
before do
|
51
|
-
@property = Representable::Hash::PropertyBinding.new(Representable::Definition.new(:song, :class => SongWithRepresenter), nil, nil)
|
52
|
-
@doc = {}
|
53
|
-
end
|
54
|
-
|
55
|
-
it "extracts with #read" do
|
56
|
-
assert_equal SongWithRepresenter.new("Thinning the Herd"), @property.read("song" => {"name" => "Thinning the Herd"})
|
57
|
-
end
|
58
|
-
|
59
|
-
it "inserts with #write" do
|
60
|
-
assert_equal({"name"=>"Thinning the Herd"}, @property.write(@doc, SongWithRepresenter.new("Thinning the Herd")))
|
61
|
-
assert_equal({"song" => {"name"=>"Thinning the Herd"}}, @doc)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
describe "with an object and :extend" do
|
66
|
-
before do
|
67
|
-
@property = Representable::Hash::PropertyBinding.new(Representable::Definition.new(:song, :class => Song, :extend => SongRepresenter), nil, nil)
|
68
|
-
@doc = {}
|
69
|
-
end
|
70
|
-
|
71
|
-
it "extracts with #read" do
|
72
|
-
assert_equal Song.new("Thinning the Herd"), @property.read("song" => {"name" => "Thinning the Herd"})
|
73
|
-
end
|
74
|
-
|
75
|
-
it "inserts with #write" do
|
76
|
-
assert_equal({"name"=>"Thinning the Herd"}, @property.write(@doc, Song.new("Thinning the Herd")))
|
77
|
-
assert_equal({"song" => {"name"=>"Thinning the Herd"}}, @doc)
|
78
|
-
end
|
79
|
-
end
|
80
32
|
end
|
81
33
|
|
82
34
|
|
83
35
|
describe "CollectionBinding" do
|
84
36
|
describe "with plain text items" do
|
85
37
|
before do
|
86
|
-
@property = Representable::Hash::
|
38
|
+
@property = Representable::Hash::Binding::Collection.new(Representable::Definition.new(:songs, :collection => true), Album.new, nil)
|
87
39
|
end
|
88
40
|
|
89
41
|
it "extracts with #read" do
|
@@ -104,7 +56,7 @@ class HashBindingTest < MiniTest::Spec
|
|
104
56
|
describe "HashBinding" do
|
105
57
|
describe "with plain text items" do
|
106
58
|
before do
|
107
|
-
@property = Representable::Hash::
|
59
|
+
@property = Representable::Hash::Binding::Hash.new(Representable::Definition.new(:songs, :hash => true), nil, nil)
|
108
60
|
end
|
109
61
|
|
110
62
|
it "extracts with #read" do
|
@@ -120,7 +72,7 @@ class HashBindingTest < MiniTest::Spec
|
|
120
72
|
|
121
73
|
describe "with objects" do
|
122
74
|
before do
|
123
|
-
@property = Representable::Hash::
|
75
|
+
@property = Representable::Hash::Binding::Hash.new(Representable::Definition.new(:songs, :hash => true, :class => Song, :extend => SongRepresenter), nil, nil)
|
124
76
|
end
|
125
77
|
|
126
78
|
it "doesn't change the represented hash in #write" do
|
data/test/hash_test.rb
CHANGED
@@ -14,7 +14,7 @@ class HashTest < MiniTest::Spec
|
|
14
14
|
|
15
15
|
|
16
16
|
describe "property" do
|
17
|
-
let (:
|
17
|
+
let (:hsh) { hash_representer do property :best_song end }
|
18
18
|
|
19
19
|
let (:album) { Album.new.tap do |album|
|
20
20
|
album.best_song = "Liar"
|
@@ -22,14 +22,14 @@ class HashTest < MiniTest::Spec
|
|
22
22
|
|
23
23
|
describe "#to_hash" do
|
24
24
|
it "renders plain property" do
|
25
|
-
album.extend(
|
25
|
+
album.extend(hsh).to_hash.must_equal("best_song" => "Liar")
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
|
30
30
|
describe "#from_hash" do
|
31
31
|
it "parses plain property" do
|
32
|
-
album.extend(
|
32
|
+
album.extend(hsh).from_hash("best_song" => "This Song Is Recycled").best_song.must_equal "This Song Is Recycled"
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -95,7 +95,7 @@ class HashTest < MiniTest::Spec
|
|
95
95
|
|
96
96
|
|
97
97
|
describe "collection" do
|
98
|
-
let (:
|
98
|
+
let (:hsh) { hash_representer do collection :songs end }
|
99
99
|
|
100
100
|
let (:album) { Album.new.tap do |album|
|
101
101
|
album.songs = ["Jackhammer", "Terrible Man"]
|
@@ -104,14 +104,14 @@ class HashTest < MiniTest::Spec
|
|
104
104
|
|
105
105
|
describe "#to_hash" do
|
106
106
|
it "renders a block style list per default" do
|
107
|
-
album.extend(
|
107
|
+
album.extend(hsh).to_hash.must_equal("songs" => ["Jackhammer", "Terrible Man"])
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
111
|
|
112
112
|
describe "#from_hash" do
|
113
113
|
it "parses a block style list" do
|
114
|
-
album.extend(
|
114
|
+
album.extend(hsh).from_hash("songs" => ["Off Key Melody", "Sinking"]).must_equal Album.new(["Off Key Melody", "Sinking"])
|
115
115
|
|
116
116
|
end
|
117
117
|
end
|
data/test/json_test.rb
CHANGED
@@ -88,21 +88,21 @@ module JsonTest
|
|
88
88
|
|
89
89
|
describe "#build_for" do
|
90
90
|
it "returns TextBinding" do
|
91
|
-
assert_kind_of Representable::Hash::
|
91
|
+
assert_kind_of Representable::Hash::Binding, Representable::Hash::Binding.build_for(Def.new(:band), nil, nil)
|
92
92
|
end
|
93
93
|
|
94
94
|
it "returns HashBinding" do
|
95
|
-
assert_kind_of Representable::Hash::
|
95
|
+
assert_kind_of Representable::Hash::Binding::Hash, Representable::Hash::Binding.build_for(Def.new(:band, :hash => true), nil, nil)
|
96
96
|
end
|
97
97
|
|
98
98
|
it "returns CollectionBinding" do
|
99
|
-
assert_kind_of Representable::Hash::
|
99
|
+
assert_kind_of Representable::Hash::Binding::Collection, Representable::Hash::Binding.build_for(Def.new(:band, :collection => true), nil, nil)
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
103
|
# describe "#representable_bindings_for" do
|
104
104
|
# it "returns bindings for each property" do
|
105
|
-
# bins = @band.send(:representable_bindings_for, Representable::JSON::
|
105
|
+
# bins = @band.send(:representable_bindings_for, Representable::JSON::Binding, {})
|
106
106
|
# assert_equal 2, bins.size
|
107
107
|
# assert_equal "name", bins.first.name
|
108
108
|
# end
|
@@ -395,15 +395,15 @@ end
|
|
395
395
|
|
396
396
|
describe "parsing" do
|
397
397
|
subject { OpenStruct.new.extend(representer) }
|
398
|
-
let (:
|
398
|
+
let (:hsh) { {"7"=>{"name"=>"Contemplation"}} }
|
399
399
|
|
400
400
|
it "parses incoming hash" do
|
401
|
-
subject.from_hash("songs"=>
|
401
|
+
subject.from_hash("songs"=>hsh).songs.must_equal({"7"=>Song.new("Contemplation")})
|
402
402
|
end
|
403
403
|
|
404
404
|
it "doesn't modify the incoming hash" do
|
405
|
-
subject.from_hash("songs"=> incoming_hash =
|
406
|
-
|
405
|
+
subject.from_hash("songs"=> incoming_hash = hsh.dup)
|
406
|
+
hsh.must_equal incoming_hash
|
407
407
|
end
|
408
408
|
end
|
409
409
|
end
|