representable 1.3.5 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 :extendv
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
@@ -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 Extend
98
+ module Prepare
99
99
  # Extends the object with its representer before serialization.
100
100
  def serialize(*)
101
- extend_for(super)
101
+ prepare(super)
102
102
  end
103
103
 
104
104
  def deserialize(*)
105
- extend_for(super)
105
+ prepare(super)
106
106
  end
107
107
 
108
- def extend_for(object)
109
- if mod = representer_module_for(object) # :extend.
110
- object.extend(*mod)
111
- end
108
+ def prepare(object)
109
+ return object unless mod = representer_module_for(object) # :extend.
112
110
 
113
- object
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::Extend # provides #serialize/#deserialize with extend.
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
- super(create_object(data)).send(deserialize_method, data, @user_options)
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
@@ -1,3 +1,3 @@
1
1
  module Representable
2
- VERSION = "1.3.5"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -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
- @nr = options[:nr]
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", Object.new).extend(representer).to_hash(:nr => 9).must_equal({"track"=>{"nr"=>9}})
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); self.class.new hsh.upcase; end # DISCUSS: from_hash must return self.
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.3.5
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-03-18 00:00:00.000000000 Z
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
@@ -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