representable 2.4.0.rc3 → 2.4.0.rc4

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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +7 -2
  3. data/Rakefile +6 -0
  4. data/lib/representable.rb +21 -17
  5. data/lib/representable/binding.rb +3 -2
  6. data/lib/representable/definition.rb +1 -1
  7. data/lib/representable/deprecations.rb +31 -5
  8. data/lib/representable/deserializer.rb +24 -34
  9. data/lib/representable/hash/collection.rb +9 -2
  10. data/lib/representable/hash_methods.rb +2 -2
  11. data/lib/representable/parse_strategies.rb +6 -7
  12. data/lib/representable/pipeline.rb +12 -1
  13. data/lib/representable/pipeline_factories.rb +9 -2
  14. data/lib/representable/serializer.rb +3 -3
  15. data/lib/representable/version.rb +1 -1
  16. data/test-with-deprecations/as_test.rb +65 -0
  17. data/test-with-deprecations/benchmarking.rb +83 -0
  18. data/test-with-deprecations/binding_test.rb +46 -0
  19. data/test-with-deprecations/blaaaaaaaa_test.rb +69 -0
  20. data/test-with-deprecations/cached_test.rb +147 -0
  21. data/test-with-deprecations/class_test.rb +119 -0
  22. data/test-with-deprecations/coercion_test.rb +52 -0
  23. data/test-with-deprecations/config/inherit_test.rb +135 -0
  24. data/test-with-deprecations/config_test.rb +122 -0
  25. data/test-with-deprecations/decorator_scope_test.rb +28 -0
  26. data/test-with-deprecations/decorator_test.rb +96 -0
  27. data/test-with-deprecations/default_test.rb +34 -0
  28. data/test-with-deprecations/defaults_options_test.rb +93 -0
  29. data/test-with-deprecations/definition_test.rb +264 -0
  30. data/test-with-deprecations/example.rb +310 -0
  31. data/test-with-deprecations/examples/object.rb +31 -0
  32. data/test-with-deprecations/exec_context_test.rb +93 -0
  33. data/test-with-deprecations/features_test.rb +70 -0
  34. data/test-with-deprecations/filter_test.rb +57 -0
  35. data/test-with-deprecations/for_collection_test.rb +74 -0
  36. data/test-with-deprecations/generic_test.rb +116 -0
  37. data/test-with-deprecations/getter_setter_test.rb +21 -0
  38. data/test-with-deprecations/hash_bindings_test.rb +87 -0
  39. data/test-with-deprecations/hash_test.rb +160 -0
  40. data/test-with-deprecations/heritage_test.rb +62 -0
  41. data/test-with-deprecations/if_test.rb +79 -0
  42. data/test-with-deprecations/include_exclude_test.rb +88 -0
  43. data/test-with-deprecations/inherit_test.rb +159 -0
  44. data/test-with-deprecations/inline_test.rb +272 -0
  45. data/test-with-deprecations/instance_test.rb +266 -0
  46. data/test-with-deprecations/is_representable_test.rb +77 -0
  47. data/test-with-deprecations/json_test.rb +355 -0
  48. data/test-with-deprecations/lonely_test.rb +239 -0
  49. data/test-with-deprecations/mongoid_test.rb +31 -0
  50. data/test-with-deprecations/nested_test.rb +115 -0
  51. data/test-with-deprecations/object_test.rb +60 -0
  52. data/{test/---deserialize-pipeline_test.rb → test-with-deprecations/parse_pipeline_test.rb} +29 -2
  53. data/test-with-deprecations/parse_strategy_test.rb +279 -0
  54. data/{test → test-with-deprecations}/pass_options_test.rb +0 -0
  55. data/test-with-deprecations/pipeline_test.rb +277 -0
  56. data/test-with-deprecations/populator_test.rb +105 -0
  57. data/test-with-deprecations/prepare_test.rb +67 -0
  58. data/test-with-deprecations/private_options_test.rb +18 -0
  59. data/test-with-deprecations/reader_writer_test.rb +19 -0
  60. data/test-with-deprecations/realistic_benchmark.rb +115 -0
  61. data/test-with-deprecations/render_nil_test.rb +21 -0
  62. data/test-with-deprecations/represent_test.rb +88 -0
  63. data/test-with-deprecations/representable_test.rb +511 -0
  64. data/test-with-deprecations/schema_test.rb +148 -0
  65. data/test-with-deprecations/serialize_deserialize_test.rb +33 -0
  66. data/test-with-deprecations/skip_test.rb +81 -0
  67. data/test-with-deprecations/stringify_hash_test.rb +41 -0
  68. data/test-with-deprecations/test_helper.rb +135 -0
  69. data/test-with-deprecations/test_helper_test.rb +25 -0
  70. data/test-with-deprecations/uncategorized_test.rb +67 -0
  71. data/test-with-deprecations/user_options_test.rb +15 -0
  72. data/test-with-deprecations/wrap_test.rb +152 -0
  73. data/test-with-deprecations/xml_bindings_test.rb +62 -0
  74. data/test-with-deprecations/xml_test.rb +503 -0
  75. data/test-with-deprecations/yaml_test.rb +162 -0
  76. data/test/as_test.rb +3 -3
  77. data/test/cached_test.rb +2 -2
  78. data/test/class_test.rb +5 -5
  79. data/test/exec_context_test.rb +2 -2
  80. data/test/filter_test.rb +1 -1
  81. data/test/getter_setter_test.rb +4 -4
  82. data/test/if_test.rb +2 -2
  83. data/test/include_exclude_test.rb +88 -0
  84. data/test/instance_test.rb +15 -15
  85. data/test/lonely_test.rb +18 -2
  86. data/test/object_test.rb +4 -4
  87. data/test/parse_pipeline_test.rb +64 -0
  88. data/test/parse_strategy_test.rb +3 -3
  89. data/test/pipeline_test.rb +8 -12
  90. data/test/prepare_test.rb +2 -3
  91. data/test/reader_writer_test.rb +3 -3
  92. data/test/representable_test.rb +12 -48
  93. data/test/serialize_deserialize_test.rb +9 -9
  94. data/test/skip_test.rb +11 -11
  95. data/test/test_helper.rb +2 -0
  96. data/test/uncategorized_test.rb +10 -10
  97. data/test/user_options_test.rb +15 -0
  98. data/test/wrap_test.rb +1 -1
  99. metadata +65 -4
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+ require 'mongoid'
3
+ require 'mongoid/document'
4
+
5
+ class MongoidTest < MiniTest::Spec
6
+ describe "Mongoid compatibility" do
7
+ it "allows #to_json" do
8
+ class Profile
9
+ include Mongoid::Document
10
+ field :name
11
+ end
12
+
13
+ class Dude
14
+ include Mongoid::Document
15
+ embeds_one :profile, :class_name => "MongoidTest::Profile"
16
+ end
17
+
18
+ module ProfileRepresenter
19
+ include Representable::JSON
20
+
21
+ property :name
22
+ end
23
+
24
+ dude = Dude.new
25
+ dude.profile = Profile.new
26
+ dude.profile.name = "Kofi"
27
+
28
+ assert_equal "{\"name\":\"Kofi\"}", dude.profile.extend(ProfileRepresenter).to_json
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,115 @@
1
+ require 'test_helper'
2
+
3
+ class NestedTest < MiniTest::Spec
4
+ Album = Struct.new(:label, :owner, :amount)
5
+
6
+ for_formats(
7
+ :hash => [Representable::Hash, {"label" => {"label"=>"Epitaph", "owner"=>"Brett Gurewitz", "releases"=>{"amount"=>19}}}],
8
+ # :xml => [Representable::XML, "<open_struct></open_struct>"],
9
+ :yaml => [Representable::YAML, "---\nlabel:\n label: Epitaph\n owner: Brett Gurewitz\n releases:\n amount: 19\n"]
10
+ ) do |format, mod, output, input|
11
+
12
+ [false, true].each do |is_decorator|
13
+ describe "::nested with (inline representer|decorator): #{is_decorator}" do
14
+ let (:format) { format }
15
+
16
+ representer!(:module => mod, :decorator => is_decorator) do
17
+ nested :label do
18
+ property :label
19
+ property :owner
20
+
21
+ # self.representation_wrap = nil if format == :xml
22
+ nested :releases do
23
+ property :amount
24
+ end
25
+ end
26
+
27
+ # self.representation_wrap = :album if format == :xml
28
+ end
29
+
30
+ let (:album) { Album.new("Epitaph", "Brett Gurewitz", 19) }
31
+ let (:decorator) { representer.prepare(album) }
32
+
33
+ it "renders nested Album-properties in separate section" do
34
+ render(decorator).must_equal_document output
35
+
36
+ # do not use extend on the nested object. # FIXME: make this a proper test with two describes instead of this pseudo-meta stuff.
37
+ if is_decorator==true
38
+ album.wont_be_kind_of(Representable::Hash)
39
+ end
40
+ end
41
+
42
+ it "parses nested properties to Album instance" do
43
+ album = parse(representer.prepare(Album.new), output)
44
+ album.label.must_equal "Epitaph"
45
+ album.owner.must_equal "Brett Gurewitz"
46
+ end
47
+ end
48
+ end
49
+
50
+
51
+ describe "Decorator ::nested with extend:" do
52
+ let (:format) { format }
53
+
54
+ representer!(:name => :label_rpr) do
55
+ include mod
56
+ property :label
57
+ property :owner
58
+
59
+ nested :releases do # DISCUSS: do we need to test this?
60
+ property :amount
61
+ end
62
+ end
63
+
64
+ representer!(:module => mod, :decorator => true, :inject => :label_rpr) do
65
+ nested :label, :extend => label_rpr
66
+
67
+ self.representation_wrap = :album if format == :xml
68
+ end
69
+
70
+ let (:album) { representer.prepare(Album.new("Epitaph", "Brett Gurewitz", 19)) }
71
+
72
+ # TODO: shared example with above.
73
+ it "renders nested Album-properties in separate section" do
74
+ render(album).must_equal_document output
75
+ end
76
+
77
+ it "parses nested properties to Album instance" do
78
+ album = parse(representer.prepare(Album.new), output)
79
+ album.label.must_equal "Epitaph"
80
+ album.owner.must_equal "Brett Gurewitz"
81
+ album.amount.must_equal 19
82
+ end
83
+ end
84
+ end
85
+
86
+
87
+ describe "::nested without block but with inherit:" do
88
+
89
+ representer!(:name => :parent) do
90
+ include Representable::Hash
91
+
92
+ nested :label do
93
+ property :owner
94
+ end
95
+ end
96
+
97
+ representer!(:module => Representable::Hash, :inject => :parent) do
98
+ include parent
99
+ nested :label, :inherit => true, :as => "Label"
100
+ end
101
+
102
+ let (:album) { representer.prepare(Album.new("Epitaph", "Brett Gurewitz", 19)) }
103
+
104
+ it "renders nested Album-properties in separate section" do
105
+ representer.prepare(album).to_hash.must_equal({"Label"=>{"owner"=>"Brett Gurewitz"}})
106
+ end
107
+
108
+ # it "parses nested properties to Album instance" do
109
+ # album = parse(representer.prepare(Album.new), output)
110
+ # album.label.must_equal "Epitaph"
111
+ # album.owner.must_equal "Brett Gurewitz"
112
+ # album.amount.must_equal 19
113
+ # end
114
+ end
115
+ end
@@ -0,0 +1,60 @@
1
+ require "test_helper"
2
+ require "representable/object"
3
+
4
+ class ObjectTest < MiniTest::Spec
5
+ Song = Struct.new(:title, :album)
6
+ Album = Struct.new(:name, :songs)
7
+
8
+ representer!(module: Representable::Object) do
9
+ property :title
10
+
11
+ property :album, instance: lambda { |fragment, *| fragment.name.upcase!; fragment } do
12
+ property :name
13
+
14
+ collection :songs, instance: lambda { |fragment, *| fragment.title.upcase!; fragment } do
15
+ property :title
16
+ end
17
+ end
18
+ # TODO: collection
19
+ end
20
+
21
+ let (:source) { Song.new("The King Is Dead", Album.new("Ruiner", [Song.new("In Vino Veritas II")])) }
22
+ let (:target) { Song.new }
23
+
24
+ it do
25
+ representer.prepare(target).from_object(source)
26
+
27
+ target.title.must_equal "The King Is Dead"
28
+ target.album.name.must_equal "RUINER"
29
+ target.album.songs[0].title.must_equal "IN VINO VERITAS II"
30
+ end
31
+
32
+ # ignore nested object when nil
33
+ it do
34
+ representer.prepare(Song.new("The King Is Dead")).from_object(Song.new)
35
+
36
+ target.title.must_equal nil # scalar property gets overridden when nil.
37
+ target.album.must_equal nil # nested property stays nil.
38
+ end
39
+
40
+ # to_object
41
+ describe "#to_object" do
42
+ representer!(module: Representable::Object) do
43
+ property :title
44
+
45
+ property :album, render_filter: lambda { |object, *| object.name = "Live"; object } do
46
+ property :name
47
+
48
+ collection :songs, render_filter: lambda { |object, *| object[0].title = 1; object } do
49
+ property :title
50
+ end
51
+ end
52
+ end
53
+
54
+ it do
55
+ representer.prepare(source).to_object
56
+ source.album.name.must_equal "Live"
57
+ source.album.songs[0].title.must_equal 1
58
+ end
59
+ end
60
+ end
@@ -1,10 +1,37 @@
1
1
  require "test_helper"
