representable 2.3.0 → 2.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +47 -0
  3. data/Gemfile +5 -0
  4. data/README.md +33 -0
  5. data/lib/representable.rb +60 -73
  6. data/lib/representable/binding.rb +37 -194
  7. data/lib/representable/cached.rb +10 -46
  8. data/lib/representable/coercion.rb +8 -8
  9. data/lib/representable/config.rb +15 -75
  10. data/lib/representable/debug.rb +41 -59
  11. data/lib/representable/declarative.rb +34 -53
  12. data/lib/representable/decorator.rb +11 -40
  13. data/lib/representable/definition.rb +14 -15
  14. data/lib/representable/deprecations.rb +90 -0
  15. data/lib/representable/deserializer.rb +87 -82
  16. data/lib/representable/for_collection.rb +5 -3
  17. data/lib/representable/hash.rb +5 -3
  18. data/lib/representable/hash/binding.rb +6 -15
  19. data/lib/representable/hash/collection.rb +10 -6
  20. data/lib/representable/hash_methods.rb +5 -5
  21. data/lib/representable/insert.rb +31 -0
  22. data/lib/representable/json.rb +7 -3
  23. data/lib/representable/json/hash.rb +1 -1
  24. data/lib/representable/object/binding.rb +5 -5
  25. data/lib/representable/parse_strategies.rb +37 -3
  26. data/lib/representable/pipeline.rb +37 -5
  27. data/lib/representable/pipeline_factories.rb +88 -0
  28. data/lib/representable/serializer.rb +38 -44
  29. data/lib/representable/version.rb +1 -1
  30. data/lib/representable/xml.rb +4 -0
  31. data/lib/representable/xml/binding.rb +25 -31
  32. data/lib/representable/xml/collection.rb +5 -3
  33. data/lib/representable/xml/hash.rb +7 -2
  34. data/lib/representable/yaml.rb +6 -3
  35. data/lib/representable/yaml/binding.rb +4 -4
  36. data/representable.gemspec +3 -3
  37. data/test/---deserialize-pipeline_test.rb +37 -0
  38. data/test/binding_test.rb +7 -7
  39. data/test/cached_test.rb +31 -19
  40. data/test/coercion_test.rb +2 -2
  41. data/test/config/inherit_test.rb +13 -12
  42. data/test/config_test.rb +12 -67
  43. data/test/decorator_test.rb +4 -5
  44. data/test/default_test.rb +34 -0
  45. data/test/defaults_options_test.rb +93 -0
  46. data/test/definition_test.rb +19 -39
  47. data/test/exec_context_test.rb +1 -1
  48. data/test/filter_test.rb +18 -20
  49. data/test/getter_setter_test.rb +1 -8
  50. data/test/hash_bindings_test.rb +13 -13
  51. data/test/heritage_test.rb +62 -0
  52. data/test/if_test.rb +1 -0
  53. data/test/inherit_test.rb +5 -3
  54. data/test/instance_test.rb +3 -4
  55. data/test/json_test.rb +3 -59
  56. data/test/lonely_test.rb +47 -3
  57. data/test/nested_test.rb +8 -2
  58. data/test/pipeline_test.rb +259 -0
  59. data/test/populator_test.rb +76 -0
  60. data/test/realistic_benchmark.rb +39 -7
  61. data/test/render_nil_test.rb +21 -0
  62. data/test/represent_test.rb +2 -2
  63. data/test/representable_test.rb +33 -103
  64. data/test/schema_test.rb +5 -15
  65. data/test/serialize_deserialize_test.rb +2 -2
  66. data/test/skip_test.rb +1 -1
  67. data/test/test_helper.rb +6 -0
  68. data/test/uncategorized_test.rb +67 -0
  69. data/test/xml_bindings_test.rb +6 -6
  70. data/test/xml_test.rb +6 -6
  71. metadata +33 -13
  72. data/lib/representable/apply.rb +0 -13
  73. data/lib/representable/inheritable.rb +0 -71
  74. data/lib/representable/mapper.rb +0 -83
  75. data/lib/representable/populator.rb +0 -56
  76. data/test/inheritable_test.rb +0 -97
@@ -27,10 +27,16 @@ class NestedTest < MiniTest::Spec
27
27
  # self.representation_wrap = :album if format == :xml
28
28
  end
29
29
 
30
- let (:album) { representer.prepare(Album.new("Epitaph", "Brett Gurewitz", 19)) }
30
+ let (:album) { Album.new("Epitaph", "Brett Gurewitz", 19) }
31
+ let (:decorator) { representer.prepare(album) }
31
32
 
32
33
  it "renders nested Album-properties in separate section" do
