representable 2.2.3 → 2.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 700ffc8c90d1f0c6b824d39a42b8af702967f2f2
4
- data.tar.gz: e593985cdb6e362606b6d4dce794f5b6b4c7af50
3
+ metadata.gz: 304ec8f66cc93beff15aba84d2d4b5d5214fa5d1
4
+ data.tar.gz: 83a928ef451bd032509abb6c41d8ababda744a97
5
5
  SHA512:
6
- metadata.gz: fb0bd53d3eb6750f5ec6e238eec0335566088a9a8f2ab18482c693e3949213d97bd8873b9a0076fa174f70ad96c6db70ad0233198e1fa84c60fb3e4354e3065a
7
- data.tar.gz: daa85190ef4bdc5f93f79c161e8443df77d02bfd095231cd3bc2bd5bf4ddfc347c4210b210bea2b2e29717ee088e1ecb825360fc55ec6bd299e9a893e62d33e0
6
+ metadata.gz: 5eb021bc55145ff44e467db742d2842c8971413b44f084df9046f9eb6ee66e5250fa523fae0f8d0f3f0f3938c2e38339ca08876bcd04f6fb291ab8fb4368b0b7
7
+ data.tar.gz: fc426cbb854f6aef32a99b5bdec3e0f364176cf9ece0d35f19c43200434db12a2d8f545430f139cc8f452baf3598183ba8bd1ab53b6ce6d944512a5ac31c1fa5
@@ -1,5 +1,5 @@
1
1
  notifications:
2
- irc: "irc.freenode.org#cells"
2
+ # irc: "irc.freenode.org#cells"
3
3
  matrix:
4
4
  include:
5
5
  - rvm: 1.9.3
