representable 1.2.9 → 1.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.
- data/CHANGES.textile +6 -0
- data/README.md +485 -0
- data/TODO +11 -1
- data/lib/representable.rb +13 -22
- data/lib/representable/binding.rb +32 -4
- data/lib/representable/bindings/hash_bindings.rb +4 -4
- data/lib/representable/bindings/xml_bindings.rb +13 -10
- data/lib/representable/bindings/yaml_bindings.rb +3 -6
- data/lib/representable/deprecations.rb +0 -7
- data/lib/representable/hash_methods.rb +2 -2
- data/lib/representable/json/collection.rb +2 -2
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml/collection.rb +4 -4
- data/test/example.rb +152 -21
- data/test/json_test.rb +3 -2
- data/test/representable_test.rb +56 -20
- data/test/test_helper.rb +9 -0
- data/test/xml_test.rb +34 -12
- metadata +3 -5
- data/.rspec +0 -1
- data/LICENSE +0 -20
- data/README.rdoc +0 -416
data/test/json_test.rb
CHANGED
@@ -146,8 +146,9 @@ module JsonTest
|
|
146
146
|
|
147
147
|
describe "#representable_bindings_for" do
|
148
148
|
it "returns bindings for each property" do
|
149
|
-
|
150
|
-
assert_equal
|
149
|
+
bins = @band.send(:representable_bindings_for, Representable::JSON::PropertyBinding, {})
|
150
|
+
assert_equal 2, bins.size
|
151
|
+
assert_equal "name", bins.first.name
|
151
152
|
end
|
152
153
|
end
|
153
154
|
end
|
data/test/representable_test.rb
CHANGED
@@ -1,15 +1,6 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class RepresentableTest < MiniTest::Spec
|
4
|
-
def self.representer!(name=:representer, &block)
|
5
|
-
let(name) do
|
6
|
-
Module.new do
|
7
|
-
include Representable::Hash
|
8
|
-
instance_exec(&block)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
4
|
class Band
|
14
5
|
include Representable
|
15
6
|
property :name
|
@@ -257,10 +248,6 @@ class RepresentableTest < MiniTest::Spec
|
|
257
248
|
assert_equal nil, @band.groupies
|
258
249
|
end
|
259
250
|
|
260
|
-
it "still accepts deprecated :except option" do # FIXME: remove :except option.
|
261
|
-
assert_equal @band.update_properties_from({"name"=>"No One's Choice", "groupies"=>2}, {:except => [:groupies]}, Representable::Hash::PropertyBinding), @band.update_properties_from({"name"=>"No One's Choice", "groupies"=>2}, {:exclude => [:groupies]}, Representable::Hash::PropertyBinding)
|
262
|
-
end
|
263
|
-
|
264
251
|
it "accepts :include option" do
|
265
252
|
@band.update_properties_from({"name"=>"No One's Choice", "groupies"=>2}, {:include => [:groupies]}, Representable::Hash::PropertyBinding)
|
266
253
|
assert_equal 2, @band.groupies
|
@@ -282,6 +269,13 @@ class RepresentableTest < MiniTest::Spec
|
|
282
269
|
@band.update_properties_from({"groupies"=>false}, {}, Representable::Hash::PropertyBinding)
|
283
270
|
assert_equal false, @band.groupies
|
284
271
|
end
|
272
|
+
|
273
|
+
it "ignores properties not present in the incoming document" do
|
274
|
+
@band.instance_eval do
|
275
|
+
def name=(*); raise "I should never be called!"; end
|
276
|
+
end
|
277
|
+
@band.update_properties_from({}, {}, Representable::Hash::PropertyBinding)
|
278
|
+
end
|
285
279
|
|
286
280
|
it "ignores (no-default) properties not present in the incoming document" do
|
287
281
|
{ Representable::JSON => [{}, Representable::Hash::PropertyBinding],
|
@@ -302,6 +296,39 @@ class RepresentableTest < MiniTest::Spec
|
|
302
296
|
assert_equal nil, @band.name, "Failed in #{format}"
|
303
297
|
end
|
304
298
|
end
|
299
|
+
|
300
|
+
describe "passing options" do
|
301
|
+
module TrackRepresenter
|
302
|
+
include Representable::Hash
|
303
|
+
property :nr
|
304
|
+
|
305
|
+
def to_hash(options)
|
306
|
+
@nr = options[:nr]
|
307
|
+
super
|
308
|
+
end
|
309
|
+
def from_hash(data, options)
|
310
|
+
super
|
311
|
+
@nr = options[:nr]
|
312
|
+
end
|
313
|
+
attr_accessor :nr
|
314
|
+
end
|
315
|
+
|
316
|
+
representer! do
|
317
|
+
property :track, :extend => TrackRepresenter
|
318
|
+
end
|
319
|
+
|
320
|
+
describe "#to_hash" do
|
321
|
+
it "propagates to nested objects" do
|
322
|
+
Song.new("Ocean Song", Object.new).extend(representer).to_hash(:nr => 9).must_equal({"track"=>{"nr"=>9}})
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
describe "#from_hash" do
|
327
|
+
it "propagates to nested objects" do
|
328
|
+
Song.new.extend(representer).from_hash({"track"=>{"nr" => "replace me"}}, :nr => 9).track.must_equal 9
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
305
332
|
end
|
306
333
|
|
307
334
|
describe "#create_representation_with" do
|
@@ -320,10 +347,6 @@ class RepresentableTest < MiniTest::Spec
|
|
320
347
|
assert_equal({"name"=>"No One's Choice"}, hash)
|
321
348
|
end
|
322
349
|
|
323
|
-
it "still accepts deprecated :except option" do # FIXME: remove :except option.
|
324
|
-
assert_equal @band.send(:create_representation_with, {}, {:except => [:groupies]}, Representable::Hash::PropertyBinding), @band.send(:create_representation_with, {}, {:exclude => [:groupies]}, Representable::Hash::PropertyBinding)
|
325
|
-
end
|
326
|
-
|
327
350
|
it "accepts :include option" do
|
328
351
|
hash = @band.send(:create_representation_with, {}, {:include => [:groupies]}, Representable::Hash::PropertyBinding)
|
329
352
|
assert_equal({"groupies"=>2}, hash)
|
@@ -375,6 +398,19 @@ class RepresentableTest < MiniTest::Spec
|
|
375
398
|
assert_equal({"name"=>"No One's Choice", "groupies" => nil}, hash)
|
376
399
|
end
|
377
400
|
end
|
401
|
+
|
402
|
+
it "does not propagate private options to nested objects" do
|
403
|
+
cover_rpr = Module.new do
|
404
|
+
include Representable::Hash
|
405
|
+
property :title
|
406
|
+
property :original, :extend => self
|
407
|
+
end
|
408
|
+
|
409
|
+
# FIXME: we should test all representable-options (:include, :exclude, ?)
|
410
|
+
|
411
|
+
Class.new(OpenStruct).new(:title => "Roxanne", :original => Class.new(OpenStruct).new(:title => "Roxanne (Don't Put On The Red Light)")).extend(cover_rpr).
|
412
|
+
to_hash(:include => [:original]).must_equal({"original"=>{"title"=>"Roxanne (Don't Put On The Red Light)"}})
|
413
|
+
end
|
378
414
|
end
|
379
415
|
|
380
416
|
describe ":if" do
|
@@ -416,11 +452,11 @@ class RepresentableTest < MiniTest::Spec
|
|
416
452
|
describe ":extend and :class" do
|
417
453
|
module UpcaseRepresenter
|
418
454
|
def to_hash(*); upcase; end
|
419
|
-
def from_hash(hsh); self.class.new hsh.upcase; end # DISCUSS: from_hash must return self.
|
455
|
+
def from_hash(hsh, *args); self.class.new hsh.upcase; end # DISCUSS: from_hash must return self.
|
420
456
|
end
|
421
457
|
module DowncaseRepresenter
|
422
458
|
def to_hash(*); downcase; end
|
423
|
-
def from_hash(hsh); hsh.downcase; end
|
459
|
+
def from_hash(hsh, *args); hsh.downcase; end
|
424
460
|
end
|
425
461
|
class UpcaseString < String; end
|
426
462
|
|
@@ -487,7 +523,7 @@ class RepresentableTest < MiniTest::Spec
|
|
487
523
|
|
488
524
|
describe "when :class lambda returns nil" do
|
489
525
|
representer! do
|
490
|
-
property :name, :extend => lambda { |name| Module.new { def from_hash(data); data; end } },
|
526
|
+
property :name, :extend => lambda { |name| Module.new { def from_hash(data, *args); data; end } },
|
491
527
|
:class => nil
|
492
528
|
end
|
493
529
|
|
data/test/test_helper.rb
CHANGED
@@ -53,4 +53,13 @@ end
|
|
53
53
|
MiniTest::Spec.class_eval do
|
54
54
|
include AssertJson::Assertions
|
55
55
|
include XmlHelper
|
56
|
+
|
57
|
+
def self.representer!(format=Representable::Hash, name=:representer, &block)
|
58
|
+
let(name) do
|
59
|
+
Module.new do
|
60
|
+
include format
|
61
|
+
instance_exec(&block)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
56
65
|
end
|
data/test/xml_test.rb
CHANGED
@@ -378,41 +378,63 @@ class CollectionTest < MiniTest::Spec
|
|
378
378
|
end
|
379
379
|
end
|
380
380
|
end
|
381
|
-
|
381
|
+
|
382
|
+
require 'representable/xml/collection'
|
383
|
+
class CollectionRepresenterTest < MiniTest::Spec
|
384
|
+
module SongRepresenter
|
385
|
+
include Representable::XML
|
386
|
+
property :name
|
387
|
+
end
|
388
|
+
|
389
|
+
describe "XML::Collection" do
|
390
|
+
describe "with contained objects" do
|
391
|
+
representer!(Representable::XML::Collection) do
|
392
|
+
items :class => Song, :extend => SongRepresenter
|
393
|
+
self.representation_wrap= :songs
|
394
|
+
end
|
395
|
+
|
396
|
+
it "renders objects with #to_xml" do
|
397
|
+
assert_xml_equal "<songs><song><name>Days Go By</name></song><song><name>Can't Take Them All</name></song></songs>", [Song.new("Days Go By"), Song.new("Can't Take Them All")].extend(representer).to_xml
|
398
|
+
end
|
399
|
+
|
400
|
+
it "returns objects array from #from_xml" do
|
401
|
+
assert_equal [Song.new("Days Go By"), Song.new("Can't Take Them All")], [].extend(representer).from_xml("<songs><song><name>Days Go By</name></song><song><name>Can't Take Them All</name></song></songs>")
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
382
407
|
require 'representable/xml/hash'
|
383
408
|
describe "XML::AttributeHash" do # TODO: move to HashTest.
|
384
|
-
|
385
|
-
|
386
|
-
include Representable::XML::AttributeHash
|
387
|
-
self.representation_wrap= :favs
|
388
|
-
end
|
409
|
+
representer!(Representable::XML::AttributeHash) do
|
410
|
+
self.representation_wrap= :favs
|
389
411
|
end
|
390
412
|
|
391
413
|
describe "#to_xml" do
|
392
414
|
it "renders values into attributes converting values to strings" do
|
393
|
-
assert_xml_equal "<favs one=\"Graveyards\" two=\"Can't Take Them All\" />", {:one => :Graveyards, :two => "Can't Take Them All"}.extend(
|
415
|
+
assert_xml_equal "<favs one=\"Graveyards\" two=\"Can't Take Them All\" />", {:one => :Graveyards, :two => "Can't Take Them All"}.extend(representer).to_xml
|
394
416
|
end
|
395
417
|
|
396
418
|
it "respects :exclude" do
|
397
|
-
assert_xml_equal "<favs two=\"Can't Take Them All\" />", {:one => :Graveyards, :two => "Can't Take Them All"}.extend(
|
419
|
+
assert_xml_equal "<favs two=\"Can't Take Them All\" />", {:one => :Graveyards, :two => "Can't Take Them All"}.extend(representer).to_xml(:exclude => [:one])
|
398
420
|
end
|
399
421
|
|
400
422
|
it "respects :include" do
|
401
|
-
assert_xml_equal "<favs two=\"Can't Take Them All\" />", {:one => :Graveyards, :two => "Can't Take Them All"}.extend(
|
423
|
+
assert_xml_equal "<favs two=\"Can't Take Them All\" />", {:one => :Graveyards, :two => "Can't Take Them All"}.extend(representer).to_xml(:include => [:two])
|
402
424
|
end
|
403
425
|
end
|
404
426
|
|
405
427
|
describe "#from_json" do
|
406
428
|
it "returns hash" do
|
407
|
-
assert_equal({"one" => "Graveyards", "two" => "Can't Take Them All"}, {}.extend(
|
429
|
+
assert_equal({"one" => "Graveyards", "two" => "Can't Take Them All"}, {}.extend(representer).from_xml("<favs one=\"Graveyards\" two=\"Can't Take Them All\" />"))
|
408
430
|
end
|
409
431
|
|
410
432
|
it "respects :exclude" do
|
411
|
-
assert_equal({"two" => "Can't Take Them All"}, {}.extend(
|
433
|
+
assert_equal({"two" => "Can't Take Them All"}, {}.extend(representer).from_xml("<favs one=\"Graveyards\" two=\"Can't Take Them All\" />", :exclude => [:one]))
|
412
434
|
end
|
413
435
|
|
414
436
|
it "respects :include" do
|
415
|
-
assert_equal({"one" => "Graveyards"}, {}.extend(
|
437
|
+
assert_equal({"one" => "Graveyards"}, {}.extend(representer).from_xml("<favs one=\"Graveyards\" two=\"Can't Take Them All\" />", :include => [:one]))
|
416
438
|
end
|
417
439
|
end
|
418
440
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: representable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.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-01-
|
12
|
+
date: 2013-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: nokogiri
|
@@ -148,12 +148,10 @@ extensions: []
|
|
148
148
|
extra_rdoc_files: []
|
149
149
|
files:
|
150
150
|
- .gitignore
|
151
|
-
- .rspec
|
152
151
|
- .travis.yml
|
153
152
|
- CHANGES.textile
|
154
153
|
- Gemfile
|
155
|
-
-
|
156
|
-
- README.rdoc
|
154
|
+
- README.md
|
157
155
|
- Rakefile
|
158
156
|
- TODO
|
159
157
|
- gemfiles/Gemfile.mongoid-2.4
|
data/.rspec
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
--colour
|
data/LICENSE
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
Copyright (c) 2004-2009 Ben Woosley, Zak Mandhro and Anders Engstrom
|
2
|
-
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
a copy of this software and associated documentation files (the
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
the following conditions:
|
10
|
-
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
included in all copies or substantial portions of the Software.
|
13
|
-
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
DELETED
@@ -1,416 +0,0 @@
|
|
1
|
-
= Representable
|
2
|
-
|
3
|
-
<em>Maps documents to Ruby objects and back.</em>
|
4
|
-
|
5
|
-
|
6
|
-
== Introduction
|
7
|
-
|
8
|
-
_Representable_ maps fragments in documents to attributes in Ruby objects and back. It allows parsing representations giving an object-oriented interface to the document. But that's only half of it! Representable can also render documents from an object instance.
|
9
|
-
|
10
|
-
This keeps your representation knowledge in one place when implementing REST services and clients.
|
11
|
-
|
12
|
-
|
13
|
-
== Features
|
14
|
-
|
15
|
-
* Bidirectional - rendering and parsing
|
16
|
-
* OOP access to documents
|
17
|
-
* Support for JSON, XML and YAML.
|
18
|
-
* Coercion support with virtus[https://github.com/solnic/virtus]
|
19
|
-
|
20
|
-
|
21
|
-
== Example
|
22
|
-
|
23
|
-
Since you keep forgetting the heroes of your childhood you decide to implement a REST service for storing and querying those. You choose representable for handling representations.
|
24
|
-
|
25
|
-
gem 'representable'
|
26
|
-
|
27
|
-
|
28
|
-
== Defining Representations
|
29
|
-
|
30
|
-
Representations are usually defined using a module. This makes them super flexibly, you'll see.
|
31
|
-
|
32
|
-
require 'representable/json'
|
33
|
-
|
34
|
-
module HeroRepresenter
|
35
|
-
include Representable::JSON
|
36
|
-
|
37
|
-
property :forename
|
38
|
-
property :surename
|
39
|
-
end
|
40
|
-
|
41
|
-
By using #property we declare two simple attributes that should be considered when representing.
|
42
|
-
|
43
|
-
To use your representer include it in the matching class. Note that you could reuse a representer in multiple classes. The represented class must have getter and setter methods for each property.
|
44
|
-
|
45
|
-
class Hero
|
46
|
-
attr_accessor :forename, :surename
|
47
|
-
|
48
|
-
include Representable
|
49
|
-
include HeroRepresenter
|
50
|
-
end
|
51
|
-
|
52
|
-
Many people dislike including representers on class layer. You might also extend an object at runtime.
|
53
|
-
|
54
|
-
Hero.new.extend(HeroRepresenter)
|
55
|
-
|
56
|
-
Alternatively, if you don't like modules (which you shouldn't), declarations can be put into classes directly. We call that inline representers.
|
57
|
-
|
58
|
-
class Hero
|
59
|
-
attr_accessor :forename, :surename
|
60
|
-
|
61
|
-
include Representable::JSON
|
62
|
-
|
63
|
-
property :forename
|
64
|
-
property :surename
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
== Rendering
|
69
|
-
|
70
|
-
Now let's create and render our first hero.
|
71
|
-
|
72
|
-
peter = Hero.new
|
73
|
-
peter.forename = "Peter"
|
74
|
-
peter.surename = "Pan"
|
75
|
-
|
76
|
-
peter.to_json
|
77
|
-
#=> {"forename":"Peter","surename":"Pan"}
|
78
|
-
|
79
|
-
Those two properties are considered when rendering in #to_json.
|
80
|
-
|
81
|
-
== Parsing
|
82
|
-
|
83
|
-
The cool thing about Representable is: it works bidirectional. The document definition in your representer module can also be used for parsing documents and assigning property values.
|
84
|
-
|
85
|
-
hook = Hero.from_json('{"forename":"Captain","surename":"Hook"}')
|
86
|
-
hook.forename #=> "Captain"
|
87
|
-
|
88
|
-
See how easy this is? You can use an object-oriented method to read from the document.
|
89
|
-
|
90
|
-
== Nesting
|
91
|
-
|
92
|
-
You need a second domain object. Every hero has a place it comes from.
|
93
|
-
|
94
|
-
class Location
|
95
|
-
attr_accessor :title
|
96
|
-
|
97
|
-
include Representable::JSON
|
98
|
-
|
99
|
-
property :title
|
100
|
-
end
|
101
|
-
|
102
|
-
Peter, where ya' from?
|
103
|
-
|
104
|
-
neverland = Location.new
|
105
|
-
neverland.title = "Neverland"
|
106
|
-
|
107
|
-
It makes sense to embed the location in the hero's document.
|
108
|
-
|
109
|
-
module HeroRepresenter
|
110
|
-
property :origin, :class => Location
|
111
|
-
end
|
112
|
-
|
113
|
-
Using the +:class+ option allows you to include other representable objects.
|
114
|
-
|
115
|
-
peter.origin = neverland
|
116
|
-
peter.to_json
|
117
|
-
#=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"}}
|
118
|
-
|
119
|
-
|
120
|
-
== Parsing Nested Documents
|
121
|
-
|
122
|
-
Don't forget how easy it is to parse nested representations.
|
123
|
-
|
124
|
-
hook = Hero.from_json('{"name":"Captain","surename":"Hook","origin":{"title":"Dark Ocean"}}')
|
125
|
-
hook.origin.inspect #=> #<Location:0x910d7c8 @title="Dark Ocean">
|
126
|
-
hook.origin.title #=> "Dark Ocean"
|
127
|
-
|
128
|
-
Representable just creates objects from the parsed document - nothing more and nothing less.
|
129
|
-
|
130
|
-
== Simple Collections
|
131
|
-
|
132
|
-
Heroes have features, special abilities that make 'em a superhero.
|
133
|
-
|
134
|
-
module HeroRepresenter
|
135
|
-
collection :features
|
136
|
-
end
|
137
|
-
|
138
|
-
The second representable API method is +collection+ and, well, declares a collection.
|
139
|
-
|
140
|
-
peter.features = ["stays young", "can fly"]
|
141
|
-
peter.to_json
|
142
|
-
#=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"]}
|
143
|
-
|
144
|
-
|
145
|
-
== Typed Collections
|
146
|
-
|
147
|
-
Ok, things start working out. Your hero has a name, an origin and a list of features so far. Why not allow adding buddies to Peter - nobody wants to be alone!
|
148
|
-
|
149
|
-
module HeroRepresenter
|
150
|
-
collection :friends, :class => Hero
|
151
|
-
end
|
152
|
-
|
153
|
-
Again, we type the collection by using the +:class+ option.
|
154
|
-
|
155
|
-
nick = Hero.new
|
156
|
-
nick.forename = "Nick"
|
157
|
-
|
158
|
-
el = Hero.new
|
159
|
-
el.forename = "El"
|
160
|
-
|
161
|
-
peter.friends = [nick, el]
|
162
|
-
|
163
|
-
I always wanted to be Peter's bro... in this example it is possible!
|
164
|
-
|
165
|
-
peter.to_json
|
166
|
-
#=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"],"friends":[{"name":"Nick"},{"name":"El"}]}
|
167
|
-
|
168
|
-
|
169
|
-
== Hashes
|
170
|
-
|
171
|
-
Hashes can be represented the same way collections work. Here, use the #hash class method.
|
172
|
-
|
173
|
-
== Lonely Collections
|
174
|
-
|
175
|
-
Need an array represented without any wrapping?
|
176
|
-
|
177
|
-
["stays young", "can fly"].extend(Representable::JSON::Collection).to_json
|
178
|
-
#=> "[\"stays young\", \"can fly\"]"
|
179
|
-
|
180
|
-
You can use #items to configure the element representations contained in the array.
|
181
|
-
|
182
|
-
module FeaturesRepresenter
|
183
|
-
include Representable::JSON::Collection
|
184
|
-
|
185
|
-
items :class => Hero, :extend => HeroRepresenter
|
186
|
-
end
|
187
|
-
|
188
|
-
Collections and hashes can also be deserialized. Note that this also works for XML.
|
189
|
-
|
190
|
-
== Lonely Hashes
|
191
|
-
|
192
|
-
The same goes with hashes where #values lets you configure the hash's values.
|
193
|
-
|
194
|
-
module FriendsRepresenter
|
195
|
-
include Representable::JSON::Hash
|
196
|
-
|
197
|
-
values :class => Hero, :extend => HeroRepresenter
|
198
|
-
end
|
199
|
-
|
200
|
-
{:stu => Hero.new("Stu"), :clive => Hero.new("Cleavage")}.extend(FriendsRepresenter).to_json
|
201
|
-
|
202
|
-
In XML, if you want to store hash attributes in tag attributes instead of dedicated nodes, use XML::AttributeHash.
|
203
|
-
|
204
|
-
|
205
|
-
== Customizing
|
206
|
-
|
207
|
-
=== Wrapping
|
208
|
-
|
209
|
-
Representable is designed to be very simple. However, a few tweaks are available. What if you want to wrap your document?
|
210
|
-
|
211
|
-
module HeroRepresenter
|
212
|
-
self.representation_wrap = true
|
213
|
-
end
|
214
|
-
|
215
|
-
peter.to_json #=> {"hero":{"name":"Peter","surename":"Pan"}}
|
216
|
-
|
217
|
-
You can also provide a custom wrapper.
|
218
|
-
|
219
|
-
module HeroRepresenter
|
220
|
-
self.representation_wrap = :boy
|
221
|
-
end
|
222
|
-
|
223
|
-
peter.to_json #=> {"boy":{"name":"Peter","surename":"Pan"}}
|
224
|
-
|
225
|
-
|
226
|
-
=== Mapping
|
227
|
-
|
228
|
-
If your accessor name doesn't match the attribute name in the document, use the +:from+ matcher.
|
229
|
-
|
230
|
-
module HeroRepresenter
|
231
|
-
property :forename, :from => :i_am_called
|
232
|
-
end
|
233
|
-
|
234
|
-
peter.to_json #=> {"i_am_called":"Peter","surename":"Pan"}
|
235
|
-
|
236
|
-
|
237
|
-
=== Filtering and Conditions
|
238
|
-
|
239
|
-
Representable allows you to skip and include properties when rendering or parsing.
|
240
|
-
|
241
|
-
peter.to_json(:include => :forename)
|
242
|
-
#=> {"forename":"Peter"}
|
243
|
-
|
244
|
-
It gives you convenient +:exclude+ and +:include+ options.
|
245
|
-
|
246
|
-
You can also define conditions on properties on the class layer.
|
247
|
-
|
248
|
-
module HeroRepresenter
|
249
|
-
property :friends, :if => lambda { forename == "Peter" }
|
250
|
-
end
|
251
|
-
|
252
|
-
When rendering or parsing, the +friends+ property is considered only if the condition block evals to true. Note that the block is executed in instance context, giving you access to instance methods.
|
253
|
-
|
254
|
-
=== False and Nil Values
|
255
|
-
|
256
|
-
Since 1.2 +false+ values are considered when parsing and rendering. That particularly means properties that used to be unset (i.e. +nil+) after parsing might be +false+ now. Vice versa, +false+ values that weren't included in the rendered document will be visible now.
|
257
|
-
|
258
|
-
If you want +nil+ values to be included when rendering, use the +:render_nil+ option.
|
259
|
-
|
260
|
-
property :surename, :render_nil => true
|
261
|
-
|
262
|
-
== DCI
|
263
|
-
|
264
|
-
Representers roughly follow the {DCI}[http://en.wikipedia.org/wiki/Data,_context_and_interaction] pattern when used on objects, only.
|
265
|
-
|
266
|
-
Hero.new.extend(HeroRepresenter)
|
267
|
-
|
268
|
-
The only difference is that you have to define which representers to use for typed properties.
|
269
|
-
|
270
|
-
module HeroRepresenter
|
271
|
-
property :forename
|
272
|
-
property :surename
|
273
|
-
collection :features
|
274
|
-
property :origin, :class => Location
|
275
|
-
collection :friends, :class => Hero, :extend => HeroRepresenter
|
276
|
-
end
|
277
|
-
|
278
|
-
There's no need to specify a representer for the +origin+ property since the +Location+ class statically includes its representation. For +friends+, we can use +:extend+ to tell representable which module to mix in dynamically.
|
279
|
-
|
280
|
-
== XML support
|
281
|
-
|
282
|
-
Representable allows declaring a document's syntax and structure while having different formats. Currently, it ships with JSON, XML and YAML bindings.
|
283
|
-
|
284
|
-
class Hero
|
285
|
-
include Representable::XML
|
286
|
-
end
|
287
|
-
|
288
|
-
peter.to_xml
|
289
|
-
#=> <hero>
|
290
|
-
<name>Peter</name>
|
291
|
-
<surename>Pan</surename>
|
292
|
-
<location>
|
293
|
-
<title>Neverland</title>
|
294
|
-
</location>
|
295
|
-
<hero>
|
296
|
-
<name>Nick</name>
|
297
|
-
</hero>
|
298
|
-
<hero>
|
299
|
-
<name>El</name>
|
300
|
-
</hero>
|
301
|
-
</hero>
|
302
|
-
|
303
|
-
The #to_xml method gives us an XML representation of Peter - great!
|
304
|
-
|
305
|
-
=== Mapping tag attributes
|
306
|
-
|
307
|
-
You can also map properties to tag attributes in representable.
|
308
|
-
|
309
|
-
class Hero
|
310
|
-
attr_accessor :name
|
311
|
-
include Representable::XML
|
312
|
-
property :name, :attribute => true
|
313
|
-
end
|
314
|
-
|
315
|
-
Hero.new(:name => "Peter Pan").to_xml
|
316
|
-
#=> <hero name="Peter Pan" />
|
317
|
-
|
318
|
-
Naturally, this works for both ways.
|
319
|
-
|
320
|
-
=== Wrapping collections
|
321
|
-
|
322
|
-
It is sometimes unavoidable to wrap tag lists in a container tag.
|
323
|
-
|
324
|
-
module AlbumRepresenter
|
325
|
-
include Representable::XML
|
326
|
-
|
327
|
-
collection :songs, :from => :song, :wrap => :songs
|
328
|
-
end
|
329
|
-
|
330
|
-
Note that +:wrap+ defines the container tag name.
|
331
|
-
|
332
|
-
Album.new.to_xml #=>
|
333
|
-
<album>
|
334
|
-
<songs>
|
335
|
-
<song>Laundry Basket</song>
|
336
|
-
<song>Two Kevins</song>
|
337
|
-
<song>Wright and Rong</song>
|
338
|
-
</songs>
|
339
|
-
</album>
|
340
|
-
|
341
|
-
|
342
|
-
== YAML Support
|
343
|
-
|
344
|
-
Representers also come in handy if you need to render or parse YAML. The YAML module works exactly like the others.
|
345
|
-
|
346
|
-
module HotBandsRepresenter
|
347
|
-
include Representable::YAML
|
348
|
-
|
349
|
-
property :for
|
350
|
-
collection :names
|
351
|
-
end
|
352
|
-
|
353
|
-
Now, just call #to_yaml to render or #from_yaml to parse.
|
354
|
-
|
355
|
-
HotBands.new(:for => "Nick", :names => ["Bad Religion", "Van Halen", "Mozart"]).
|
356
|
-
extend(HotBandsRepresenter).
|
357
|
-
to_yaml
|
358
|
-
|
359
|
-
#=> ---
|
360
|
-
for: Nick
|
361
|
-
names:
|
362
|
-
- Bad Religion
|
363
|
-
- Van Halen
|
364
|
-
- Mozart
|
365
|
-
|
366
|
-
=== Nested Objects
|
367
|
-
|
368
|
-
The YAML parser does handle nested objects just like JSON and XML does it.
|
369
|
-
|
370
|
-
=== Flow Style Lists
|
371
|
-
|
372
|
-
If you want flow style (aka inline style) lists, use the :style option. See http://www.yaml.org/spec/1.2/spec.html#id2790088 for more infos on flow sequences.
|
373
|
-
|
374
|
-
module HotBandsRepresenter
|
375
|
-
include Representable::YAML
|
376
|
-
|
377
|
-
collection :names, :style => :flow
|
378
|
-
end
|
379
|
-
|
380
|
-
#=> ---
|
381
|
-
names: [Bad Religion, Van Halen, Mozart]
|
382
|
-
|
383
|
-
Need anything else for YAML? Let me know.
|
384
|
-
|
385
|
-
== Coercion
|
386
|
-
|
387
|
-
If you fancy coercion when parsing a document you can use the Coercion module which uses virtus[https://github.com/solnic/virtus] for type conversion.
|
388
|
-
|
389
|
-
Include virtus in your Gemfile, first. Be sure to include virtus 0.5.0 or greater.
|
390
|
-
|
391
|
-
gem 'virtus'
|
392
|
-
|
393
|
-
Use the +:type+ option to specify the conversion target. Note that +:default+ still works.
|
394
|
-
|
395
|
-
module HeroRepresenter
|
396
|
-
include Representable::JSON
|
397
|
-
include Virtus
|
398
|
-
include Representable::Coercion
|
399
|
-
|
400
|
-
property :born_at, :type => DateTime, :default => "May 12th, 2012"
|
401
|
-
end
|
402
|
-
|
403
|
-
|
404
|
-
== More
|
405
|
-
|
406
|
-
Instead of spreading knowledge about your representations about the entire framework, Representable keeps rendering and parsing representations in one single, testable asset. It is a new abstraction layer missing in many "RESTful" frameworks.
|
407
|
-
|
408
|
-
Representable was written with REST representations in mind. However, it is a generic module for working with documents. If you do consider using it for a REST project, check out the {Roar framework}[http://github.com/apotonick/roar], which comes with representers, built-in hypermedia support and more. It internally uses Representable and streamlines the process for building hypermedia-driven REST applications.
|
409
|
-
|
410
|
-
|
411
|
-
== Copyright
|
412
|
-
|
413
|
-
Representable is a heavily simplified fork of the ROXML gem. Big thanks to Ben Woosley for his inspiring work.
|
414
|
-
|
415
|
-
* Copyright (c) 2011 Nick Sutterer <apotonick@gmail.com>
|
416
|
-
* ROXML is Copyright (c) 2004-2009 Ben Woosley, Zak Mandhro and Anders Engstrom.
|