33
- render(album).must_equal_document output
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
34
40
  end
35
41
 
36
42
  it "parses nested properties to Album instance" do
@@ -0,0 +1,259 @@
1
+ require "test_helper"
2
+
3
+ class PipelineTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :artist)
5
+ Artist = Struct.new(:name)
6
+ Album = Struct.new(:ratings, :artists)
7
+
8
+ R = Representable
9
+ P = R::Pipeline
10
+
11
+ Getter = ->(input, options) { "Yo" }
12
+ StopOnNil = ->(input, options) { input }
13
+ SkipRender = ->(input, *) { input == "Yo" ? input : P::Stop }
14
+
15
+ Prepare = ->(input, options) { "Prepare(#{input})" }
16
+ Deserialize = ->(input, options) { "Deserialize(#{input}, #{options[:fragment]})" }
17
+
18
+ SkipParse = ->(input, options) { input }
19
+ CreateObject = ->(input, options) { OpenStruct.new }
20
+
21
+
22
+ Setter = ->(input, options) { "Setter(#{input})" }
23
+
24
+ AssignFragment = ->(input, options) { options[:fragment] = input }
25
+
26
+ it "linear" do
27
+ P[SkipParse, Setter].("doc", {fragment: 1}).must_equal "Setter(doc)"
28
+
29
+
30
+ # parse style.
31
+ P[AssignFragment, SkipParse, CreateObject, Prepare].("Bla", {}).must_equal "Prepare(#<OpenStruct>)"
32
+
33
+
34
+ # render style.
35
+ P[Getter, StopOnNil, SkipRender, Prepare, Setter].(nil, {}).
36
+ must_equal "Setter(Prepare(Yo))"
37
+
38
+ # pipeline = Representable::Pipeline[SkipParse , SetResult, ModifyResult]
39
+ # pipeline.(fragment: "yo!").must_equal "modified object from yo!"
40
+ end
41
+
42
+ Stopping = ->(input, options) { return P::Stop if options[:fragment] == "stop!"; input }
43
+
44
+
45
+ it "stopping" do
46
+
47
+
48
+ pipeline = Representable::Pipeline[SkipParse, Stopping, Prepare]
49
+ pipeline.(nil, fragment: "oy!").must_equal "Prepare()"
50
+ pipeline.(nil, fragment: "stop!").must_equal Representable::Pipeline::Stop
51
+ end
52
+
53
+ describe "Collect" do
54
+ Reverse = ->(input, options) { input.reverse }
55
+ Add = ->(input, options) { "#{input}+" }
56
+ let(:pipeline) { R::Collect[Reverse, Add] }
57
+
58
+ it { pipeline.(["yo!", "oy!"], {}).must_equal ["!oy+", "!yo+"] }
59
+
60
+ describe "Pipeline with Collect" do
61
+ let(:pipeline) { P[Reverse, R::Collect[Reverse, Add]] }
62
+ it { pipeline.(["yo!", "oy!"], {}).must_equal ["!yo+", "!oy+"] }
63
+ end
64
+ end
65
+
66
+
67
+
68
+
69
+ ######### scalar property
70
+
71
+ let (:title) {
72
+ dfn = R::Definition.new(:title)
73
+
74
+ R::Hash::Binding.new(dfn)
75
+ }
76
+
77
+ it "rendering scalar property" do
78
+ doc = {}
79
+ P[
80
+ R::Get,
81
+ R::StopOnSkipable,
82
+ R::AssignName,
83
+ R::WriteFragment
84
+ ].(nil, {represented: Song.new("Lime Green"), binding: title, doc: doc}).must_equal "Lime Green"
85
+
86
+ doc.must_equal({"title"=>"Lime Green"})
87
+ end
88
+
89
+ it "parsing scalar property" do
90
+ P[
91
+ R::AssignName,
92
+ R::ReadFragment,
93
+ R::StopOnNotFound,
94
+ R::OverwriteOnNil,
95
+ # R::SkipParse,
96
+ R::Set,
97
+ ].extend(P::Debug).(doc={"title"=>"Eruption"}, {represented: song=Song.new("Lime Green"), binding: title, doc: doc}).must_equal "Eruption"
98
+ song.title.must_equal "Eruption"
99
+ end
100
+
101
+
102
+
103
+ module ArtistRepresenter
104
+ include Representable::Hash
105
+ property :name
106
+ end
107
+
108
+ let (:artist) {
109
+ dfn = R::Definition.new(:artist, extend: ArtistRepresenter, class: Artist)
110
+
111
+ R::Hash::Binding.new(dfn)
112
+ }
113
+
114
+ let (:song_model) { Song.new("Lime Green", Artist.new("Diesel Boy")) }
115
+
116
+ it "rendering typed property" do
117
+ doc = {}
118
+ P[
119
+ R::Get,
120
+ R::StopOnSkipable,
121
+ R::StopOnNil,
122
+ R::SkipRender,
123
+ R::Decorate,
124
+ R::Serialize,
125
+ R::AssignName,
126
+ R::WriteFragment
127
+ ].extend(P::Debug).(nil, {represented: song_model, binding: artist, doc: doc, user_options: {}}).must_equal({"name" => "Diesel Boy"})
128
+
129
+ doc.must_equal({"artist"=>{"name"=>"Diesel Boy"}})
130
+ end
131
+
132
+ it "parsing typed property" do
133
+ P[
134
+ R::AssignName,
135
+ R::ReadFragment,
136
+ R::StopOnNotFound,
137
+ R::OverwriteOnNil,
138
+ # R::SkipParse,
139
+ R::CreateObject,
140
+ R::Decorate,
141
+ R::Deserialize,
142
+ R::Set,
143
+ ].extend(P::Debug).(doc={"artist"=>{"name"=>"Doobie Brothers"}}, {represented: song_model, binding: artist, doc: doc, user_options: {}}).must_equal model=Artist.new("Doobie Brothers")
144
+ song_model.artist.must_equal model
145
+ end
146
+
147
+
148
+ ######### collection :ratings
149
+
150
+ let (:ratings) {
151
+ dfn = R::Definition.new(:ratings, collection: true)
152
+
153
+ R::Hash::Binding::Collection.new(dfn)
154
+ }
155
+ it "render scalar collection" do
156
+ doc = {}
157
+ P[
158
+ R::Get,
159
+ R::StopOnSkipable,
160
+ R::Collect[
161
+ R::SkipRender,
162
+ ],
163
+ R::AssignName,
164
+ R::WriteFragment
165
+ ].extend(P::Debug).(nil, {represented: Album.new([1,2,3]), binding: ratings, doc: doc}).must_equal([1,2,3])
166
+
167
+ doc.must_equal({"ratings"=>[1,2,3]})
168
+ end
169
+
170
+ ######### collection :songs, extend: SongRepresenter
171
+ let (:artists) {
172
+ dfn = R::Definition.new(:artists, collection: true, extend: ArtistRepresenter, class: Artist)
173
+
174
+ R::Hash::Binding::Collection.new(dfn)
175
+ }
176
+ it "render typed collection" do
177
+ doc = {}
178
+ P[
179
+ R::Get,
180
+ R::StopOnSkipable,
181
+ R::Collect[
182
+ R::SkipRender,
183
+ R::Decorate,
184
+ R::Serialize,
185
+ ],
186
+ R::AssignName,
187
+ R::WriteFragment
188
+ ].extend(P::Debug).(nil, {represented: Album.new(nil, [Artist.new("Diesel Boy"), Artist.new("Van Halen")]), binding: artists, doc: doc, user_options: {}}).must_equal([{"name"=>"Diesel Boy"}, {"name"=>"Van Halen"}])
189
+
190
+ doc.must_equal({"artists"=>[{"name"=>"Diesel Boy"}, {"name"=>"Van Halen"}]})
191
+ end
192
+
193
+ let (:album_model) { Album.new(nil, [Artist.new("Diesel Boy"), Artist.new("Van Halen")]) }
194
+
195
+ it "parse typed collection" do
196
+ doc = {"artists"=>[{"name"=>"Diesel Boy"}, {"name"=>"Van Halen"}]}
197
+ P[
198
+ R::AssignName,
199
+ R::ReadFragment,
200
+ R::StopOnNotFound,
201
+ R::OverwriteOnNil,
202
+ # R::SkipParse,
203
+ R::Collect[
204
+ R::SkipRender,
205
+ R::CreateObject,
206
+ R::Decorate,
207
+ R::Deserialize,
208
+ ],
209
+ R::Set,
210
+ ].extend(P::Debug).(doc, {represented: album_model, binding: artists, doc: doc, user_options: {}}).must_equal([Artist.new("Diesel Boy"), Artist.new("Van Halen")])
211
+
212
+ album_model.artists.must_equal([Artist.new("Diesel Boy"), Artist.new("Van Halen")])
213
+ end
214
+
215
+ # TODO: test with arrays, too, not "only" Pipeline instances.
216
+ describe "#Insert Pipeline[], Function, replace: OldFunction" do
217
+ let (:pipeline) { P[R::Get, R::StopOnSkipable, R::StopOnNil] }
218
+
219
+ it "returns Pipeline instance when passing in Pipeline instance" do
220
+ P::Insert.(pipeline, R::Default, replace: R::StopOnSkipable).must_be_instance_of(R::Pipeline)
221
+ end
222
+
223
+ it "replaces if exists" do
224
+ # pipeline.insert!(R::Default, replace: R::StopOnSkipable)
225
+ P::Insert.(pipeline, R::Default, replace: R::StopOnSkipable).must_equal P[R::Get, R::Default, R::StopOnNil]
226
+ pipeline.must_equal P[R::Get, R::StopOnSkipable, R::StopOnNil]
227
+ end
228
+
229
+ it "replaces Function instance" do
230
+ pipeline = P[R::Prepare, R::StopOnSkipable, R::StopOnNil]
231
+ P::Insert.(pipeline, R::Default, replace: R::Prepare).must_equal P[R::Default, R::StopOnSkipable, R::StopOnNil]
232
+ pipeline.must_equal P[R::Prepare, R::StopOnSkipable, R::StopOnNil]
233
+ end
234
+
235
+ it "does not replace when not existing" do
236
+ P::Insert.(pipeline, R::Default, replace: R::Prepare)
237
+ pipeline.must_equal P[R::Get, R::StopOnSkipable, R::StopOnNil]
238
+ end
239
+
240
+ it "applies on nested Collect" do
241
+ pipeline = P[R::Get, R::Collect[R::Get, R::StopOnSkipable], R::StopOnNil]
242
+
243
+ P::Insert.(pipeline, R::Default, replace: R::StopOnSkipable).extend(P::Debug).inspect.must_equal "Pipeline[Get, Collect[Get, Default], StopOnNil]"
244
+ pipeline.must_equal P[R::Get, R::Collect[R::Get, R::StopOnSkipable], R::StopOnNil]
245
+
246
+
247
+ P::Insert.(pipeline, R::Default, replace: R::StopOnNil).extend(P::Debug).inspect.must_equal "Pipeline[Get, Collect[Get, StopOnSkipable], Default]"
248
+ end
249
+ end
250
+
251
+ describe "Insert delete: true" do
252
+ let(:pipeline) { P[R::Get, R::Collect[R::Get, R::StopOnSkipable], R::StopOnNil] }
253
+
254
+ it do
255
+ P::Insert.(pipeline, R::Get, delete: true).extend(P::Debug).inspect.must_equal "Pipeline[Collect[Get, StopOnSkipable], StopOnNil]"
256
+ pipeline.extend(P::Debug).inspect.must_equal "Pipeline[Get, Collect[Get, StopOnSkipable], StopOnNil]"
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,76 @@
1
+ require "test_helper"
2
+
3
+ class PopulatorFindOrInstantiateTest < MiniTest::Spec
4
+ Song = Struct.new(:id, :title, :uid) do
5
+ def self.find_by(attributes={})
6
+ return new(1, "Resist Stan", "abcd") if attributes[:id]==1# we should return the same object here
7
+ new
8
+ end
9
+ end
10
+
11
+ Composer = Struct.new(:song)
12
+ Composer.class_eval do
13
+ def song=(v)
14
+ @song = v
15
+ "Absolute nonsense" # this tests that the populator always returns the correct object.
16
+ end
17
+
18
+ attr_reader :song
19
+ end
20
+
21
+ describe "FindOrInstantiate with property" do
22
+ representer! do
23
+ property :song, populator: Representable::FindOrInstantiate, class: Song do
24
+ property :id
25
+ property :title
26
+ end
27
+ end
28
+
29
+ let (:album) { Composer.new.extend(representer) }
30
+
31
+ it "finds by :id and creates new without :id" do
32
+ album.from_hash({"song"=>{"id" => 1, "title"=>"Resist Stance"}})
33
+
34
+ album.song.title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
35
+ album.song.id.must_equal 1
36
+ album.song.uid.must_equal "abcd" # not changed via populator, indicating this is a formerly "persisted" object.
37
+ end
38
+
39
+ it "creates new without :id" do
40
+ album.from_hash({"song"=>{"title"=>"Lower"}})
41
+
42
+ album.song.title.must_equal "Lower"
43
+ album.song.id.must_equal nil
44
+ album.song.uid.must_equal nil
45
+ end
46
+ end
47
+
48
+ describe "FindOrInstantiate with collection" do
49
+ representer! do
50
+ collection :songs, populator: Representable::FindOrInstantiate, class: Song do
51
+ property :id
52
+ property :title
53
+ end
54
+ end
55
+
56
+ let (:album) { Struct.new(:songs).new([]).extend(representer) }
57
+
58
+ it "finds by :id and creates new without :id" do
59
+ album.from_hash({"songs"=>[
60
+ {"id" => 1, "title"=>"Resist Stance"},
61
+ {"title"=>"Suffer"}
62
+ ]})
63
+
64
+ album.songs[0].title.must_equal "Resist Stance" # note how title is updated from "Resist Stan"
65
+ album.songs[0].id.must_equal 1
66
+ album.songs[0].uid.must_equal "abcd" # not changed via populator, indicating this is a formerly "persisted" object.
67
+
68
+ album.songs[1].title.must_equal "Suffer"
69
+ album.songs[1].id.must_equal nil
70
+ album.songs[1].uid.must_equal nil
71
+ end
72
+
73
+ # TODO: test with existing collection
74
+ end
75
+
76
+ end
@@ -1,8 +1,21 @@
1
1
  require 'test_helper'
