representable 1.7.7 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +42 -8
  3. data/README.md +208 -55
  4. data/Rakefile +0 -6
  5. data/lib/representable.rb +39 -43
  6. data/lib/representable/binding.rb +59 -37
  7. data/lib/representable/bindings/hash_bindings.rb +3 -4
  8. data/lib/representable/bindings/xml_bindings.rb +10 -10
  9. data/lib/representable/bindings/yaml_bindings.rb +2 -2
  10. data/lib/representable/coercion.rb +1 -1
  11. data/lib/representable/config.rb +11 -5
  12. data/lib/representable/definition.rb +67 -35
  13. data/lib/representable/deserializer.rb +23 -27
  14. data/lib/representable/hash.rb +15 -4
  15. data/lib/representable/hash/allow_symbols.rb +27 -0
  16. data/lib/representable/json.rb +0 -1
  17. data/lib/representable/json/collection.rb +0 -2
  18. data/lib/representable/mapper.rb +6 -13
  19. data/lib/representable/parse_strategies.rb +57 -0
  20. data/lib/representable/readable_writeable.rb +29 -0
  21. data/lib/representable/serializer.rb +9 -4
  22. data/lib/representable/version.rb +1 -1
  23. data/lib/representable/xml.rb +1 -1
  24. data/lib/representable/xml/collection.rb +0 -2
  25. data/lib/representable/yaml.rb +0 -1
  26. data/representable.gemspec +1 -0
  27. data/test/as_test.rb +43 -0
  28. data/test/class_test.rb +124 -0
  29. data/test/config_test.rb +13 -3
  30. data/test/decorator_scope_test.rb +28 -0
  31. data/test/definition_test.rb +46 -35
  32. data/test/exec_context_test.rb +93 -0
  33. data/test/generic_test.rb +0 -154
  34. data/test/getter_setter_test.rb +28 -0
  35. data/test/hash_bindings_test.rb +35 -35
  36. data/test/hash_test.rb +0 -20
  37. data/test/if_test.rb +78 -0
  38. data/test/inherit_test.rb +21 -1
  39. data/test/inheritance_test.rb +1 -1
  40. data/test/inline_test.rb +40 -2
  41. data/test/instance_test.rb +286 -0
  42. data/test/is_representable_test.rb +77 -0
  43. data/test/json_test.rb +6 -29
  44. data/test/nested_test.rb +30 -0
  45. data/test/parse_strategy_test.rb +249 -0
  46. data/test/pass_options_test.rb +27 -0
  47. data/test/prepare_test.rb +67 -0
  48. data/test/reader_writer_test.rb +19 -0
  49. data/test/representable_test.rb +25 -265
  50. data/test/stringify_hash_test.rb +41 -0
  51. data/test/test_helper.rb +12 -4
  52. data/test/wrap_test.rb +48 -0
  53. data/test/xml_bindings_test.rb +37 -37
  54. data/test/xml_test.rb +14 -14
  55. metadata +94 -30
  56. data/lib/representable/deprecations.rb +0 -4
  57. data/lib/representable/feature/readable_writeable.rb +0 -30
