representable 1.3.5 → 1.4.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.
- data/CHANGES.textile +5 -0
- data/README.md +30 -0
- data/TODO +5 -5
- data/lib/representable.rb +21 -6
- data/lib/representable/binding.rb +14 -11
- data/lib/representable/decorator.rb +19 -0
- data/lib/representable/definition.rb +15 -15
- data/lib/representable/version.rb +1 -1
- data/test/representable_test.rb +103 -10
- metadata +3 -3
- data/test/polymorphic_test.rb +0 -45
data/CHANGES.textile
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
h2. 1.4.0
|
2
|
+
|
3
|
+
* We now have two strategies for representing: the old extend approach and the brand-new decorator which leaves represented objects untouched. See "README":https://github.com/apotonick/representable#decorator-vs-extend for details.
|
4
|
+
* Internally, either extending or decorating in the Binding now happens through the representer class method `::prepare` (i.e. `Decorator::prepare` or `Representable::prepare` for modules). That means any representer module or class must expose this class method.
|
5
|
+
|
1
6
|
h2. 1.3.5
|
2
7
|
|
3
8
|
* Added `:reader` and `:writer` to allow overriding rendering/parsing of a property fragment and to give the user access to the entire document.
|
data/README.md
CHANGED
@@ -55,6 +55,12 @@ It also adds support for parsing.
|
|
55
55
|
song = Song.new.extend(SongRepresenter).from_json(%{ {"title":"Roxanne"} })
|
56
56
|
#=> #<Song title="Roxanne", track=nil>
|
57
57
|
|
58
|
+
|
59
|
+
## Extend vs. Decorator
|
60
|
+
|
61
|
+
If you don't want representer modules to be mixed into your objects (using `#extend`) you can use the `Decorator` strategy [described below](https://github.com/apotonick/representable#decorator-vs-extend). Decorating instead of extending was introduced in 1.4.
|
62
|
+
|
63
|
+
|
58
64
|
## Aliasing
|
59
65
|
|
60
66
|
If your property name doesn't match the name in the document, use the `:as` option.
|
@@ -146,6 +152,30 @@ Parsing a documents needs both `:extend` and the `:class` option as the parser r
|
|
146
152
|
|
147
153
|
#=> #<Album name="Offspring", songs=[#<Song title="Genocide">, #<Song title="Nitro", composers=["Offspring"]>]>
|
148
154
|
|
155
|
+
## Decorator vs. Extend
|
156
|
+
|
157
|
+
People who dislike `:extend` go use the `Decorator` strategy!
|
158
|
+
|
159
|
+
class SongRepresentation < Representable::Decorator
|
160
|
+
include Representable::JSON
|
161
|
+
|
162
|
+
property :title
|
163
|
+
property :track
|
164
|
+
end
|
165
|
+
|
166
|
+
The `Decorator` constructor requires the represented object.
|
167
|
+
|
168
|
+
SongRepresentation.new(song).to_json
|
169
|
+
|
170
|
+
This will leave the `song` instance untouched as the decorator just uses public accessors to represent the hit.
|
171
|
+
|
172
|
+
In compositions you need to specify the decorators for the nested items using the `:decorator` option where you'd normally use `:extend`.
|
173
|
+
|
174
|
+
class AlbumRepresentation < Representable::Decorator
|
175
|
+
include Representable::JSON
|
176
|
+
|
177
|
+
collection :songs, :class => Song, :decorator => SongRepresentation
|
178
|
+
end
|
149
179
|
|
150
180
|
## XML Support
|
151
181
|
|
data/TODO
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
* Pass key/index as first block arg to :class and :
|
1
|
+
* Pass key/index as first block arg to :class and :extend
|
2
2
|
class: |key, hsh|
|
3
3
|
|
4
4
|
* Allow passing options to Binding#serialize.
|
5
5
|
serialize(.., options{:exclude => ..})
|
6
6
|
|
7
|
-
* Allow :writer and :reader / :getter/:setter
|
8
|
-
|
9
7
|
document `XML::AttributeHash` etc
|
10
8
|
|
11
9
|
* deprecate :from, make :a(lia)s authorative.
|
@@ -16,7 +14,7 @@ document `XML::AttributeHash` etc
|
|
16
14
|
|
17
15
|
* have representable-options (:include, :exclude) and user-options
|
18
16
|
|
19
|
-
|
17
|
+
* make all properties "Object-like", even arrays of strings etc.
|
20
18
|
|
21
19
|
|
22
20
|
def compile_fragment(doc)
|
@@ -25,4 +23,6 @@ module ReaderWriter
|
|
25
23
|
do whatever
|
26
24
|
super
|
27
25
|
end
|
28
|
-
=> do that for all "features" (what parts would that be?: getter/setter, reader/writer, readable/writeable )?
|
26
|
+
=> do that for all "features" (what parts would that be?: getter/setter, reader/writer, readable/writeable )?
|
27
|
+
|
28
|
+
* alias :extend with :decorator
|
data/lib/representable.rb
CHANGED
@@ -87,7 +87,7 @@ private
|
|
87
87
|
args = []
|
88
88
|
args << binding.user_options if condition.arity > 0 # TODO: remove arity check. users should know whether they pass options or not.
|
89
89
|
|
90
|
-
not instance_exec(*args, &condition)
|
90
|
+
not represented.instance_exec(*args, &condition)
|
91
91
|
end
|
92
92
|
|
93
93
|
# TODO: remove in 1.4.
|
@@ -104,17 +104,26 @@ private
|
|
104
104
|
@representable_attrs ||= self.class.representable_attrs # DISCUSS: copy, or better not?
|
105
105
|
end
|
106
106
|
|
107
|
-
def representable_bindings_for(format, options)
|
108
|
-
options = cleanup_options(options) # FIXME: make representable-options and user-options two different hashes.
|
109
|
-
representable_attrs.map {|attr| format.build(attr, self, options) }
|
110
|
-
end
|
111
|
-
|
112
107
|
# Returns the wrapper for the representation. Mostly used in XML.
|
113
108
|
def representation_wrap
|
114
109
|
representable_attrs.wrap_for(self.class.name)
|
115
110
|
end
|
116
111
|
|
117
112
|
private
|
113
|
+
def representable_bindings_for(format, options)
|
114
|
+
options = cleanup_options(options) # FIXME: make representable-options and user-options two different hashes.
|
115
|
+
representable_attrs.map {|attr| representable_binding_for(attr, format, options) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def representable_binding_for(attribute, format, options)
|
119
|
+
# DISCUSS: shouldn't this happen in Binding?
|
120
|
+
format.build(attribute, represented, options)
|
121
|
+
end
|
122
|
+
|
123
|
+
def represented
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
118
127
|
def cleanup_options(options) # TODO: remove me.
|
119
128
|
options.reject { |k,v| [:include, :exclude].include?(k) }
|
120
129
|
end
|
@@ -143,6 +152,10 @@ private
|
|
143
152
|
end
|
144
153
|
end
|
145
154
|
|
155
|
+
def prepare(represented)
|
156
|
+
represented.extend(self) # was: PrepareStrategy::Extend.
|
157
|
+
end
|
158
|
+
|
146
159
|
|
147
160
|
module Declarations
|
148
161
|
def representable_attrs
|
@@ -227,3 +240,5 @@ private
|
|
227
240
|
end
|
228
241
|
end
|
229
242
|
end
|
243
|
+
|
244
|
+
require 'representable/decorator'
|
@@ -93,24 +93,23 @@ module Representable
|
|
93
93
|
end
|
94
94
|
|
95
95
|
|
96
|
-
# Hooks into #serialize and #deserialize to extend typed properties
|
96
|
+
# Hooks into #serialize and #deserialize to setup (extend/decorate) typed properties
|
97
97
|
# at runtime.
|
98
|
-
module
|
98
|
+
module Prepare
|
99
99
|
# Extends the object with its representer before serialization.
|
100
100
|
def serialize(*)
|
101
|
-
|
101
|
+
prepare(super)
|
102
102
|
end
|
103
103
|
|
104
104
|
def deserialize(*)
|
105
|
-
|
105
|
+
prepare(super)
|
106
106
|
end
|
107
107
|
|
108
|
-
def
|
109
|
-
|
110
|
-
object.extend(*mod)
|
111
|
-
end
|
108
|
+
def prepare(object)
|
109
|
+
return object unless mod = representer_module_for(object) # :extend.
|
112
110
|
|
113
|
-
|
111
|
+
mod = mod.first if mod.is_a?(Array) # TODO: deprecate :extend => [..]
|
112
|
+
mod.prepare(object)
|
114
113
|
end
|
115
114
|
|
116
115
|
private
|
@@ -125,8 +124,10 @@ module Representable
|
|
125
124
|
end
|
126
125
|
end
|
127
126
|
|
127
|
+
# Overrides #serialize/#deserialize to call #to_*/from_*.
|
128
|
+
# Computes :class in #deserialize. # TODO: shouldn't this be in a separate module? ObjectSerialize/ObjectDeserialize?
|
128
129
|
module Object
|
129
|
-
include Binding::
|
130
|
+
include Binding::Prepare
|
130
131
|
|
131
132
|
def serialize(object)
|
132
133
|
return object if object.nil?
|
@@ -136,7 +137,9 @@ module Representable
|
|
136
137
|
|
137
138
|
def deserialize(data)
|
138
139
|
# DISCUSS: does it make sense to skip deserialization of nil-values here?
|
139
|
-
|
140
|
+
create_object(data).tap do |obj|
|
141
|
+
super(obj).send(deserialize_method, data, @user_options)
|
142
|
+
end
|
140
143
|
end
|
141
144
|
|
142
145
|
def create_object(fragment)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Representable
|
2
|
+
class Decorator
|
3
|
+
attr_reader :represented
|
4
|
+
|
5
|
+
def self.prepare(represented)
|
6
|
+
new(represented) # was: PrepareStrategy::Decorate.
|
7
|
+
end
|
8
|
+
|
9
|
+
include Representable # include after class methods so Decorator::prepare can't be overwritten by Representable::prepare.
|
10
|
+
|
11
|
+
def initialize(represented)
|
12
|
+
@represented = represented
|
13
|
+
end
|
14
|
+
|
15
|
+
def representable_binding_for(attr, format, options)
|
16
|
+
format.build(attr, represented, options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -3,14 +3,14 @@ module Representable
|
|
3
3
|
class Definition
|
4
4
|
attr_reader :name, :options
|
5
5
|
alias_method :getter, :name
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(sym, options={})
|
8
8
|
@name = sym.to_s
|
9
9
|
@options = options
|
10
|
-
|
10
|
+
|
11
11
|
@options[:default] ||= [] if array? # FIXME: move to CollectionBinding!
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def clone
|
15
15
|
self.class.new(name, options.clone) # DISCUSS: make generic Definition.cloned_attribute that passes list to constructor.
|
16
16
|
end
|
@@ -18,48 +18,48 @@ module Representable
|
|
18
18
|
def setter
|
19
19
|
:"#{name}="
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def typed?
|
23
23
|
sought_type.is_a?(Class) or representer_module or options[:instance] # also true if only :extend is set, for people who want solely rendering.
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
def array?
|
27
27
|
options[:collection]
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
def hash?
|
31
31
|
options[:hash]
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
def sought_type
|
35
35
|
options[:class]
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def from
|
39
39
|
(options[:from] || options[:as] || name).to_s
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def default_for(value)
|
43
43
|
return default if skipable_nil_value?(value)
|
44
44
|
value
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
def has_default?
|
48
48
|
options.has_key?(:default)
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
def representer_module
|
52
|
-
options[:extend]
|
52
|
+
options[:extend] or options[:decorator]
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
def attribute
|
56
56
|
options[:attribute]
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
def skipable_nil_value?(value)
|
60
60
|
value.nil? and not options[:render_nil]
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
def default
|
64
64
|
options[:default]
|
65
65
|
end
|
data/test/representable_test.rb
CHANGED
@@ -303,6 +303,10 @@ class RepresentableTest < MiniTest::Spec
|
|
303
303
|
end
|
304
304
|
|
305
305
|
describe "passing options" do
|
306
|
+
class Track
|
307
|
+
attr_accessor :nr
|
308
|
+
end
|
309
|
+
|
306
310
|
module TrackRepresenter
|
307
311
|
include Representable::Hash
|
308
312
|
property :nr
|
@@ -312,25 +316,25 @@ class RepresentableTest < MiniTest::Spec
|
|
312
316
|
super
|
313
317
|
end
|
314
318
|
def from_hash(data, options)
|
315
|
-
super
|
316
|
-
|
319
|
+
super.tap do
|
320
|
+
@nr = options[:nr]
|
321
|
+
end
|
317
322
|
end
|
318
|
-
attr_accessor :nr
|
319
323
|
end
|
320
324
|
|
321
325
|
representer! do
|
322
|
-
property :track, :extend => TrackRepresenter
|
326
|
+
property :track, :extend => TrackRepresenter, :class => Track
|
323
327
|
end
|
324
328
|
|
325
329
|
describe "#to_hash" do
|
326
330
|
it "propagates to nested objects" do
|
327
|
-
Song.new("Ocean Song",
|
331
|
+
Song.new("Ocean Song", Track.new).extend(representer).to_hash(:nr => 9).must_equal({"track"=>{"nr"=>9}})
|
328
332
|
end
|
329
333
|
end
|
330
334
|
|
331
335
|
describe "#from_hash" do
|
332
336
|
it "propagates to nested objects" do
|
333
|
-
Song.new.extend(representer).from_hash({"track"=>{"nr" => "replace me"}}, :nr => 9).track.must_equal 9
|
337
|
+
Song.new.extend(representer).from_hash({"track"=>{"nr" => "replace me"}}, :nr => 9).track.nr.must_equal 9
|
334
338
|
end
|
335
339
|
end
|
336
340
|
end
|
@@ -452,6 +456,31 @@ class RepresentableTest < MiniTest::Spec
|
|
452
456
|
assert_equal "oh yes", band.fame
|
453
457
|
end
|
454
458
|
|
459
|
+
describe "executing :if lambda in represented instance context" do
|
460
|
+
representer! do
|
461
|
+
property :label, :if => lambda { signed_contract }
|
462
|
+
end
|
463
|
+
|
464
|
+
subject { OpenStruct.new(:signed_contract => false, :label => "Fat") }
|
465
|
+
|
466
|
+
it "skips when false" do
|
467
|
+
subject.extend(representer).to_hash.must_equal({})
|
468
|
+
end
|
469
|
+
|
470
|
+
it "represents when true" do
|
471
|
+
subject.signed_contract= true
|
472
|
+
subject.extend(representer).to_hash.must_equal({"label"=>"Fat"})
|
473
|
+
end
|
474
|
+
|
475
|
+
it "works with decorator" do
|
476
|
+
rpr = representer
|
477
|
+
Class.new(Representable::Decorator) do
|
478
|
+
include rpr
|
479
|
+
end.new(subject).to_hash.must_equal({})
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
|
455
484
|
describe "propagating user options to the block" do
|
456
485
|
representer! do
|
457
486
|
property :name, :if => lambda { |opts| opts[:include_name] }
|
@@ -506,12 +535,14 @@ class RepresentableTest < MiniTest::Spec
|
|
506
535
|
|
507
536
|
describe ":extend and :class" do
|
508
537
|
module UpcaseRepresenter
|
538
|
+
include Representable
|
509
539
|
def to_hash(*); upcase; end
|
510
|
-
def from_hash(hsh, *args);
|
540
|
+
def from_hash(hsh, *args); replace hsh.upcase; end # DISCUSS: from_hash must return self.
|
511
541
|
end
|
512
542
|
module DowncaseRepresenter
|
543
|
+
include Representable
|
513
544
|
def to_hash(*); downcase; end
|
514
|
-
def from_hash(hsh, *args); hsh.downcase; end
|
545
|
+
def from_hash(hsh, *args); replace hsh.downcase; end
|
515
546
|
end
|
516
547
|
class UpcaseString < String; end
|
517
548
|
|
@@ -533,7 +564,7 @@ class RepresentableTest < MiniTest::Spec
|
|
533
564
|
|
534
565
|
describe ":instance" do
|
535
566
|
obj = String.new("Fate")
|
536
|
-
mod = Module.new { def from_hash(*); self; end }
|
567
|
+
mod = Module.new { include Representable; def from_hash(*); self; end }
|
537
568
|
representer! do
|
538
569
|
property :name, :extend => mod, :instance => lambda { |name| obj }
|
539
570
|
end
|
@@ -578,7 +609,7 @@ class RepresentableTest < MiniTest::Spec
|
|
578
609
|
|
579
610
|
describe "when :class lambda returns nil" do
|
580
611
|
representer! do
|
581
|
-
property :name, :extend => lambda { |name| Module.new { def from_hash(data, *args); data; end } },
|
612
|
+
property :name, :extend => lambda { |name| Module.new { include Representable; def from_hash(data, *args); data; end } },
|
582
613
|
:class => nil
|
583
614
|
end
|
584
615
|
|
@@ -618,6 +649,16 @@ class RepresentableTest < MiniTest::Spec
|
|
618
649
|
end
|
619
650
|
end
|
620
651
|
|
652
|
+
describe ":decorator" do
|
653
|
+
let (:extend_rpr) { Module.new { include Representable::Hash; collection :songs, :extend => SongRepresenter } }
|
654
|
+
let (:decorator_rpr) { Module.new { include Representable::Hash; collection :songs, :decorator => SongRepresenter } }
|
655
|
+
let (:songs) { [Song.new("Bloody Mary")] }
|
656
|
+
|
657
|
+
it "is aliased to :extend" do
|
658
|
+
Album.new(songs).extend(extend_rpr).to_hash.must_equal Album.new(songs).extend(decorator_rpr).to_hash
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
621
662
|
describe ":binding" do
|
622
663
|
representer! do
|
623
664
|
class MyBinding < Representable::Binding
|
@@ -633,6 +674,58 @@ class RepresentableTest < MiniTest::Spec
|
|
633
674
|
end
|
634
675
|
end
|
635
676
|
|
677
|
+
describe "decorator" do
|
678
|
+
# TODO: Move to global place since it's used twice.
|
679
|
+
class SongRepresentation < Representable::Decorator
|
680
|
+
include Representable::JSON
|
681
|
+
property :name
|
682
|
+
end
|
683
|
+
|
684
|
+
class AlbumRepresentation < Representable::Decorator
|
685
|
+
include Representable::JSON
|
686
|
+
|
687
|
+
collection :songs, :class => Song, :extend => SongRepresentation
|
688
|
+
end
|
689
|
+
|
690
|
+
let (:song) { Song.new("Mama, I'm Coming Home") }
|
691
|
+
let (:album) { Album.new([song]) }
|
692
|
+
let (:decorator) { AlbumRepresentation.new(album) }
|
693
|
+
|
694
|
+
it "renders" do
|
695
|
+
decorator.to_hash.must_equal({"songs"=>[{"name"=>"Mama, I'm Coming Home"}]})
|
696
|
+
album.wont_respond_to :to_hash
|
697
|
+
song.wont_respond_to :to_hash # DISCUSS: weak test, how to assert blank slate?
|
698
|
+
end
|
699
|
+
|
700
|
+
it "parses" do
|
701
|
+
decorator.from_hash({"songs"=>[{"name"=>"Atomic Garden"}]})
|
702
|
+
album.songs.first.must_be_kind_of Song
|
703
|
+
album.songs.must_equal [Song.new("Atomic Garden")]
|
704
|
+
album.wont_respond_to :to_hash
|
705
|
+
song.wont_respond_to :to_hash # DISCUSS: weak test, how to assert blank slate?
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
describe "::prepare" do
|
710
|
+
let (:song) { Song.new("Still Friends In The End") }
|
711
|
+
let (:album) { Album.new([song]) }
|
712
|
+
|
713
|
+
describe "module including Representable" do
|
714
|
+
it "uses :extend strategy" do
|
715
|
+
album_rpr = Module.new { include Representable::Hash; collection :songs, :class => Song, :extend => SongRepresenter}
|
716
|
+
|
717
|
+
album_rpr.prepare(album).to_hash.must_equal({"songs"=>[{"name"=>"Still Friends In The End"}]})
|
718
|
+
album.must_respond_to :to_hash
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
describe "Decorator subclass" do
|
723
|
+
it "uses :decorate strategy" do
|
724
|
+
AlbumRepresentation.prepare(album).to_hash.must_equal({"songs"=>[{"name"=>"Still Friends In The End"}]})
|
725
|
+
album.wont_respond_to :to_hash
|
726
|
+
end
|
727
|
+
end
|
728
|
+
end
|
636
729
|
end
|
637
730
|
|
638
731
|
describe "Config" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: representable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-04-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: nokogiri
|
@@ -176,6 +176,7 @@ files:
|
|
176
176
|
- lib/representable/bindings/xml_bindings.rb
|
177
177
|
- lib/representable/bindings/yaml_bindings.rb
|
178
178
|
- lib/representable/coercion.rb
|
179
|
+
- lib/representable/decorator.rb
|
179
180
|
- lib/representable/definition.rb
|
180
181
|
- lib/representable/deprecations.rb
|
181
182
|
- lib/representable/feature/readable_writeable.rb
|
@@ -198,7 +199,6 @@ files:
|
|
198
199
|
- test/hash_test.rb
|
199
200
|
- test/json_test.rb
|
200
201
|
- test/mongoid_test.rb
|
201
|
-
- test/polymorphic_test.rb
|
202
202
|
- test/representable_test.rb
|
203
203
|
- test/test_helper.rb
|
204
204
|
- test/test_helper_test.rb
|
data/test/polymorphic_test.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
module PolymorphicExtender
|
4
|
-
def self.extended(model)
|
5
|
-
model.extend(representer_name_for(model))
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.representer_name_for(model)
|
9
|
-
PolymorphicTest.const_get("#{model.class.to_s.split("::").last}Representer")
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
|
14
|
-
class PolymorphicTest < MiniTest::Spec
|
15
|
-
class PopSong < Song
|
16
|
-
end
|
17
|
-
|
18
|
-
module SongRepresenter
|
19
|
-
include Representable::JSON
|
20
|
-
property :name
|
21
|
-
end
|
22
|
-
|
23
|
-
module PopSongRepresenter
|
24
|
-
include Representable::JSON
|
25
|
-
property :name, :from => "known_as"
|
26
|
-
end
|
27
|
-
|
28
|
-
class Album
|
29
|
-
attr_accessor :songs
|
30
|
-
end
|
31
|
-
|
32
|
-
module AlbumRepresenter
|
33
|
-
include Representable::JSON
|
34
|
-
collection :songs, :extend => PolymorphicExtender
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
describe "PolymorphicExtender" do
|
39
|
-
it "extends each model with the correct representer in #to_json" do
|
40
|
-
album = Album.new
|
41
|
-
album.songs = [PopSong.new("Let Me Down"), Song.new("The 4 Horsemen")]
|
42
|
-
assert_equal "{\"songs\":[{\"known_as\":\"Let Me Down\"},{\"name\":\"The 4 Horsemen\"}]}", album.extend(AlbumRepresenter).to_json
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|