2
2
 
3
- class DeserializePipelineTest < MiniTest::Spec
4
- Album = Struct.new(:artist, :songs)
3
+ class ParsePipelineTest < MiniTest::Spec
4
+ Album = Struct.new(:id, :artist, :songs)
5
5
  Artist = Struct.new(:email)
6
6
  Song = Struct.new(:title)
7
7
 
8
+ describe "transforming nil to [] when parsing" do
9
+ representer!(decorator: true) do
10
+ collection :songs,
11
+ parse_pipeline: ->(*) {
12
+ Representable::Pipeline.insert(
13
+ parse_functions, # original function list from Binding#parse_functions.
14
+ ->(input, options) { input.nil? ? [] : input }, # your new function (can be any callable object)..
15
+ replace: Representable::OverwriteOnNil # ..that replaces the original function.
16
+ )
17
+ },
18
+ class: Song do
19
+ property :title
20
+ end
21
+ end
22
+
23
+ it do
24
+ representer.new(album = Album.new).from_hash("songs"=>nil)
25
+ album.songs.must_equal []
26
+ end
27
+
28
+ it do
29
+ representer.new(album = Album.new).from_hash("songs"=>[{"title" => "Business Conduct"}])
30
+ album.songs.must_equal [Song.new("Business Conduct")]
31
+ end
32
+ end
33
+
34
+
8
35
  # tests [Collect[Instance, Prepare, Deserialize], Setter]
