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.
@@ -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.