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/CHANGES.textile
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
h2. 1.3.0
|
2
|
+
|
3
|
+
* Remove @:exclude@ option.
|
4
|
+
* Moving all read/write logic to @Binding@. If you did override @#read_fragment@ and friends in your representer/models this won't work anymore.
|
5
|
+
* Options passed to @to_*/from_*@ are now passed to nested objects.
|
6
|
+
|
1
7
|
h2. 1.2.9
|
2
8
|
|
3
9
|
* When @:class@ returns @nil@ we no longer try to create a new instance but use the processed fragment itself.
|
data/README.md
ADDED
@@ -0,0 +1,485 @@
|
|
1
|
+
# Representable
|
2
|
+
|
3
|
+
Representable maps ruby objects to documents and back.
|
4
|
+
|
5
|
+
In other words: Take an object and extend it with a representer module. This will allow you to render a JSON, XML or YAML document from that object. But that's only half of it! You can also use representers to parse a document and create an object.
|
6
|
+
|
7
|
+
Representable is helpful for all kind of rendering and parsing workflows. However, it is mostly useful in API code. Are you planning to write a real REST API with representable? Then check out the [roar](http://github.com/apotonick/roar) gem first, save work and time and make the world a better place instead.
|
8
|
+
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
The representable gem is almost dependency-free. Almost.
|
13
|
+
|
14
|
+
gem 'representable'
|
15
|
+
|
16
|
+
|
17
|
+
## Example
|
18
|
+
|
19
|
+
What if we're writing an API for music - songs, albums, bands.
|
20
|
+
|
21
|
+
class Song < OpenStruct
|
22
|
+
end
|
23
|
+
|
24
|
+
song = Song.new(title: "Fallout", track: 1)
|
25
|
+
|
26
|
+
|
27
|
+
## Defining Representations
|
28
|
+
|
29
|
+
Representations are defined using representer modules.
|
30
|
+
|
31
|
+
require 'representable/json'
|
32
|
+
|
33
|
+
module SongRepresenter
|
34
|
+
include Representable::JSON
|
35
|
+
|
36
|
+
property :title
|
37
|
+
property :track
|
38
|
+
end
|
39
|
+
|
40
|
+
In the representer the #property method allows declaring represented attributes of the object. All the representer requires for rendering are readers on the represented object, e.g. `#title` and `#track`. When parsing, it will call setters - in our example, that'd be `#title=` and `#track=`.
|
41
|
+
|
42
|
+
|
43
|
+
## Rendering
|
44
|
+
|
45
|
+
Mixing in the representer into the object adds a rendering method.
|
46
|
+
|
47
|
+
song.extend(SongRepresenter).to_json
|
48
|
+
#=> {"title":"Fallout","track":1}
|
49
|
+
|
50
|
+
|
51
|
+
## Parsing
|
52
|
+
|
53
|
+
It also adds support for parsing.
|
54
|
+
|
55
|
+
song = Song.new.extend(SongRepresenter).from_json(%{ {"title":"Roxanne"} })
|
56
|
+
#=> #<Song title="Roxanne", track=nil>
|
57
|
+
|
58
|
+
|
59
|
+
## Wrapping
|
60
|
+
|
61
|
+
Let the representer know if you want wrapping.
|
62
|
+
|
63
|
+
module SongRepresenter
|
64
|
+
include Representable::JSON
|
65
|
+
|
66
|
+
self.representation_wrap= :hit
|
67
|
+
|
68
|
+
property :title
|
69
|
+
property :track
|
70
|
+
end
|
71
|
+
|
72
|
+
This will add a container for rendering and consuming.
|
73
|
+
|
74
|
+
song.extend(SongRepresenter).to_json
|
75
|
+
#=> {"hit":{"title":"Fallout","track":1}}
|
76
|
+
|
77
|
+
Setting `self.representation_wrap = true` will advice representable to figure out the wrap itself by inspecting the represented object class.
|
78
|
+
|
79
|
+
|
80
|
+
## Collections
|
81
|
+
|
82
|
+
Let's add a list of composers to the song representation.
|
83
|
+
|
84
|
+
module SongRepresenter
|
85
|
+
include Representable::JSON
|
86
|
+
|
87
|
+
property :title
|
88
|
+
property :track
|
89
|
+
collection :composers
|
90
|
+
end
|
91
|
+
|
92
|
+
Surprisingly, `#collection` lets us define lists of objects to represent.
|
93
|
+
|
94
|
+
Song.new(title: "Fallout", composers: ["Steward Copeland", "Sting"]).
|
95
|
+
extend(SongRepresenter).to_json
|
96
|
+
|
97
|
+
#=> {"title":"Fallout","composers":["Steward Copeland","Sting"]}
|
98
|
+
|
99
|
+
|
100
|
+
And again, this works both ways - in addition to the title it extracts the composers from the document, too.
|
101
|
+
|
102
|
+
|
103
|
+
## Nesting
|
104
|
+
|
105
|
+
Representers can also manage compositions. Why not use an album that contains a list of songs?
|
106
|
+
|
107
|
+
class Album < OpenStruct
|
108
|
+
end
|
109
|
+
|
110
|
+
album = Album.new(name: "The Police", songs: [song, Song.new(title: "Synchronicity")])
|
111
|
+
|
112
|
+
|
113
|
+
Here comes the representer that defines the composition.
|
114
|
+
|
115
|
+
module AlbumRepresenter
|
116
|
+
include Representable::JSON
|
117
|
+
|
118
|
+
property :name
|
119
|
+
collection :songs, extend: SongRepresenter, class: Song
|
120
|
+
end
|
121
|
+
|
122
|
+
Note that nesting works with both plain `#property` and `#collection`.
|
123
|
+
|
124
|
+
When rendering, the `:extend` module is used to extend the attribute(s) with the correct representer module.
|
125
|
+
|
126
|
+
album.extend(AlbumRepresenter).to_json
|
127
|
+
#=> {"name":"The Police","songs":[{"title":"Fallout","composers":["Steward Copeland","Sting"]},{"title":"Synchronicity","composers":[]}]}
|
128
|
+
|
129
|
+
Parsing a documents needs both `:extend` and the `:class` option as the parser requires knowledge what kind of object to create from the nested composition.
|
130
|
+
|
131
|
+
Album.new.extend(AlbumRepresenter).
|
132
|
+
from_json(%{{"name":"Offspring","songs":[{"title":"Genocide"},{"title":"Nitro","composers":["Offspring"]}]}})
|
133
|
+
|
134
|
+
#=> #<Album name="Offspring", songs=[#<Song title="Genocide">, #<Song title="Nitro", composers=["Offspring"]>]>
|
135
|
+
|
136
|
+
|
137
|
+
## XML Support
|
138
|
+
|
139
|
+
While representable does a great job with JSON, it also features support for XML, YAML and pure ruby hashes.
|
140
|
+
|
141
|
+
require 'representable/xml'
|
142
|
+
|
143
|
+
module SongRepresenter
|
144
|
+
include Representable::XML
|
145
|
+
|
146
|
+
property :title
|
147
|
+
property :track
|
148
|
+
collection :composers
|
149
|
+
end
|
150
|
+
|
151
|
+
For XML we just include the `Representable::XML` module.
|
152
|
+
|
153
|
+
Song.new(title: "Fallout", composers: ["Steward Copeland", "Sting"]).
|
154
|
+
extend(SongRepresenter).to_xml
|
155
|
+
|
156
|
+
#=> <song>
|
157
|
+
<title>Fallout</title>
|
158
|
+
<composers>Steward Copeland</composers>
|
159
|
+
<composers>Sting</composers>
|
160
|
+
</song>
|
161
|
+
|
162
|
+
|
163
|
+
## Using Helpers
|
164
|
+
|
165
|
+
Sometimes it's useful to override accessors to customize output or parsing.
|
166
|
+
|
167
|
+
module AlbumRepresenter
|
168
|
+
include Representable::JSON
|
169
|
+
|
170
|
+
property :name
|
171
|
+
collection :songs
|
172
|
+
|
173
|
+
def name
|
174
|
+
super.upcase
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
Album.new(:name => "The Police").
|
179
|
+
extend(AlbumRepresenter).to_json
|
180
|
+
|
181
|
+
#=> {"name":"THE POLICE","songs":[]}
|
182
|
+
|
183
|
+
Note how the representer allows calling `super` in order to access the original attribute method of the represented object.
|
184
|
+
|
185
|
+
To change the parsing process override the setter.
|
186
|
+
|
187
|
+
def name=(value)
|
188
|
+
super(value.downcase)
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
## Inheritance
|
193
|
+
|
194
|
+
To reuse existing representers you can inherit from those modules.
|
195
|
+
|
196
|
+
module CoverSongRepresenter
|
197
|
+
include Representable::JSON
|
198
|
+
include SongRepresenter
|
199
|
+
|
200
|
+
property :copyright
|
201
|
+
end
|
202
|
+
|
203
|
+
Inheritance works by `include`ing already defined representers.
|
204
|
+
|
205
|
+
Song.new(:title => "Truth Hits Everybody", :copyright => "The Police").
|
206
|
+
extend(CoverSongRepresenter).to_json
|
207
|
+
|
208
|
+
#=> {"title":"Truth Hits Everybody","copyright":"The Police"}
|
209
|
+
|
210
|
+
|
211
|
+
## Polymorphic Extend
|
212
|
+
|
213
|
+
Sometimes heterogenous collections of objects from different classes must be represented. Or you don't know which representer to use at compile-time and need to delay the computation until runtime. This is why `:extend` accepts a lambda, too.
|
214
|
+
|
215
|
+
Given we not only have songs, but also cover songs.
|
216
|
+
|
217
|
+
class CoverSong < Song
|
218
|
+
end
|
219
|
+
|
220
|
+
And a non-homogenous collection of songs.
|
221
|
+
|
222
|
+
songs = [ Song.new(title: "Weirdo", track: 5),
|
223
|
+
CoverSong.new(title: "Truth Hits Everybody", track: 6, copyright: "The Police")]
|
224
|
+
|
225
|
+
album = Album.new(name: "Incognito", songs: songs)
|
226
|
+
|
227
|
+
|
228
|
+
The `CoverSong` instances are to be represented by their very own `CoverSongRepresenter` defined above. We can't just use a static module in the `:extend` option, so go use a dynamic lambda!
|
229
|
+
|
230
|
+
module AlbumRepresenter
|
231
|
+
include Representable::JSON
|
232
|
+
|
233
|
+
property :name
|
234
|
+
collection :songs, :extend => lambda { |song| song.is_a?(CoverSong) ? CoverSongRepresenter : SongRepresenter }
|
235
|
+
end
|
236
|
+
|
237
|
+
Note that the lambda block is evaluated in the represented object context which allows to access helpers or whatever in the block. This works for single properties, too.
|
238
|
+
|
239
|
+
|
240
|
+
## Polymorphic Object Creation
|
241
|
+
|
242
|
+
Rendering heterogenous collections usually implies that you also need to parse those. Luckily, `:class` also accepts a lambda.
|
243
|
+
|
244
|
+
module AlbumRepresenter
|
245
|
+
include Representable::JSON
|
246
|
+
|
247
|
+
property :name
|
248
|
+
collection :songs,
|
249
|
+
:extend => ...,
|
250
|
+
:class => lambda { |hsh| hsh.has_key?("copyright") ? CoverSong : Song }
|
251
|
+
end
|
252
|
+
|
253
|
+
The block for `:class` receives the currently parsed fragment. Here, this might be somthing like `{"title"=>"Weirdo", "track"=>5}`.
|
254
|
+
|
255
|
+
If this is not enough, you may override the entire object creation process using `:instance`.
|
256
|
+
|
257
|
+
module AlbumRepresenter
|
258
|
+
include Representable::JSON
|
259
|
+
|
260
|
+
property :name
|
261
|
+
collection :songs,
|
262
|
+
:extend => ...,
|
263
|
+
:instance => lambda { |hsh| hsh.has_key?("copyright") ? CoverSong.new : Song.new(original: true) }
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
## Hashes
|
268
|
+
|
269
|
+
As an addition to single properties and collections representable also offers to represent hash attributes.
|
270
|
+
|
271
|
+
module SongRepresenter
|
272
|
+
include Representable::JSON
|
273
|
+
|
274
|
+
property :title
|
275
|
+
hash :ratings
|
276
|
+
end
|
277
|
+
|
278
|
+
Song.new(title: "Bliss", ratings: {"Rolling Stone" => 4.9, "FryZine" => 4.5}).
|
279
|
+
extend(SongRepresenter).to_json
|
280
|
+
|
281
|
+
#=> {"title":"Bliss","ratings":{"Rolling Stone":4.9,"FryZine":4.5}}
|
282
|
+
|
283
|
+
|
284
|
+
## Lonely Hashes
|
285
|
+
|
286
|
+
Need to represent a bare hash without any container? Use the `JSON::Hash` representer (or XML::Hash).
|
287
|
+
|
288
|
+
require 'representable/json/hash'
|
289
|
+
|
290
|
+
module FavoriteSongsRepresenter
|
291
|
+
include Representable::JSON::Hash
|
292
|
+
end
|
293
|
+
|
294
|
+
{"Nick" => "Hyper Music", "El" => "Blown In The Wind"}.extend(FavoriteSongsRepresenter).to_json
|
295
|
+
#=> {"Nick":"Hyper Music","El":"Blown In The Wind"}
|
296
|
+
|
297
|
+
Works both ways. The values are configurable and might be self-representing objects in turn. Tell the `Hash` by using `#values`.
|
298
|
+
|
299
|
+
module FavoriteSongsRepresenter
|
300
|
+
include Representable::JSON::Hash
|
301
|
+
|
302
|
+
values extend: SongRepresenter, class: Song
|
303
|
+
end
|
304
|
+
|
305
|
+
{"Nick" => Song.new(title: "Hyper Music")}.extend(FavoriteSongsRepresenter).to_json
|
306
|
+
|
307
|
+
In XML, if you want to store hash attributes in tag attributes instead of dedicated nodes, use `XML::AttributeHash`.
|
308
|
+
|
309
|
+
## Lonely Collections
|
310
|
+
|
311
|
+
Same goes with arrays.
|
312
|
+
|
313
|
+
require 'representable/json/collection'
|
314
|
+
|
315
|
+
module SongsRepresenter
|
316
|
+
include Representable::JSON::Collection
|
317
|
+
|
318
|
+
items extend: SongRepresenter, class: Song
|
319
|
+
end
|
320
|
+
|
321
|
+
The `#items` method lets you configure the contained entity representing here.
|
322
|
+
|
323
|
+
[Song.new(title: "Hyper Music"), Song.new(title: "Screenager")].extend(SongsRepresenter).to_json
|
324
|
+
#=> [{"title":"Hyper Music"},{"title":"Screenager"}]
|
325
|
+
|
326
|
+
Note that this also works for XML.
|
327
|
+
|
328
|
+
|
329
|
+
## YAML Support
|
330
|
+
|
331
|
+
Representable also comes with a YAML representer.
|
332
|
+
|
333
|
+
module SongRepresenter
|
334
|
+
include Representable::YAML
|
335
|
+
|
336
|
+
property :title
|
337
|
+
property :track
|
338
|
+
collection :composers, :style => :flow
|
339
|
+
end
|
340
|
+
|
341
|
+
A nice feature is that `#collection` also accepts a `:style` option which helps having nicely formatted inline (or "flow") arrays in your YAML - if you want that!
|
342
|
+
|
343
|
+
song.extend(SongRepresenter).to_yaml
|
344
|
+
#=>
|
345
|
+
---
|
346
|
+
title: Fallout
|
347
|
+
composers: [Steward Copeland, Sting]
|
348
|
+
|
349
|
+
|
350
|
+
## More on XML
|
351
|
+
|
352
|
+
### Mapping tag attributes
|
353
|
+
|
354
|
+
You can also map properties to tag attributes in representable.
|
355
|
+
|
356
|
+
module SongRepresenter
|
357
|
+
include Representable::XML
|
358
|
+
|
359
|
+
property :title, attribute: true
|
360
|
+
property :track, attribute: true
|
361
|
+
end
|
362
|
+
|
363
|
+
Song.new(title: "American Idle").to_xml
|
364
|
+
#=> <song title="American Idle" />
|
365
|
+
|
366
|
+
Naturally, this works for both ways.
|
367
|
+
|
368
|
+
### Wrapping collections
|
369
|
+
|
370
|
+
It is sometimes unavoidable to wrap tag lists in a container tag.
|
371
|
+
|
372
|
+
module AlbumRepresenter
|
373
|
+
include Representable::XML
|
374
|
+
|
375
|
+
collection :songs, :as => :song, :wrap => :songs
|
376
|
+
end
|
377
|
+
|
378
|
+
Note that `:wrap` defines the container tag name.
|
379
|
+
|
380
|
+
Album.new.to_xml #=>
|
381
|
+
<album>
|
382
|
+
<songs>
|
383
|
+
<song>Laundry Basket</song>
|
384
|
+
<song>Two Kevins</song>
|
385
|
+
<song>Wright and Rong</song>
|
386
|
+
</songs>
|
387
|
+
</album>
|
388
|
+
|
389
|
+
|
390
|
+
## Avoiding Modules
|
391
|
+
|
392
|
+
There's been a rough discussion whether or not to use `extend` in Ruby. If you want to save that particular step when representing objects, define the representers right in your classes.
|
393
|
+
|
394
|
+
class Song < OpenStruct
|
395
|
+
include Representable::JSON
|
396
|
+
|
397
|
+
property :name
|
398
|
+
end
|
399
|
+
|
400
|
+
I do not recommend this approach as it bloats your domain classes with representation logic that is barely needed elsewhere.
|
401
|
+
|
402
|
+
|
403
|
+
## More Options
|
404
|
+
|
405
|
+
Here's a quick overview about other available options for `#property` and its bro `#collection`.
|
406
|
+
|
407
|
+
|
408
|
+
### Read/Write Restrictions
|
409
|
+
|
410
|
+
Using the `:readable` and `:writeable` options access to properties can be restricted.
|
411
|
+
|
412
|
+
property :title, :readable => false
|
413
|
+
|
414
|
+
This will leave out the `title` property in the rendered document. Vice-versa, `:writeable` will skip the property when parsing and does not assign it.
|
415
|
+
|
416
|
+
|
417
|
+
### Filtering
|
418
|
+
|
419
|
+
Representable also allows you to skip and include properties using the `:exclude` and `:include` options passed directly to the respective method.
|
420
|
+
|
421
|
+
song.to_json(:include => :title)
|
422
|
+
#=> {"title":"Roxanne"}
|
423
|
+
|
424
|
+
|
425
|
+
### Conditions
|
426
|
+
|
427
|
+
You can also define conditions on properties using `:if`, making them being considered only when the block returns a true value.
|
428
|
+
|
429
|
+
module SongRepresenter
|
430
|
+
include Representable::JSON
|
431
|
+
|
432
|
+
property :title
|
433
|
+
property :track, if: lambda { track > 0 }
|
434
|
+
end
|
435
|
+
|
436
|
+
When rendering or parsing, the `track` property is considered only if track is valid. Note that the block is executed in instance context, giving you access to instance methods.
|
437
|
+
|
438
|
+
|
439
|
+
### Mapping
|
440
|
+
|
441
|
+
If your property name doesn't match the attribute name in the document, use the `:as` option.
|
442
|
+
|
443
|
+
module SongRepresenter
|
444
|
+
property :title
|
445
|
+
property :track, as: :track_number
|
446
|
+
end
|
447
|
+
|
448
|
+
song.to_json #=> {"title":"Superstars","track_number":1}
|
449
|
+
|
450
|
+
|
451
|
+
### False and Nil Values
|
452
|
+
|
453
|
+
Since representable-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` properties that weren't included in the rendered document will be visible now.
|
454
|
+
|
455
|
+
If you want `nil` values to be included when rendering, use the `:render_nil` option.
|
456
|
+
|
457
|
+
property :track, render_nil: true
|
458
|
+
|
459
|
+
|
460
|
+
## Coercion
|
461
|
+
|
462
|
+
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.
|
463
|
+
|
464
|
+
Include virtus in your Gemfile, first. Be sure to include virtus 0.5.0 or greater.
|
465
|
+
|
466
|
+
gem 'virtus', ">= 0.5.0"
|
467
|
+
|
468
|
+
Use the `:type` option to specify the conversion target. Note that `:default` still works.
|
469
|
+
|
470
|
+
module SongRepresenter
|
471
|
+
include Representable::JSON
|
472
|
+
include Virtus
|
473
|
+
include Representable::Coercion
|
474
|
+
|
475
|
+
property :title
|
476
|
+
property :recorded_at, :type => DateTime, :default => "May 12th, 2012"
|
477
|
+
end
|
478
|
+
|
479
|
+
|
480
|
+
## Copyright
|
481
|
+
|
482
|
+
Representable started as a heavily simplified fork of the ROXML gem. Big thanks to Ben Woosley for his inspiring work.
|
483
|
+
|
484
|
+
* Copyright (c) 2011-2013 Nick Sutterer <apotonick@gmail.com>
|
485
|
+
* ROXML is Copyright (c) 2004-2009 Ben Woosley, Zak Mandhro and Anders Engstrom.
|