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.
@@ -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