representable 2.3.0 → 2.4.0.rc1

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 (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