representable 1.7.7 → 1.8.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.
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