@@ -0,0 +1,77 @@
1
+ require 'test_helper'
2
+
3
+ class IsRepresentableTest < BaseTest
4
+ describe "representable: false, extend:" do
5
+ representer!(:inject => :song_representer) do
6
+ property :song,
7
+ :representable => false,
8
+ :extend => song_representer
9
+ end
10
+
11
+ it "does extend but doesn't call #to_hash" do
12
+ Struct.new(:song).new(song = Object.new).extend(representer).
13
+ to_hash.must_equal("song" => song)
14
+ song.must_be_kind_of Representable::Hash
15
+ end
16
+ end
17
+
18
+
19
+ describe "representable: true, no extend:" do
20
+ representer!(:inject => :song_representer) do
21
+ property :song,
22
+ :representable => true
23
+ end
24
+
25
+ it "doesn't extend but calls #to_hash" do
26
+ song = Object.new
27
+ song.instance_eval do
28
+ def to_hash(*)
29
+ 1
30
+ end
31
+ end
32
+
33
+ Struct.new(:song).new(song).extend(representer).
34
+ to_hash.must_equal("song" => 1)
35
+ song.wont_be_kind_of Representable::Hash
36
+ end
37
+ end
38
+
39
+ # TODO: TEST implement for from_hash.
40
+
41
+ describe "representable: false, with class:" do
42
+ representer!(:inject => :song_representer) do
43
+ property :song,
44
+ :representable => false, :class => OpenStruct, :extend => song_representer
45
+ end
46
+
47
+ it "does extend but doesn't call #from_hash" do
48
+ hit = Struct.new(:song).new.extend(representer).
49
+ from_hash("song" => 1)
50
+
51
+ hit.song.must_equal OpenStruct.new
52
+ hit.song.must_be_kind_of Representable::Hash
53
+ end
54
+ end
55
+
56
+
57
+ describe "representable: true, without extend: but class:" do
58
+ SongReader = Class.new do
59
+ def from_hash(*)
60
+ "Piano?"
61
+ end
62
+ end
63
+
64
+ representer!(:inject => :song_representer) do
65
+ property :song,
66
+ :representable => true, :class => SongReader
67
+ end
68
+
69
+ it "doesn't extend but calls #from_hash" do
70
+ hit = Struct.new(:song).new.extend(representer).
71
+ from_hash("song" => "Sonata No.2")
72
+
73
+ hit.song.must_equal "Piano?"
74
+ hit.song.wont_be_kind_of Representable::Hash
75
+ end
76
+ end
77
+ end
@@ -109,11 +109,6 @@ module JsonTest
109
109
  assert_equal({"name"=>"Rise Against"}, hash)
110
110
  end
111
111
 
112
- it "respects #representation_wrap=" do
113
- @Band.representation_wrap = :group
114
- assert_equal({:group=>{"name"=>"Rise Against"}}, @Band.new("Rise Against").to_hash)
115
- end
116
-
117
112
  it "respects :wrap option" do
118
113
  assert_equal({:band=>{"name"=>"NOFX"}}, @Band.new("NOFX").to_hash(:wrap => :band))
119
114
  end
@@ -128,15 +123,15 @@ module JsonTest
128
123
 
129
124
  describe "#build_for" do
130
125
  it "returns TextBinding" do
131
- assert_kind_of Representable::Hash::PropertyBinding, Representable::Hash::PropertyBinding.build_for(Def.new(:band), nil)
126
+ assert_kind_of Representable::Hash::PropertyBinding, Representable::Hash::PropertyBinding.build_for(Def.new(:band), nil, nil)
132
127
  end
133
128
 
134
129
  it "returns HashBinding" do
135
- assert_kind_of Representable::Hash::HashBinding, Representable::Hash::PropertyBinding.build_for(Def.new(:band, :hash => true), nil)
130
+ assert_kind_of Representable::Hash::HashBinding, Representable::Hash::PropertyBinding.build_for(Def.new(:band, :hash => true), nil, nil)
136
131
  end
137
132
 
138
133
  it "returns CollectionBinding" do
139
- assert_kind_of Representable::Hash::CollectionBinding, Representable::Hash::PropertyBinding.build_for(Def.new(:band, :collection => true), nil)
134
+ assert_kind_of Representable::Hash::CollectionBinding, Representable::Hash::PropertyBinding.build_for(Def.new(:band, :collection => true), nil, nil)
140
135
  end
141
136
  end
142
137
 
@@ -264,24 +259,6 @@ module JsonTest
264
259
  end
265
260
  end
266
261
 
267
- describe ":from => :songName" do
268
- class Song
269
- include Representable::JSON
270
- property :name, :from => :songName
271
- attr_accessor :name
272
- end
273
-
274
- it "respects :from in #from_json" do
275
- song = Song.from_json({:songName => "Run To The Hills"}.to_json)
276
- assert_equal "Run To The Hills", song.name
277
- end
278
-
279
- it "respects :from in #to_json" do
280
- song = Song.new; song.name = "Run To The Hills"
281
- assert_json '{"songName":"Run To The Hills"}', song.to_json
282
- end
283
- end
284
-
285
262
  describe ":as => :songName" do
286
263
  class Song
287
264
  include Representable::JSON
@@ -403,14 +380,14 @@ end
403
380
  end
404
381
 
405
382
 
406
- describe ":from => :songList" do
383
+ describe ":as => :songList" do
407
384
  class Songs
408
385
  include Representable::JSON
409
- collection :tracks, :from => :songList
386
+ collection :tracks, :as => :songList
410
387
  attr_accessor :tracks
411
388
  end
412
389
 