2
2
  require 'benchmark'
3
3
 
4
+ Kernel.class_eval do
5
+ # def respond_to_missing?
6
+ # raise
7
+ # end
8
+ # alias_method :orig_hash, :hash
9
+ # def hash
10
+ # puts "hash in #{self.class}"
11
+ # raise self.inspect if self.class == Symbol
12
+ # orig_hash
13
+ # end
14
+ end
15
+ Representable.deprecations = false
16
+
4
17
  SONG_PROPERTIES = 50.times.collect do |i|
5
- "property_#{i}"
18
+ "song_property_#{i}"
6
19
  end
7
20
 
8
21
 
@@ -12,37 +25,56 @@ module SongRepresenter
12
25
  SONG_PROPERTIES.each { |p| property p }
13
26
  end
14
27
 
15
- class SongDecorator < Representable::Decorator
28
+ class NestedProperty < Representable::Decorator
16
29
  include Representable::JSON
17
30
 
18
31
  SONG_PROPERTIES.each { |p| property p }
19
32
  end
20
33
 
21
- module AlbumRepresenter
34
+
35
+ class SongDecorator < Representable::Decorator
36
+ include Representable::JSON
37
+
38
+ SONG_PROPERTIES.each { |p| property p, extend: NestedProperty }
39
+ end
40
+
41
+ class AlbumRepresenter < Representable::Decorator
22
42
  include Representable::JSON
