representable 1.2.9 → 1.3.0

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