413
- it "respects :from in #from_json" do
390
+ it "respects :as in #from_json" do
414
391
  songs = Songs.from_json({:songList => ["Out in the cold", "Microphone"]}.to_json)
415
392
  assert_equal ["Out in the cold", "Microphone"], songs.tracks
416
393
  end
@@ -76,4 +76,34 @@ class NestedTest < MiniTest::Spec
76
76
  end
77
77
  end
78
78
  end
79
+
80
+
81
+ describe "::nested without block but with inherit:" do
82
+
83
+ representer!(:name => :parent) do
84
+ include Representable::Hash
85
+
86
+ nested :label do
87
+ property :owner
88
+ end
89
+ end
90
+
91
+ representer!(:module => Representable::Hash, :inject => :parent) do
92
+ include parent
93
+ nested :label, :inherit => true, :as => "Label"
94
+ end
95
+
96
+ let (:album) { representer.prepare(Album.new("Epitaph", "Brett Gurewitz", 19)) }
97
+
98
+ it "renders nested Album-properties in separate section" do
99
+ representer.prepare(album).to_hash.must_equal({"Label"=>{"owner"=>"Brett Gurewitz"}})
100
+ end
101
+
102
+ # it "parses nested properties to Album instance" do
103
+ # album = parse(representer.prepare(Album.new), output)
104
+ # album.label.must_equal "Epitaph"
105
+ # album.owner.must_equal "Brett Gurewitz"
106
+ # album.amount.must_equal 19
107
+ # end
108
+ end
79
109
  end
