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