9
36
  class Representer < Representable::Decorator
10
37
  include Representable::Hash
@@ -0,0 +1,279 @@
1
+ require 'test_helper'
2
+
3
+ # parse_strategy: :sync
4
+ # parse_strategy: :replace
5
+ # parse_strategy: :find_or_instantiate ("expand" since we don't delete existing unmatched in target)
6
+
7
+
8
+ class ParseStrategySyncTest < BaseTest
9
+ for_formats(
10
+ :hash => [Representable::Hash, {"song"=>{"title"=>"Resist Stance"}}, {"song"=>{"title"=>"Suffer"}}],
11
+ :xml => [Representable::XML, "<open_struct><song><title>Resist Stance</title></song></open_struct>", "<open_struct><song><title>Suffer</title></song></open_struct>",],
12
+ :yaml => [Representable::YAML, "---\nsong:\n title: Resist Stance\n", "---\nsong:\n title: Suffer\n"],
13
+ ) do |format, mod, output, input|
14
+
15
+ describe "[#{format}] property with parse_strategy: :sync" do # TODO: introduce :representable option?
16
+ let (:format) { format }
17
+
18
+ representer!(:module => mod, :name => :song_representer) do
19
+ property :title
20
+ self.representation_wrap = :song if format == :xml
21
+ end
22
+
23
+ representer!(:inject => :song_representer, :module => mod) do
24
+ property :song, :parse_strategy => :sync, :extend => song_representer
25
+ end
26
+
27
+ let (:hit) { hit = OpenStruct.new(:song => song).extend(representer) }
28
+
29
+ it "calls #to_hash on song instance, nothing else" do
30
+ render(hit).must_equal_document(output)
31
+ end
32
+
33
+
34
+ it "calls #from_hash on the existing song instance, nothing else" do
35
+ song_id = hit.song.object_id
36
+
37
+ parse(hit, input)
38
+
39
+ hit.song.title.must_equal "Suffer"
40
+ hit.song.object_id.must_equal song_id
41
+ end
42
+ end
43
+ end
44
+
45
+ # FIXME: there's a bug with XML and the collection name!
46
+ for_formats(
47
+ :hash => [Representable::Hash, {"songs"=>[{"title"=>"Resist Stance"}]}, {"songs"=>[{"title"=>"Suffer"}]}],
48
+ #:json => [Representable::JSON, "{\"song\":{\"name\":\"Alive\"}}", "{\"song\":{\"name\":\"You've Taken Everything\"}}"],
49
+ :xml => [Representable::XML, "<open_struct><song><title>Resist Stance</title></song></open_struct>", "<open_struct><songs><title>Suffer</title></songs></open_struct>"],
50
+ :yaml => [Representable::YAML, "---\nsongs:\n- title: Resist Stance\n", "---\nsongs:\n- title: Suffer\n"],
51
+ ) do |format, mod, output, input|
52
+
53
+ describe "[#{format}] collection with :parse_strategy: :sync" do # TODO: introduce :representable option?
54
+ let (:format) { format }
55
+ representer!(:module => mod, :name => :song_representer) do
56
+ property :title
57
+ self.representation_wrap = :song if format == :xml
58
+ end
59
+
60
+ representer!(:inject => :song_representer, :module => mod) do
61
+ collection :songs, :parse_strategy => :sync, :extend => song_representer
62
+ end
63
+
64
+ let (:album) { OpenStruct.new(:songs => [song]).extend(representer) }
65
+
66
+ it "calls #to_hash on song instances, nothing else" do
67
+ render(album).must_equal_document(output)
68
+ end
69
+
70
+ it "calls #from_hash on the existing song instance, nothing else" do
71
+ collection_id = album.songs.object_id
72
+ song = album.songs.first
73
+ song_id = song.object_id
74
+
75
+ parse(album, input)
76
+
77
+ album.songs.first.title.must_equal "Suffer"
78
+ song.title.must_equal "Suffer"
79
+ #album.songs.object_id.must_equal collection_id # TODO: don't replace!
80
+ song.object_id.must_equal song_id
81
+ end
82
+ end
83
+ end
84
+
85
+
86
+ # Sync errors, when model and incoming are not in sync.
87
+ describe ":sync with error" do
88
+ representer! do
89
+ property :song, :parse_strategy => :sync do
90
+ property :title
91
+ end
92
+ end
93
+
94
+ # object.song is nil whereas the document contains one.
95
+ it do
96
+ assert_raises Representable::DeserializeError do
97
+ OpenStruct.new.extend(representer).from_hash({"song" => {"title" => "Perpetual"}})
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+
104
+ # Lonely Collection
105
+ for_formats(
106
+ :hash => [Representable::Hash::Collection, [{"title"=>"Resist Stance"}], [{"title"=>"Suffer"}]],
107
+ # :xml => [Representable::XML, "<open_struct><song><title>Resist Stance</title></song></open_struct>", "<open_struct><songs><title>Suffer</title></songs></open_struct>"],
108
+ ) do |format, mod, output, input|
109
+
110
+ describe "[#{format}] lonely collection with :parse_strategy: :sync" do # TODO: introduce :representable option?
111
+ let (:format) { format }
112
+ representer!(:module => Representable::Hash, :name => :song_representer) do
113
+ property :title
114
+ self.representation_wrap = :song if format == :xml
115
+ end
116
+
117
+ representer!(:inject => :song_representer, :module => mod) do
118
+ items :parse_strategy => :sync, :extend => song_representer
119
+ end
120
+
121
+ let (:album) { [song].extend(representer) }
122
+
123
+ it "calls #to_hash on song instances, nothing else" do
124
+ render(album).must_equal_document(output)
125
+ end
126
+
127
+ it "calls #from_hash on the existing song instance, nothing else" do
128
+ #collection_id = album.object_id
129
+ song = album.first
130
+ song_id = song.object_id
131
+
132
+ parse(album, input)
133
+
134
+ album.first.title.must_equal "Suffer"
135
+ song.title.must_equal "Suffer"
136
+ song.object_id.must_equal song_id
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+
143
+ class ParseStrategyFindOrInstantiateTest < BaseTest
144
+ # parse_strategy: :find_or_instantiate
145
+
146
+ Song = Struct.new(:id, :title)
147
+ Song.class_eval do
148
+ def self.find_by(attributes={})
149
+ return new(1, "Resist Stan") if attributes[:id]==1# we should return the same object here
150
+ new
151
+ end
152
+ end
153
+
154
+ representer!(:name => :song_representer) do
155
+ property :title
156
+ end
157
+
158
+
159
+ describe "collection" do
160
+ representer!(:inject => :song_representer) do
161
+ collection :songs, :parse_strategy => :find_or_instantiate, :extend => song_representer, :class => Song
162
+ end
163
+
164
+ let (:album) { Struct.new(:songs).new([]).extend(representer) }
165
+
166
+
167
+ it "replaces the existing collection with a new consisting of existing items or new items" do
168
+ songs_id = album.songs.object_id
169
+
170
+ album.from_hash({"songs"=>[{"id" => 1, "title"=>"Resist Stance"}, {"title"=>"Suffer"}]})
171
+
172
+ album.songs[0].title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
173
+ album.songs[0].id.must_equal 1
174
+ album.songs[1].title.must_equal "Suffer"
175
+ album.songs[1].id.must_equal nil
176
+
177
+ album.songs.object_id.wont_equal songs_id
178
+ end
179
+
180
+ # TODO: test with existing collection
181
+ end
182
+
183
+
184
+ describe "property" do
185
+ representer!(:inject => :song_representer) do
186
+ property :song, :parse_strategy => :find_or_instantiate, :extend => song_representer, :class => Song
187
+ end
188
+
189
+ let (:album) { Struct.new(:song).new.extend(representer) }
190
+
191
+
192
+ it "finds song by id" do
193
+ album.from_hash({"song"=>{"id" => 1, "title"=>"Resist Stance"}})
194
+
195
+ album.song.title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
196
+ album.song.id.must_equal 1
197
+ end
198
+
199
+ it "creates song" do
200
+ album.from_hash({"song"=>{"title"=>"Off The Track"}})
201
+
202
+ album.song.title.must_equal "Off The Track"
203
+ album.song.id.must_equal nil
204
+ end
205
+ end
206
+
207
+
208
+ describe "property with dynamic :class" do
209
+ representer!(:inject => :song_representer) do
210
+ property :song, :parse_strategy => :find_or_instantiate, :extend => song_representer,
211
+ :class => lambda { |fragment, *args| fragment["class"] }
212
+ end
213
+
214
+ let (:album) { Struct.new(:song).new.extend(representer) }
215
+
216
+
217
+ it "finds song by id" do
218
+ album.from_hash({"song"=>{"id" => 1, "title"=>"Resist Stance", "class"=>Song}})
219
+
220
+ album.song.title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
221
+ album.song.id.must_equal 1
222
+ end
223
+ end
224
+ end
225
+
226
+
227
+ class ParseStrategyLambdaTest < MiniTest::Spec
228
+ Song = Struct.new(:id, :title)
229
+ Song.class_eval do
230
+ def self.find_by(attributes={})
231
+ return new(1, "Resist Stan") if attributes[:id]==1# we should return the same object here
232
+ new
233
+ end
234
+ end
235
+
236
+ representer!(:name => :song_representer) do
237
+ property :title
238
+ end
239
+
240
+ # property with instance: lambda, using representable's setter. # TODO: that should be handled better via my api.
241
+ describe "property parse_strategy: lambda, representable: false" do
242
+ representer! do
243
+ property :title,
244
+ :instance => lambda { |fragment, options| fragment.to_s }, # this will still call song.title= "8675309".
245
+ :representable => false # don't call object.from_hash
246
+ end
247
+
248
+ let (:song) { Song.new(nil, nil) }
249
+ it { song.extend(representer).from_hash("title" => 8675309).title.must_equal "8675309" }
250
+ end
251
+
252
+
253
+ describe "collection" do
254
+ representer!(:inject => :song_representer) do
255
+ collection :songs, :parse_strategy => lambda { |fragment, i, options|
256
+ songs << song = Song.new
257
+ song
258
+ }, :extend => song_representer
259
+ end
260
+
261
+ let (:album) { Struct.new(:songs).new([Song.new(1, "A Walk")]).extend(representer) }
262
+
263
+
264
+ it "adds to existing collection" do
265
+ songs_id = album.songs.object_id
266
+
267
+ album.from_hash({"songs"=>[{"title"=>"Resist Stance"}]})
268
+
269
+ album.songs[0].title.must_equal "A Walk" # note how title is updated from "Resist Stan"
270
+ album.songs[0].id.must_equal 1
271
+ album.songs[1].title.must_equal "Resist Stance"
272
+ album.songs[1].id.must_equal nil
273
+
274
+ album.songs.object_id.must_equal songs_id
275
+ end
276
+
277
+ # TODO: test with existing collection
278
+ end
279
+ end