@@ -0,0 +1,249 @@
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
+ # Lonely Collection
87
+ for_formats(
88
+ :hash => [Representable::Hash::Collection, [{"title"=>"Resist Stance"}], [{"title"=>"Suffer"}]],
89
+ # :xml => [Representable::XML, "<open_struct><song><title>Resist Stance</title></song></open_struct>", "<open_struct><songs><title>Suffer</title></songs></open_struct>"],
90
+ ) do |format, mod, output, input|
91
+
92
+ describe "[#{format}] lonely collection with :parse_strategy: :sync" do # TODO: introduce :representable option?
93
+ let (:format) { format }
94
+ representer!(:module => Representable::Hash, :name => :song_representer) do
95
+ property :title
96
+ self.representation_wrap = :song if format == :xml
97
+ end
98
+
99
+ representer!(:inject => :song_representer, :module => mod) do
100
+ items :parse_strategy => :sync, :extend => song_representer
101
+ end
102
+
103
+ let (:album) { [song].extend(representer) }
104
+
105
+ it "calls #to_hash on song instances, nothing else" do
106
+ render(album).must_equal_document(output)
107
+ end
108
+
109
+ it "calls #from_hash on the existing song instance, nothing else" do
110
+ #collection_id = album.object_id
111
+ song = album.first
112
+ song_id = song.object_id
113
+
114
+ parse(album, input)
115
+
116
+ album.first.title.must_equal "Suffer"
117
+ song.title.must_equal "Suffer"
118
+ song.object_id.must_equal song_id
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+
125
+ class ParseStrategyFindOrInstantiateTest < BaseTest
126
+ # parse_strategy: :find_or_instantiate
127
+
128
+ Song = Struct.new(:id, :title)
129
+ Song.class_eval do
130
+ def self.find(id)
131
+ return new(1, "Resist Stan") if id==1# we should return the same object here
132
+ new
133
+ end
134
+ end
135
+
136
+ representer!(:name => :song_representer) do
137
+ property :title
138
+ end
139
+
140
+
141
+ describe "collection" do
142
+ representer!(:inject => :song_representer) do
143
+ collection :songs, :parse_strategy => :find_or_instantiate, :extend => song_representer, :class => Song
144
+ end
145
+
146
+ let (:album) { Struct.new(:songs).new([]).extend(representer) }
147
+
148
+
149
+ it "replaces the existing collection with a new consisting of existing items or new items" do
150
+ songs_id = album.songs.object_id
151
+
152
+ album.from_hash({"songs"=>[{"id" => 1, "title"=>"Resist Stance"}, {"title"=>"Suffer"}]})
153
+
154
+ album.songs[0].title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
155
+ album.songs[0].id.must_equal 1
156
+ album.songs[1].title.must_equal "Suffer"
157
+ album.songs[1].id.must_equal nil
158
+
159
+ album.songs.object_id.wont_equal songs_id
160
+ end
161
+
162
+ # TODO: test with existing collection
163
+ end
164
+
165
+
166
+ describe "property" do
167
+ representer!(:inject => :song_representer) do
168
+ property :song, :parse_strategy => :find_or_instantiate, :extend => song_representer, :class => Song
169
+ end
170
+
171
+ let (:album) { Struct.new(:song).new.extend(representer) }
172
+
173
+
174
+ it "finds song by id" do
175
+ album.from_hash({"song"=>{"id" => 1, "title"=>"Resist Stance"}})
176
+
177
+ album.song.title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
178
+ album.song.id.must_equal 1
179
+ end
180
+
181
+ it "creates song" do
182
+ album.from_hash({"song"=>{"title"=>"Off The Track"}})
183
+
184
+ album.song.title.must_equal "Off The Track"
185
+ album.song.id.must_equal nil
186
+ end
187
+ end
188
+
189
+
190
+ describe "property with dynamic :class" do
191
+ representer!(:inject => :song_representer) do
192
+ property :song, :parse_strategy => :find_or_instantiate, :extend => song_representer,
193
+ :class => lambda { |fragment, *args| fragment["class"] }
194
+ end
195
+
196
+ let (:album) { Struct.new(:song).new.extend(representer) }
197
+
198
+
199
+ it "finds song by id" do
200
+ album.from_hash({"song"=>{"id" => 1, "title"=>"Resist Stance", "class"=>Song}})
201
+
202
+ album.song.title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
203
+ album.song.id.must_equal 1
204
+ end
205
+ end
206
+ end
207
+
208
+
209
+ class ParseStrategyLambdaTest < MiniTest::Spec
210
+ Song = Struct.new(:id, :title)
211
+ Song.class_eval do
212
+ def self.find(id)
213
+ return new(1, "Resist Stan") if id==1# we should return the same object here
214
+ new
215
+ end
216
+ end
217
+
218
+ representer!(:name => :song_representer) do
219
+ property :title
220
+ end
221
+
222
+
223
+ describe "collection" do
224
+ representer!(:inject => :song_representer) do
225
+ collection :songs, :parse_strategy => lambda { |fragment, i, options|
226
+ songs << song = Song.new
227
+ song
228
+ }, :extend => song_representer
229
+ end
230
+
231
+ let (:album) { Struct.new(:songs).new([Song.new(1, "A Walk")]).extend(representer) }
232
+
233
+
234
+ it "adds to existing collection" do
235
+ songs_id = album.songs.object_id
236
+
237
+ album.from_hash({"songs"=>[{"title"=>"Resist Stance"}]})
238
+
239
+ album.songs[0].title.must_equal "A Walk" # note how title is updated from "Resist Stan"
240
+ album.songs[0].id.must_equal 1
241
+ album.songs[1].title.must_equal "Resist Stance"
242
+ album.songs[1].id.must_equal nil
243
+
244
+ album.songs.object_id.must_equal songs_id
245
+ end
246
+
247
+ # TODO: test with existing collection
248
+ end
249
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ class PassOptionsTest < BaseTest
4
+ let (:format) { :hash }
5
+ let (:song) { Struct.new(:title).new("Revolution") }
6
+ let (:prepared) { representer.prepare song }
7
+
8
+ describe "module" do
9
+ representer! do
10
+ property :title, :pass_options => true,
11
+ :as => lambda { |args| [args.binding.name, args.user_options, args.represented, args.decorator] }
12
+ end
13
+
14
+ it { render(prepared, :volume => 1).must_equal_document({["title", {:volume=>1}, prepared, prepared] => "Revolution"}) }
15
+ # it { parse(prepared, {"args" => "Wie Es Geht"}).name.must_equal "Wie Es Geht" }
16
+ end
17
+
18
+ describe "decorator" do
19
+ representer!(:decorator => true) do
20
+ property :title, :pass_options => true,
21
+ :as => lambda { |args| [args.binding.name, args.user_options, args.represented, args.decorator] }
22
+ end
23
+
24
+ it { render(prepared, :volume => 1).must_equal_document({["title", {:volume=>1}, song, prepared] => "Revolution"}) }
25
+ # it { parse(prepared, {"args" => "Wie Es Geht"}).name.must_equal "Wie Es Geht" }
26
+ end
27
+ end