data/CHANGES.md CHANGED
@@ -1,3 +1,31 @@
1
+ # 2.3.0
2
+
3
+ * Remove dependency to Nokogiri and Multi_JSON. You have to add what you need to your `Gemfile`/`gemspec` now.
4
+ * `to_*`/`from_*` with options do no longer change the hash but work on copies.
5
+ * `to_*`/`from_*` now respect `wrap: false`. This will suppress the wrapping on the first level.
6
+ * Introduce `property "name", wrap: false`. This allows reusing existing representers with `representation_wrap` set as nested representers but suppressing the wrapping.
7
+
8
+ ```ruby
9
+ class BandDecorator < Representable::Decorator
10
+ include Representable::Hash
11
+ self.representation_wrap = :bands # wrap set!
12
+
13
+ property :name
14
+ end
15
+
16
+ class AlbumDecorator < Representable::Decorator
17
+ include Representable::Hash
18
+ self.representation_wrap = :albums # wrap set!
19
+
20
+ property :band, decorator: BandDecorator, wrap: false # you can now set :wrap to false!
21
+ end
22
+
23
+ album.to_hash #=> {"albums" => {"band" => {"name"=>"Social Distortion"}}}
24
+ ```
25
+
26
+ Thanks to @fighella for inspiring this feature when working on [roarify](https://github.com/fighella/roarify).
27
+ * `from_hash` no longer crashes with "NoMethodError: undefined method 'has_key?' for nil:NilClass" when an incoming nested object's value is `nil`. This was a problem with documents like `{song: nil}` where `song` is a nested property. Thanks to @mhuggins and @moxley for helping here.
28
+
1
29
  # 2.2.3
2
30
 
3
31
  * Introduce `Decorator::clone` to make sure cloning properly copies representable_attrs. in former versions, this would partially share definitions with subclasses when the decorator was an `inheritable_attr`.
data/Gemfile CHANGED
@@ -7,3 +7,8 @@ platform :rbx do
7
7
  gem "rubysl-irb"
8
8
  gem "json_pure"
9
9
  end
10
+
11
+ group :test do
12
+ gem "nokogiri", require: false
13
+ gem "multi_json", require: false
14
+ end
data/README.md CHANGED
@@ -15,6 +15,23 @@ The representable gem runs with all Ruby versions >= 1.9.3.
15
15
  gem 'representable'
16
16
  ```
17
17
 
18
+ ### Dependencies
19
+
20
+ Representable does a great job with JSON, it also features support for XML, YAML and pure ruby
21
+ hashes. But Representable did not bundle dependencies for JSON and XML.
22
+
23
+ If you want to use JSON, add the following to your Gemfile:
24
+
25
+ ```ruby
26
+ gem 'multi_json'
27
+ ```
28
+
29
+ If you want to use XML, add the following to your Gemfile:
30
+
31
+ ```ruby
32
+ gem 'nokogiri'
33
+ ```
34
+
18
35
  ## Example
19
36
 
20
37
  What if we're writing an API for music - songs, albums, bands.
@@ -176,6 +193,37 @@ Album.new.extend(AlbumRepresenter).
176
193
  #=> #<Album name="Offspring", songs=[#<Song title="Genocide">, #<Song title="Nitro", composers=["Offspring"]>]>
177
194
  ```
178
195
 
196
+ ## Suppressing Nested Wraps
197
+
198
+ When reusing a representer for a nested document, you might want to suppress the wrap for the nested fragment.
199
+
200
+ ```ruby
201
+ module SongRepresenter
202
+ include Representable::JSON
203
+
204
+ self.representation_wrap = :songs
205
+ property :title
206
+ end
207
+ ```
208
+
209
+ When reusing `SongRepresenter` in a nested setup you can suppress the wrapping using the `:wrap` option.
210
+
211
+ ```ruby
212
+ module AlbumRepresenter
213
+ include Representable::JSON
214
+
215
+ collection :songs, extend: SongRepresenter, wrap: false
216
+ end
217
+ ```
218
+
219
+ The `representation_wrap` from the nested representer now won't be rendered and parsed.
220
+
221
+ ```ruby
222
+ album.extend(AlbumRepresenter).to_json #=> "{\"songs\": [{\"name\": \"Roxanne\"}]}"
223
+ ```
224
+
225
+ Note that this only works for JSON and Hash at the moment.
226
+
179
227
 
180
228
  ## Parse Strategies
181
229
 
@@ -14,8 +14,8 @@ require "representable/serializer"
14
14
  require "representable/cached"
15
15
 
16
16
 
17
- require 'uber/callable'
18
- require 'representable/pipeline'
17
+ require "uber/callable"
18
+ require "representable/pipeline"
19
19
 
20
20
  module Representable
21
21
  attr_writer :representable_attrs
@@ -36,16 +36,16 @@ module Representable
36
36
  private
37
37
  # Reads values from +doc+ and sets properties accordingly.
38
38
  def update_properties_from(doc, options, format)
39
- private_options = normalize_options!(options)
39
+ propagated_options, private_options = normalize_options!(options)
40
40
 
41
- representable_mapper(format, options).deserialize(represented, doc, options, private_options)
41
+ representable_mapper(format, propagated_options).deserialize(represented, doc, propagated_options, private_options)
42
42
  end
43
43
 
44
44
  # Compiles the document going through all properties.
45
45
  def create_representation_with(doc, options, format)
46
- private_options = normalize_options!(options)
46
+ propagated_options, private_options = normalize_options!(options)
47
47
 
48
- representable_mapper(format, options).serialize(represented, doc, options, private_options)
48
+ representable_mapper(format, propagated_options).serialize(represented, doc, propagated_options, private_options)
49
49
  end
50
50
 
51
51
  def representable_bindings_for(format, options)
@@ -56,13 +56,20 @@ private
56
56
  format.build(definition, self)
57
57
  end
58
58
 
59
+ # Make sure we do not change original options. However, private options like :include or :wrap are
60
+ # not passed on to child representers.
59
61
  def normalize_options!(options)
60
- # TODO: ideally, private_options would be nil if none set or so, so we could save a lot of time in nested objects.
62
+ # here, we could also filter out local options e.g. like options[:band].
61
63
  private_options = {}
62
- # return private_options if options.size == 0
63
- private_options[:include] = options.delete(:include) if options[:include]
64
- private_options[:exclude] = options.delete(:exclude) if options[:exclude]
65
- private_options
64
+ return [options, private_options] if options.size == 0
65
+
66
+ propagated_options = options.dup
67
+
68
+ private_options[:include] = propagated_options.delete(:include) if options[:include]
69
+ private_options[:exclude] = propagated_options.delete(:exclude) if options[:exclude]
70
+ propagated_options.delete(:wrap) # FIXME.
71
+
72
+ [propagated_options, private_options]
66
73
  end
67
74
 
68
75
  def representable_attrs
@@ -44,6 +44,9 @@ module Representable
44
44
  @as ||= evaluate_option(:as)
45
45
  end
46
46
 
47
+ # Single entry points for rendering and parsing a property are #compile_fragment
48
+ # and #uncompile_fragment in Mapper.
49
+ #
47
50
  # DISCUSS:
48
51
  # currently, we need to call B#update! before compile_fragment/uncompile_fragment.
49
52
  # this will change to B#renderer(represented, options).call
@@ -146,15 +149,23 @@ module Representable
146
149
 
147
150
  # Note: this method is experimental.
148
151
  def update!(represented, user_options)
149
- @represented = represented
150
- @user_options = user_options
152
+ @represented = represented
151
153
 
154
+ setup_user_options!(user_options)
152
155
  setup_exec_context!
153
156
  end
154
157
 
155
158
  attr_accessor :cached_representer
156
159
 
157
160
  private
161
+ def setup_user_options!(user_options)
162
+ @user_options = user_options
163
+ # this is the propagated_options.
164
+ @user_options = user_options.merge(wrap: false) if self[:wrap] == false
165
+
166
+ # @user_options = user_options.merge(wrap: self[:wrap]) if self[:wrap]
167
+ end
168
+
158
169
  # 1.80 0.066 0.027 0.000 0.039 30002 Representable::Binding#setup_exec_context!
159
170
  # 0.98 0.034 0.014 0.000 0.020 30002 Representable::Binding#setup_exec_context!
160
171
  def setup_exec_context!
@@ -88,10 +88,6 @@ module Representable
88
88
  value
89
89
  end
90
90
 
91
- # def binding_cache
92
- # @binding_cache ||= {}
93
- # end
94
-
95
91
  private
96
92
  def infer_name_for(name)
97
93
  name.to_s.split('::').last.
@@ -2,6 +2,10 @@ require "representable"
2
2
 
3
3
  module Representable
4
4
  class Decorator
5
+ class << self
6
+ include Cloneable
7
+ end
8
+
5
9
  attr_reader :represented
6
10
  alias_method :decorated, :represented
7
11
 
@@ -10,7 +10,6 @@ module Representable
10
10
  alias_method :getter, :name
11
11
 
12
12
  def initialize(sym, options={}, &block)
13
- @options = {}
14
13
  @options = Cloneable::Hash.new # allows deep cloning. we then had to set Pipeline cloneable.
15
14
  @name = sym.to_s
16
15
  options = options.clone
@@ -15,6 +15,7 @@ module Representable
15
15
 
16
16
  def call(fragment, *args) # FIXME: args is always i.
17
17
  return fragment unless @binding.typed? # customize with :extend. this is not really straight-forward.
18
+ return fragment if fragment.nil?
18
19
 
19
20
  # what if create_object is responsible for providing the deserialize-to object?
20
21
  object = create_object(fragment, *args) # customize with :instance and :class.
@@ -22,9 +22,6 @@ module Representable
22
22
  end
23
23
 
24
24
 
25
- # Note: `#from_hash` still does _not_ stringify incoming hashes. This is per design: Representable is not made for hashes, only,
26
- # but for any arbitrary data structure. A generic `key.to_s` with non-hash data would result in weird issues.
27
- # I decided it's more predictable to require the user to provide stringified keys.
28
25
  def from_hash(data, options={}, binding_builder=Binding)
29
26
  data = filter_wrap(data, options)
30
27
 
@@ -34,6 +31,7 @@ module Representable
34
31
  def to_hash(options={}, binding_builder=Binding)
35
32
  hash = create_representation_with({}, options, binding_builder)
36
33
 
34
+ return hash if options[:wrap] == false # TODO: same for parse.
37
35
  return hash unless wrap = options[:wrap] || representation_wrap(options)
38
36
 
39
37
  {wrap => hash}
@@ -41,6 +39,7 @@ module Representable
41
39
 
42
40
  private
43
41
  def filter_wrap(data, options)
42
+ return data if options[:wrap] == false
44
43
  return data unless wrap = options[:wrap] || representation_wrap(options)
45
44
  filter_wrap_for(data, wrap)
46
45
  end
@@ -11,7 +11,7 @@ module Representable
11
11
  end
12
12
 
13
13
  def read(hash)
14
- return FragmentNotFound unless hash.has_key?(as) # DISCUSS: put it all in #read for performance. not really sure if i like returning that special thing.
14
+ return FragmentNotFound unless hash.has_key?(as)
15
15
 
16
16
  hash[as] # fragment
17
17
  end
@@ -1,7 +1,12 @@
1
- require 'multi_json'
2
1
  require 'representable/hash'
3
2
  require 'representable/json/collection'
4
3
 
4
+ begin
5
+ require 'multi_json'
6
+ rescue LoadError => _
7
+ abort "Missing dependency 'multi_json' for Representable::JSON. See dependencies section in README.md for details."
8
+ end
9
+
5
10
  module Representable
6
11
  # Brings #to_json and #from_json to your object.
7
12
  module JSON
@@ -18,7 +18,7 @@ module Representable
18
18
  value = @binding[:default]
19
19
  else
20
20
  # DISCUSS: should we return a Skip object instead of this block trick? (same in Binding#serialize?)
21
- value = deserialize(fragment) { return } # stop here if skip_parse?
21
+ value = deserialize(fragment) { return } # stop here if skip_parse.
22
22
  end
23
23
 
24
24
  value = @binding.parse_filter(value, doc)
@@ -1,3 +1,3 @@
1
1
  module Representable
2
- VERSION = "2.2.3"
2
+ VERSION = "2.3.0"
3
3
  end
@@ -1,7 +1,12 @@
1
1
  require 'representable'
2
2
  require 'representable/xml/binding'
3
3
  require 'representable/xml/collection'
4
- require 'nokogiri'
4
+
5
+ begin
6
+ require 'nokogiri'
7
+ rescue LoadError => _
8
+ abort "Missing dependency 'nokogiri' for Representable::XML. See dependencies section in README.md for details."
9
+ end
5
10
 
6
11
  module Representable
7
12
  module XML
@@ -20,8 +20,6 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
  s.license = "MIT"
22
22
 
23
- s.add_dependency "nokogiri"
24
- s.add_dependency "multi_json"
25
23
  s.add_dependency "uber", "~> 0.0.7"
26
24
 
27
25
  s.add_development_dependency "rake"
@@ -40,4 +40,26 @@ class AsTest < MiniTest::Spec
40
40
  it { parse(song, {"[{:volume=>1}]" => "Wie Es Geht"}, :volume => 1).name.must_equal "Wie Es Geht" }
41
41
  end
42
42
  end
43
+ end
44
+
45
+
46
+ # hash: to_hash(wrap: ) is representation_wrap
47
+
48
+ class AsXmlTest < MiniTest::Spec
49
+ Band = Struct.new(:name, :label)
50
+ Album = Struct.new(:band)
51
+ Label = Struct.new(:name)
52
+
53
+ representer!(module: Representable::XML, decorator: true) do
54
+ self.representation_wrap = :album
55
+ property :band, as: :combo do
56
+ self.representation_wrap = :band
57
+ property :name
58
+ end
59
+ end
60
+
61
+ it do
62
+ skip
63
+ representer.new(Album.new(Band.new("Offspring"))).to_xml.must_equal ""
64
+ end
43
65
  end
@@ -24,9 +24,12 @@ class ConfigInheritTest < MiniTest::Spec
24
24
  # in Decorator ------------------------------------------------
25
25
  class Decorator < Representable::Decorator
26
26
  property :title
27
+ property :artist do
28
+ property :id
29
+ end
27
30
  end
28
31
 
29
- it { Decorator.representable_attrs[:definitions].keys.must_equal ["title"] }
32
+ it { Decorator.representable_attrs[:definitions].keys.must_equal ["title", "artist"] }
30
33
 
31
34
  # in inheriting Decorator
32
35
 
@@ -34,8 +37,11 @@ class ConfigInheritTest < MiniTest::Spec
34
37
  property :location
35
38
  end
36
39
 
37
- it { InheritingDecorator.representable_attrs[:definitions].keys.must_equal ["title", "location"] }
40
+ it { InheritingDecorator.representable_attrs[:definitions].keys.must_equal ["title", "artist", "location"] }
38
41
  it { assert_cloned(InheritingDecorator, Decorator, "title") }
42
+ it do
43
+ InheritingDecorator.representable_attrs.get(:artist).representer_module.object_id.wont_equal Decorator.representable_attrs.get(:artist).representer_module.object_id
44
+ end
39
45
 
40
46
  # in inheriting and including Decorator
41
47
 
@@ -44,7 +50,7 @@ class ConfigInheritTest < MiniTest::Spec
44
50
  property :location
45
51
  end
46
52
 
47
- it { InheritingAndIncludingDecorator.representable_attrs[:definitions].keys.must_equal ["title", "genre", "location"] }
53
+ it { InheritingAndIncludingDecorator.representable_attrs[:definitions].keys.must_equal ["title", "artist", "genre", "location"] }
48
54
  it { assert_cloned(InheritingAndIncludingDecorator, GenreModule, :genre) }
49
55
 
50
56
 
@@ -1,149 +1,160 @@
1
1
  require 'test_helper'
2
2
 
3
- class HashTest < MiniTest::Spec
4
- def self.hash_representer(&block)
5
- Module.new do
6
- include Representable::Hash
7
- instance_exec &block
3
+ class HashWithScalarPropertyTest < MiniTest::Spec
4
+ Album = Struct.new(:title)
5
+
6
+ representer! do
7
+ property :title
8
+ end
9
+
10
+ let (:album) { Album.new("Liar") }
11
+
12
+ describe "#to_hash" do
13
+ it "renders plain property" do
14
+ album.extend(representer).to_hash.must_equal("title" => "Liar")
8
15
  end
9
16
  end
10
17
 
11
- def hash_representer(&block)
12
- self.class.hash_representer(&block)
18
+
19
+ describe "#from_hash" do
20
+ it "parses plain property" do
21
+ album.extend(representer).from_hash("title" => "This Song Is Recycled").title.must_equal "This Song Is Recycled"
22
+ end
23
+
24
+ # Fixes issue #115
25
+ it "allows nil value in the incoming document and corresponding nil value for the represented" do
26
+ album = Album.new
27
+ album.title.must_equal nil
28
+ album.extend(representer).from_hash("title" => nil)
29
+ end
13
30
  end
31
+ end
32
+
14
33
 
34
+ class HashWithTypedPropertyTest < MiniTest::Spec
35
+ Album = Struct.new(:best_song)
15
36
 
16
- describe "property" do
17
- let (:hsh) { hash_representer do property :best_song end }
37
+ representer! do
38
+ property :best_song, :class => Song do
39
+ property :name
40
+ end
41
+ end
18
42
 
19
- let (:album) { Album.new.tap do |album|
20
- album.best_song = "Liar"
21
- end }
43
+ let (:album) { Album.new(Song.new("Liar")) }
22
44
 
23
- describe "#to_hash" do
24
- it "renders plain property" do
25
- album.extend(hsh).to_hash.must_equal("best_song" => "Liar")
26
- end
45
+ describe "#to_hash" do
46
+ it "renders embedded typed property" do
47
+ album.extend(representer).to_hash.must_equal("best_song" => {"name" => "Liar"})
27
48
  end
49
+ end
28
50
 
51
+ describe "#from_hash" do
52
+ it "parses embedded typed property" do
53
+ album.extend(representer).from_hash("best_song" => {"name" => "Go With Me"})
54
+ album.best_song.name.must_equal "Go With Me"
55
+ end
29
56
 
30
- describe "#from_hash" do
31
- it "parses plain property" do
32
- album.extend(hsh).from_hash("best_song" => "This Song Is Recycled").best_song.must_equal "This Song Is Recycled"
33
- end
57
+ # nested nil removes nested object.
58
+ it do
59
+ album = Album.new(Song.new("Pre-medicated Murder"))
60
+ album.extend(representer).from_hash("best_song" => nil)
61
+ album.best_song.must_equal nil
34
62
  end
35
63
 
64
+ # nested blank hash creates blank object when not populated.
65
+ it do
66
+ album = Album.new#(Song.new("Pre-medicated Murder"))
67
+ album.extend(representer).from_hash("best_song" => {})
68
+ album.best_song.name.must_equal nil
69
+ end
36
70
 
37
- describe "with :class and :extend" do
38
- hash_song = hash_representer do property :name end
39
- let (:hash_album) { Module.new do
40
- include Representable::Hash
41
- property :best_song, :extend => hash_song, :class => Song
42
- end }
71
+ # Fixes issue #115
72
+ it "allows nil value in the incoming document and corresponding nil value for the represented" do
73
+ album = Album.new
74
+ album.extend(representer).from_hash("best_song" => nil)
75
+ album.best_song.must_equal nil
76
+ end
77
+ end
78
+ end
43
79
 
44
- let (:album) { Album.new.tap do |album|
45
- album.best_song = Song.new("Liar")
46
- end }
80
+ # TODO: move to AsTest.
81
+ class HashWithTypedPropertyAndAs < MiniTest::Spec
82
+ representer! do
83
+ property :song, :class => Song, :as => :hit do
84
+ property :name
85
+ end
86
+ end
47
87
 
88
+ let (:album) { OpenStruct.new(:song => Song.new("Liar")).extend(representer) }
48
89
 
49
- describe "#to_hash" do
50
- it "renders embedded typed property" do
51
- album.extend(hash_album).to_hash.must_equal("best_song" => {"name" => "Liar"})
52
- end
53
- end
90
+ it { album.to_hash.must_equal("hit" => {"name" => "Liar"}) }
91
+ it { album.from_hash("hit" => {"name" => "Go With Me"}).must_equal OpenStruct.new(:song => Song.new("Go With Me")) }
92
+ end
93
+ # # describe "FIXME COMBINE WITH ABOVE with :extend and :as" do
94
+ # # hash_song = Module.new do
95
+ # # include Representable::XML
96
+ # # self.representation_wrap = :song
97
+ # # property :name
98
+ # # end
54
99
 
55
- describe "#from_hash" do
56
- it "parses embedded typed property" do
57
- album.extend(hash_album).from_hash("best_song" => {"name" => "Go With Me"}).must_equal Album.new(nil,Song.new("Go With Me"))
58
- end
59
- end
60
- end
100
+ # # let (:hash_album) { Module.new do
101
+ # # include Representable::XML
102
+ # # self.representation_wrap = :album
103
+ # # property :song, :extend => hash_song, :class => Song, :as => :hit
104
+ # # end }
105
+
106
+ # # let (:album) { OpenStruct.new(:song => Song.new("Liar")).extend(hash_album) }
61
107
 
108
+ # # it { album.to_xml.must_equal_xml("<album><hit><name>Liar</name></hit></album>") }
109
+ # # #it { album.from_hash("hit" => {"name" => "Go With Me"}).must_equal OpenStruct.new(:song => Song.new("Go With Me")) }
110
+ # # end
111
+ # end
62
112
 
63
- describe "with :extend and :as" do
64
- hash_song = hash_representer do property :name end
65
113
 
66
- let (:hash_album) { Module.new do
67
- include Representable::Hash
68
- property :song, :extend => hash_song, :class => Song, :as => :hit
69
- end }
70
114
 
71
- let (:album) { OpenStruct.new(:song => Song.new("Liar")).extend(hash_album) }
115
+ class HashWithTypedCollectionTest < MiniTest::Spec
116
+ Album = Struct.new(:songs)
72
117
 
73
- it { album.to_hash.must_equal("hit" => {"name" => "Liar"}) }
74
- it { album.from_hash("hit" => {"name" => "Go With Me"}).must_equal OpenStruct.new(:song => Song.new("Go With Me")) }
118
+ representer! do
119
+ collection :songs, class: Song do
120
+ property :name
121
+ property :track
75
122
  end
76
- # describe "FIXME COMBINE WITH ABOVE with :extend and :as" do
77
- # hash_song = Module.new do
78
- # include Representable::XML
79
- # self.representation_wrap = :song
80
- # property :name
81
- # end
82
-
83
- # let (:hash_album) { Module.new do
84
- # include Representable::XML
85
- # self.representation_wrap = :album
86
- # property :song, :extend => hash_song, :class => Song, :as => :hit
87
- # end }
88
-
89
- # let (:album) { OpenStruct.new(:song => Song.new("Liar")).extend(hash_album) }
90
-
91
- # it { album.to_xml.must_equal_xml("<album><hit><name>Liar</name></hit></album>") }
92
- # #it { album.from_hash("hit" => {"name" => "Go With Me"}).must_equal OpenStruct.new(:song => Song.new("Go With Me")) }
93
- # end
94
123
  end
95
124
 
125
+ let (:album) { Album.new([Song.new("Liar", 1), Song.new("What I Know", 2)]) }
96
126
 
97
- describe "collection" do
98
- let (:hsh) { hash_representer do collection :songs end }
99
-
100
- let (:album) { Album.new.tap do |album|
101
- album.songs = ["Jackhammer", "Terrible Man"]
102
- end }
103
-
127
+ describe "#to_hash" do
128
+ it "renders collection of typed property" do
129
+ album.extend(representer).to_hash.must_equal("songs" => [{"name" => "Liar", "track" => 1}, {"name" => "What I Know", "track" => 2}])
130
+ end
131
+ end
104
132
 
105
- describe "#to_hash" do
106
- it "renders a block style list per default" do
107
- album.extend(hsh).to_hash.must_equal("songs" => ["Jackhammer", "Terrible Man"])
108
- end
133
+ describe "#from_hash" do
134
+ it "parses collection of typed property" do
135
+ album.extend(representer).from_hash("songs" => [{"name" => "One Shot Deal", "track" => 4},
136
+ {"name" => "Three Way Dance", "track" => 5}]).must_equal Album.new([Song.new("One Shot Deal", 4), Song.new("Three Way Dance", 5)])
109
137
  end
138
+ end
139
+ end
140
+
141
+ class HashWithScalarCollectionTest < MiniTest::Spec
142
+ Album = Struct.new(:songs)
143
+ representer! { collection :songs }
110
144
 
145
+ let (:album) { Album.new(["Jackhammer", "Terrible Man"]) }
111
146
 
112
- describe "#from_hash" do
113
- it "parses a block style list" do
114
- album.extend(hsh).from_hash("songs" => ["Off Key Melody", "Sinking"]).must_equal Album.new(["Off Key Melody", "Sinking"])
115
147
 
116
- end
148
+ describe "#to_hash" do
149
+ it "renders a block style list per default" do
150
+ album.extend(representer).to_hash.must_equal("songs" => ["Jackhammer", "Terrible Man"])
117
151
  end
152
+ end
118
153
 
119
154
 
120
- describe "with :class and :extend" do
121
- hash_song = hash_representer do
122
- property :name
123
- property :track
124
- end
125
- let (:hash_album) { Module.new do
126
- include Representable::Hash
127
- collection :songs, :extend => hash_song, :class => Song
128
- end }
129
-
130
- let (:album) { Album.new.tap do |album|
131
- album.songs = [Song.new("Liar", 1), Song.new("What I Know", 2)]
132
- end }
133
-
134
-
135
- describe "#to_hash" do
136
- it "renders collection of typed property" do
137
- album.extend(hash_album).to_hash.must_equal("songs" => [{"name" => "Liar", "track" => 1}, {"name" => "What I Know", "track" => 2}])
138
- end
139
- end
140
-
141
- describe "#from_hash" do
142
- it "parses collection of typed property" do
143
- album.extend(hash_album).from_hash("songs" => [{"name" => "One Shot Deal", "track" => 4},
144
- {"name" => "Three Way Dance", "track" => 5}]).must_equal Album.new([Song.new("One Shot Deal", 4), Song.new("Three Way Dance", 5)])
145
- end
146
- end
155
+ describe "#from_hash" do
156
+ it "parses a block style list" do
157
+ album.extend(representer).from_hash("songs" => ["Off Key Melody", "Sinking"]).must_equal Album.new(["Off Key Melody", "Sinking"])
147
158
  end
148
159
  end
149
160
  end
@@ -0,0 +1,18 @@
1
+ require "test_helper"
2
+
3
+ class PrivateOptionsTest < MiniTest::Spec # TODO: move me to separate file.
4
+ representer!(decorator: true) do
5
+ end
6
+
7
+ options = {exclude: "name"}
8
+
9
+ it "render: doesn't modify options" do
10
+ representer.new(nil).to_hash(options)
11
+ options.must_equal({exclude: "name"})
12
+ end
13
+
14
+ it "parse: doesn't modify options" do
15
+ representer.new(nil).from_hash(options)
16
+ options.must_equal({exclude: "name"})
17
+ end
18
+ end
@@ -193,13 +193,22 @@ class RepresentableTest < MiniTest::Spec
193
193
  end
194
194
  end
195
195
 
196
+ class Hometown
197
+ attr_accessor :name
198
+ end
199
+
200
+ module HometownRepresentable
201
+ include Representable::JSON
202
+ property :name
203
+ end
196
204
 
197
205
  # DISCUSS: i don't like the JSON requirement here, what about some generic test module?
198
206
  class PopBand
199
207
  include Representable::JSON
200
208
  property :name
201
209
  property :groupies
202
- attr_accessor :name, :groupies
210
+ property :hometown, class: Hometown, extend: HometownRepresentable
211
+ attr_accessor :name, :groupies, :hometown
203
212
  end
204
213
 
205
214
  describe "#update_properties_from" do
@@ -249,6 +258,7 @@ class RepresentableTest < MiniTest::Spec
249
258
  @band.from_hash({})
250
259
  end
251
260
 
261
+
252
262
  # FIXME: do we need this test with XML _and_ JSON?
253
263
  it "ignores (no-default) properties not present in the incoming document" do
254
264
  { Representable::Hash => [:from_hash, {}],
@@ -46,3 +46,107 @@ class WrapTest < MiniTest::Spec
46
46
  end
47
47
  end
48
48
  end
49
+
50
+
51
+ class HashDisableWrapTest < MiniTest::Spec
52
+ Band = Struct.new(:name, :label)
53
+ Album = Struct.new(:band)
54
+ Label = Struct.new(:name)
55
+
56
+ class BandDecorator < Representable::Decorator
57
+ include Representable::Hash
58
+
59
+ self.representation_wrap = :bands
60
+ property :name
61
+
62
+ property :label do # this should have a wrap!
63
+ self.representation_wrap = :important
64
+ property :name
65
+ end
66
+ end
67
+
68
+ let (:band) { BandDecorator.prepare(Band.new("Social Distortion")) }
69
+
70
+ # direct, local api.
71
+ it do
72
+ band.to_hash.must_equal({"bands" => {"name"=>"Social Distortion"}})
73
+ band.to_hash(wrap: false).must_equal({"name"=>"Social Distortion"})
74
+ band.to_hash(wrap: :band).must_equal(:band=>{"name"=>"Social Distortion"})
75
+ end
76
+
77
+ it do
78
+ band.from_hash({"bands" => {"name"=>"Social Distortion"}}).name.must_equal "Social Distortion"
79
+ band.from_hash({"name"=>"Social Distortion"}, wrap: false).name.must_equal "Social Distortion"
80
+ band.from_hash({band: {"name"=>"Social Distortion"}}, wrap: :band).name.must_equal "Social Distortion"
81
+ end
82
+
83
+
84
+ class AlbumDecorator < Representable::Decorator
85
+ include Representable::Hash
86
+
87
+ self.representation_wrap = :albums
88
+
89
+ property :band, decorator: BandDecorator, wrap: false, class: Band
90
+ end
91
+
92
+
93
+ let (:album) { AlbumDecorator.prepare(Album.new(Band.new("Social Distortion", Label.new("Epitaph")))) }
94
+
95
+ # band has wrap turned of per property definition, however, label still has wrap.
96
+ it "renders" do
97
+ album.to_hash.must_equal({"albums" => {"band" => {"name"=>"Social Distortion", "label"=>{"important"=>{"name"=>"Epitaph"}}}}})
98
+ end
99
+
100
+ it "parses" do
101
+ album.from_hash({"albums" => {"band" => {"name"=>"Rvivr"}}}).band.name.must_equal "Rvivr"
102
+ end
103
+ end
104
+
105
+
106
+ class XMLDisableWrapTest < MiniTest::Spec
107
+ Band = Struct.new(:name, :label)
108
+ Album = Struct.new(:band)
109
+ Label = Struct.new(:name)
110
+
111
+ class BandDecorator < Representable::Decorator
112
+ include Representable::XML
113
+
114
+ self.representation_wrap = :bands # when nested, it uses this.
115
+ property :name
116
+
117
+ # property :label do # this should have a wrap!
118
+ # self.representation_wrap = :important
119
+ # property :name
120
+ # end
121
+ end
122
+
123
+ let (:band) { BandDecorator.prepare(Band.new("Social Distortion")) }
124
+
125
+ it do
126
+ band.to_xml.must_equal_xml "<bands><name>Social Distortion</name></bands>"
127
+ band.to_xml(wrap: "combo").must_equal_xml "<combo><name>Social Distortion</name></combo>"
128
+ end
129
+
130
+
131
+ class AlbumDecorator < Representable::Decorator
132
+ include Representable::XML
133
+
134
+ self.representation_wrap = :albums
135
+
136
+ property :band, decorator: BandDecorator, wrap: "po", class: Band
137
+ end
138
+
139
+
140
+ let (:album) { AlbumDecorator.prepare(Album.new(Band.new("Social Distortion", Label.new("Epitaph")))) }
141
+
142
+ # band has wrap turned of per property definition, however, label still has wrap.
143
+ it "rendersxx" do
144
+ skip
145
+ album.to_xml.must_equal({"albums" => {"band" => {"name"=>"Social Distortion", "label"=>{"important"=>{"name"=>"Epitaph"}}}}})
146
+ end
147
+
148
+ # it "parses" do
149
+ # album.from_hash({"albums" => {"band" => {"name"=>"Rvivr"}}}).band.name.must_equal "Rvivr"
150
+ # end
151
+ end
152
+
metadata CHANGED
@@ -1,43 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: representable
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.3
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-20 00:00:00.000000000 Z
11
+ date: 2015-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: nokogiri
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: multi_json
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
13
  - !ruby/object:Gem::Dependency
42
14
  name: uber
43
15
  requirement: !ruby/object:Gem::Requirement
@@ -240,6 +212,7 @@ files:
240
212
  - test/parse_strategy_test.rb
241
213
  - test/pass_options_test.rb
242
214
  - test/prepare_test.rb
215
+ - test/private_options_test.rb
243
216
  - test/reader_writer_test.rb
244
217
  - test/realistic_benchmark.rb
245
218
  - test/represent_test.rb
@@ -274,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
274
247
  version: '0'
275
248
  requirements: []
276
249
  rubyforge_project:
277
- rubygems_version: 2.2.2
250
+ rubygems_version: 2.4.8
278
251
  signing_key:
279
252
  specification_version: 4
280
253
  summary: Renders and parses JSON/XML/YAML documents from and to Ruby objects. Includes