representable 2.2.3 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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