23
43
 
24
44
  # collection :songs, extend: SongRepresenter
25
45
  collection :songs, extend: SongDecorator
26
46
  end
27
47
 
48
+ Song = Struct.new(*SONG_PROPERTIES.map(&:to_sym))
49
+ Album = Struct.new(:songs)
50
+
28
51
  def random_song
29
- attrs = Hash[SONG_PROPERTIES.collect { |n| [n,n] }]
30
- OpenStruct.new(attrs)
52
+ Song.new(*SONG_PROPERTIES.collect { |p| Song.new(*SONG_PROPERTIES) })
31
53
  end
32
54
 
33
55
  times = []
34
56
 
35
57
  3.times.each do
36
- album = OpenStruct.new(songs: 50.times.collect { random_song })
58
+ album = Album.new(100.times.collect { random_song })
37
59
 
38
60
  times << Benchmark.measure do
39
61
  puts "================ next!"
40
- album.extend(AlbumRepresenter).to_json
62
+ AlbumRepresenter.new(album).to_json
41
63
  end
42
64
  end
43
65
 
44
66
  puts times.join("")
45
67
 
68
+ album = Album.new(100.times.collect { random_song })
69
+ require 'ruby-prof'
70
+ RubyProf.start
71
+ AlbumRepresenter.new(album).to_hash
72
+ res = RubyProf.stop
73
+ printer = RubyProf::FlatPrinter.new(res)
74
+ printer.print(array = [])
75
+
76
+ array[0..60].each { |a| puts a }
77
+
46
78
  # 100 songs, 100 attrs
47
79
  # 0.050000 0.000000 0.050000 ( 0.093157)
48
80
 
@@ -0,0 +1,21 @@
1
+ require "test_helper"
2
+
3
+ class RenderNilTest < MiniTest::Spec
4
+ Song = Struct.new(:title)
5
+
6
+ describe "render_nil: true" do
7
+ representer! do
8
+ property :title, render_nil: true
9
+ end
10
+
11
+ it { Song.new.extend(representer).to_hash.must_equal({"title"=>nil}) }
12
+ end
13
+
14
+ describe "with :extend it shouldn't extend nil" do
15
+ representer! do
16
+ property :title, render_nil: true, extend: Class
17
+ end
18
+
19
+ it { Song.new.extend(representer).to_hash.must_equal({"title"=>nil}) }
20
+ end
21
+ end