representable 1.4.2 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.textile +4 -0
- data/README.md +323 -229
- data/TODO +1 -1
- data/lib/representable/binding.rb +24 -19
- data/lib/representable/version.rb +1 -1
- data/test/representable_test.rb +9 -9
- metadata +2 -2
data/CHANGES.textile
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
h2. 1.5.0
|
2
|
+
|
3
|
+
* All lambdas now receive user options, too. Note that this might break your existing lambdas (especially with `:extend` or `:class`) raising an `ArgumentError: wrong number of arguments (2 for 1)`. Fix this by declaring your block params correctly, e.g. `lambda { |name, *|`. Internally, this happens by running all lambdas through the new `Binding#represented_exec_for`.
|
4
|
+
|
1
5
|
h2. 1.4.2
|
2
6
|
|
3
7
|
* Fix the processing of `:setter`, we called both the setter lambda and the setter method.
|
data/README.md
CHANGED
@@ -11,31 +11,35 @@ Representable is helpful for all kind of rendering and parsing workflows. Howeve
|
|
11
11
|
|
12
12
|
The representable gem is almost dependency-free. Almost.
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
```ruby
|
15
|
+
gem 'representable'
|
16
|
+
```
|
16
17
|
|
17
18
|
## Example
|
18
19
|
|
19
20
|
What if we're writing an API for music - songs, albums, bands.
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
song = Song.new(title: "Fallout", track: 1)
|
22
|
+
```ruby
|
23
|
+
class Song < OpenStruct
|
24
|
+
end
|
25
25
|
|
26
|
+
song = Song.new(title: "Fallout", track: 1)
|
27
|
+
```
|
26
28
|
|
27
29
|
## Defining Representations
|
28
30
|
|
29
31
|
Representations are defined using representer modules.
|
30
32
|
|
31
|
-
|
33
|
+
```ruby
|
34
|
+
require 'representable/json'
|
32
35
|
|
33
|
-
|
34
|
-
|
36
|
+
module SongRepresenter
|
37
|
+
include Representable::JSON
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
property :title
|
40
|
+
property :track
|
41
|
+
end
|
42
|
+
```
|
39
43
|
|
40
44
|
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
45
|
|
@@ -44,17 +48,19 @@ In the representer the #property method allows declaring represented attributes
|
|
44
48
|
|
45
49
|
Mixing in the representer into the object adds a rendering method.
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
```ruby
|
52
|
+
song.extend(SongRepresenter).to_json
|
53
|
+
#=> {"title":"Fallout","track":1}
|
54
|
+
```
|
50
55
|
|
51
56
|
## Parsing
|
52
57
|
|
53
58
|
It also adds support for parsing.
|
54
59
|
|
55
|
-
|
56
|
-
|
57
|
-
|
60
|
+
```ruby
|
61
|
+
song = Song.new.extend(SongRepresenter).from_json(%{ {"title":"Roxanne"} })
|
62
|
+
#=> #<Song title="Roxanne", track=nil>
|
63
|
+
```
|
58
64
|
|
59
65
|
## Extend vs. Decorator
|
60
66
|
|
@@ -65,33 +71,39 @@ If you don't want representer modules to be mixed into your objects (using `#ext
|
|
65
71
|
|
66
72
|
If your property name doesn't match the name in the document, use the `:as` option.
|
67
73
|
|
68
|
-
|
69
|
-
|
74
|
+
```ruby
|
75
|
+
module SongRepresenter
|
76
|
+
include Representable::JSON
|
70
77
|
|
71
|
-
|
72
|
-
|
73
|
-
|
78
|
+
property :title, as: :name
|
79
|
+
property :track
|
80
|
+
end
|
74
81
|
|
75
|
-
|
82
|
+
song.to_json #=> {"name":"Fallout","track":1}
|
83
|
+
```
|
76
84
|
|
77
85
|
|
78
86
|
## Wrapping
|
79
87
|
|
80
88
|
Let the representer know if you want wrapping.
|
81
89
|
|
82
|
-
|
83
|
-
|
90
|
+
```ruby
|
91
|
+
module SongRepresenter
|
92
|
+
include Representable::JSON
|
84
93
|
|
85
|
-
|
94
|
+
self.representation_wrap= :hit
|
86
95
|
|
87
|
-
|
88
|
-
|
89
|
-
|
96
|
+
property :title
|
97
|
+
property :track
|
98
|
+
end
|
99
|
+
```
|
90
100
|
|
91
101
|
This will add a container for rendering and consuming.
|
92
102
|
|
93
|
-
|
94
|
-
|
103
|
+
```ruby
|
104
|
+
song.extend(SongRepresenter).to_json
|
105
|
+
#=> {"hit":{"title":"Fallout","track":1}}
|
106
|
+
```
|
95
107
|
|
96
108
|
Setting `self.representation_wrap = true` will advice representable to figure out the wrap itself by inspecting the represented object class.
|
97
109
|
|
@@ -100,21 +112,24 @@ Setting `self.representation_wrap = true` will advice representable to figure ou
|
|
100
112
|
|
101
113
|
Let's add a list of composers to the song representation.
|
102
114
|
|
103
|
-
|
104
|
-
|
115
|
+
```ruby
|
116
|
+
module SongRepresenter
|
117
|
+
include Representable::JSON
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
119
|
+
property :title
|
120
|
+
property :track
|
121
|
+
collection :composers
|
122
|
+
end
|
123
|
+
```
|
110
124
|
|
111
125
|
Surprisingly, `#collection` lets us define lists of objects to represent.
|
112
126
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
#=> {"title":"Fallout","composers":["Steward Copeland","Sting"]}
|
127
|
+
```ruby
|
128
|
+
Song.new(title: "Fallout", composers: ["Steward Copeland", "Sting"]).
|
129
|
+
extend(SongRepresenter).to_json
|
117
130
|
|
131
|
+
#=> {"title":"Fallout","composers":["Steward Copeland","Sting"]}
|
132
|
+
```
|
118
133
|
|
119
134
|
And again, this works both ways - in addition to the title it extracts the composers from the document, too.
|
120
135
|
|
@@ -123,111 +138,135 @@ And again, this works both ways - in addition to the title it extracts the compo
|
|
123
138
|
|
124
139
|
Representers can also manage compositions. Why not use an album that contains a list of songs?
|
125
140
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
album = Album.new(name: "The Police", songs: [song, Song.new(title: "Synchronicity")])
|
141
|
+
```ruby
|
142
|
+
class Album < OpenStruct
|
143
|
+
end
|
130
144
|
|
145
|
+
album = Album.new(name: "The Police", songs: [song, Song.new(title: "Synchronicity")])
|
146
|
+
```
|
131
147
|
|
132
148
|
Here comes the representer that defines the composition.
|
133
149
|
|
134
|
-
|
135
|
-
|
150
|
+
```ruby
|
151
|
+
module AlbumRepresenter
|
152
|
+
include Representable::JSON
|
136
153
|
|
137
|
-
|
138
|
-
|
139
|
-
|
154
|
+
property :name
|
155
|
+
collection :songs, extend: SongRepresenter, class: Song
|
156
|
+
end
|
157
|
+
```
|
140
158
|
|
141
159
|
Note that nesting works with both plain `#property` and `#collection`.
|
142
160
|
|
143
161
|
When rendering, the `:extend` module is used to extend the attribute(s) with the correct representer module.
|
144
162
|
|
145
|
-
|
146
|
-
|
163
|
+
```ruby
|
164
|
+
album.extend(AlbumRepresenter).to_json
|
165
|
+
#=> {"name":"The Police","songs":[{"title":"Fallout","composers":["Steward Copeland","Sting"]},{"title":"Synchronicity","composers":[]}]}
|
166
|
+
```
|
147
167
|
|
148
168
|
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.
|
149
169
|
|
150
|
-
|
151
|
-
|
170
|
+
```ruby
|
171
|
+
Album.new.extend(AlbumRepresenter).
|
172
|
+
from_json(%{{"name":"Offspring","songs":[{"title":"Genocide"},{"title":"Nitro","composers":["Offspring"]}]}})
|
152
173
|
|
153
|
-
|
174
|
+
#=> #<Album name="Offspring", songs=[#<Song title="Genocide">, #<Song title="Nitro", composers=["Offspring"]>]>
|
175
|
+
```
|
154
176
|
|
155
177
|
## Decorator vs. Extend
|
156
178
|
|
157
179
|
People who dislike `:extend` go use the `Decorator` strategy!
|
158
180
|
|
159
|
-
|
160
|
-
|
181
|
+
```ruby
|
182
|
+
class SongRepresentation < Representable::Decorator
|
183
|
+
include Representable::JSON
|
161
184
|
|
162
|
-
|
163
|
-
|
164
|
-
|
185
|
+
property :title
|
186
|
+
property :track
|
187
|
+
end
|
188
|
+
```
|
165
189
|
|
166
190
|
The `Decorator` constructor requires the represented object.
|
167
191
|
|
168
|
-
|
192
|
+
```ruby
|
193
|
+
SongRepresentation.new(song).to_json
|
194
|
+
```
|
169
195
|
|
170
196
|
This will leave the `song` instance untouched as the decorator just uses public accessors to represent the hit.
|
171
197
|
|
172
198
|
In compositions you need to specify the decorators for the nested items using the `:decorator` option where you'd normally use `:extend`.
|
173
199
|
|
174
|
-
|
175
|
-
|
200
|
+
```ruby
|
201
|
+
class AlbumRepresentation < Representable::Decorator
|
202
|
+
include Representable::JSON
|
176
203
|
|
177
|
-
|
178
|
-
|
204
|
+
collection :songs, :class => Song, :decorator => SongRepresentation
|
205
|
+
end
|
206
|
+
```
|
179
207
|
|
180
208
|
## XML Support
|
181
209
|
|
182
210
|
While representable does a great job with JSON, it also features support for XML, YAML and pure ruby hashes.
|
183
211
|
|
184
|
-
|
212
|
+
```ruby
|
213
|
+
require 'representable/xml'
|
185
214
|
|
186
|
-
|
187
|
-
|
215
|
+
module SongRepresenter
|
216
|
+
include Representable::XML
|
188
217
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
218
|
+
property :title
|
219
|
+
property :track
|
220
|
+
collection :composers
|
221
|
+
end
|
222
|
+
```
|
193
223
|
|
194
224
|
For XML we just include the `Representable::XML` module.
|
195
225
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
226
|
+
```xml
|
227
|
+
Song.new(title: "Fallout", composers: ["Steward Copeland", "Sting"]).
|
228
|
+
extend(SongRepresenter).to_xml #=>
|
229
|
+
<song>
|
230
|
+
<title>Fallout</title>
|
231
|
+
<composers>Steward Copeland</composers>
|
232
|
+
<composers>Sting</composers>
|
233
|
+
</song>
|
234
|
+
```
|
204
235
|
|
205
236
|
## Passing Options
|
206
237
|
|
207
238
|
You're free to pass an options hash into the rendering or parsing.
|
208
239
|
|
209
|
-
|
240
|
+
```ruby
|
241
|
+
song.to_json(:append => "SOLD OUT!")
|
242
|
+
```
|
210
243
|
|
211
244
|
If you want to append the "SOLD OUT!" to the song's `title` when rendering, use the `:getter` option.
|
212
245
|
|
213
|
-
|
214
|
-
|
246
|
+
```ruby
|
247
|
+
SongRepresenter
|
248
|
+
include Representable::JSON
|
215
249
|
|
216
|
-
|
217
|
-
|
250
|
+
property :title, :getter => lambda { |args| title + args[:append] }
|
251
|
+
end
|
252
|
+
```
|
218
253
|
|
219
254
|
Note that the block is executed in the represented model context which allows using accessors and instance variables.
|
220
255
|
|
221
256
|
|
222
257
|
The same works for parsing using the `:setter` method.
|
223
258
|
|
224
|
-
|
259
|
+
```ruby
|
260
|
+
property :title, :setter => lambda { |val, args| self.title= val + args[:append] }
|
261
|
+
```
|
225
262
|
|
226
263
|
Here, the block retrieves two arguments: the parsed value and your user options.
|
227
264
|
|
228
265
|
You can also use the `:getter` option instead of writing a reader method. Even when you're not interested in user options you can still use this technique.
|
229
266
|
|
230
|
-
|
267
|
+
```ruby
|
268
|
+
property :title, :getter => lambda { |*| @name }
|
269
|
+
```
|
231
270
|
|
232
271
|
This hash will also be available in the `:if` block, documented [here](https://github.com/apotonick/representable/#conditions) and will be passed to nested objects.
|
233
272
|
|
@@ -235,48 +274,55 @@ This hash will also be available in the `:if` block, documented [here](https://g
|
|
235
274
|
|
236
275
|
Sometimes it's useful to override accessors to customize output or parsing.
|
237
276
|
|
238
|
-
|
239
|
-
|
277
|
+
```ruby
|
278
|
+
module AlbumRepresenter
|
279
|
+
include Representable::JSON
|
240
280
|
|
241
|
-
|
242
|
-
|
281
|
+
property :name
|
282
|
+
collection :songs
|
243
283
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
284
|
+
def name
|
285
|
+
super.upcase
|
286
|
+
end
|
287
|
+
end
|
248
288
|
|
249
|
-
|
250
|
-
|
289
|
+
Album.new(:name => "The Police").
|
290
|
+
extend(AlbumRepresenter).to_json
|
251
291
|
|
252
|
-
|
292
|
+
#=> {"name":"THE POLICE","songs":[]}
|
293
|
+
```
|
253
294
|
|
254
295
|
Note how the representer allows calling `super` in order to access the original attribute method of the represented object.
|
255
296
|
|
256
297
|
To change the parsing process override the setter.
|
257
298
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
299
|
+
```ruby
|
300
|
+
def name=(value)
|
301
|
+
super(value.downcase)
|
302
|
+
end
|
303
|
+
```
|
262
304
|
|
263
305
|
## Inheritance
|
264
306
|
|
265
307
|
To reuse existing representers you can inherit from those modules.
|
266
308
|
|
267
|
-
|
268
|
-
|
269
|
-
|
309
|
+
```ruby
|
310
|
+
module CoverSongRepresenter
|
311
|
+
include Representable::JSON
|
312
|
+
include SongRepresenter
|
270
313
|
|
271
|
-
|
272
|
-
|
314
|
+
property :copyright
|
315
|
+
end
|
316
|
+
```
|
273
317
|
|
274
318
|
Inheritance works by `include`ing already defined representers.
|
275
319
|
|
276
|
-
|
277
|
-
|
320
|
+
```ruby
|
321
|
+
Song.new(:title => "Truth Hits Everybody", :copyright => "The Police").
|
322
|
+
extend(CoverSongRepresenter).to_json
|
278
323
|
|
279
|
-
|
324
|
+
#=> {"title":"Truth Hits Everybody","copyright":"The Police"}
|
325
|
+
```
|
280
326
|
|
281
327
|
|
282
328
|
## Polymorphic Extend
|
@@ -285,25 +331,30 @@ Sometimes heterogenous collections of objects from different classes must be rep
|
|
285
331
|
|
286
332
|
Given we not only have songs, but also cover songs.
|
287
333
|
|
288
|
-
|
289
|
-
|
334
|
+
```ruby
|
335
|
+
class CoverSong < Song
|
336
|
+
end
|
337
|
+
```
|
290
338
|
|
291
339
|
And a non-homogenous collection of songs.
|
292
340
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
album = Album.new(name: "Incognito", songs: songs)
|
341
|
+
```ruby
|
342
|
+
songs = [ Song.new(title: "Weirdo", track: 5),
|
343
|
+
CoverSong.new(title: "Truth Hits Everybody", track: 6, copyright: "The Police")]
|
297
344
|
|
345
|
+
album = Album.new(name: "Incognito", songs: songs)
|
346
|
+
```
|
298
347
|
|
299
348
|
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!
|
300
349
|
|
301
|
-
|
302
|
-
|
350
|
+
```ruby
|
351
|
+
module AlbumRepresenter
|
352
|
+
include Representable::JSON
|
303
353
|
|
304
|
-
|
305
|
-
|
306
|
-
|
354
|
+
property :name
|
355
|
+
collection :songs, :extend => lambda { |song, *| song.is_a?(CoverSong) ? CoverSongRepresenter : SongRepresenter }
|
356
|
+
end
|
357
|
+
```
|
307
358
|
|
308
359
|
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.
|
309
360
|
|
@@ -312,87 +363,99 @@ Note that the lambda block is evaluated in the represented object context which
|
|
312
363
|
|
313
364
|
Rendering heterogenous collections usually implies that you also need to parse those. Luckily, `:class` also accepts a lambda.
|
314
365
|
|
315
|
-
|
316
|
-
|
366
|
+
```ruby
|
367
|
+
module AlbumRepresenter
|
368
|
+
include Representable::JSON
|
317
369
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
370
|
+
property :name
|
371
|
+
collection :songs,
|
372
|
+
:extend => ...,
|
373
|
+
:class => lambda { |hsh, *| hsh.has_key?("copyright") ? CoverSong : Song }
|
374
|
+
end
|
375
|
+
```
|
323
376
|
|
324
377
|
The block for `:class` receives the currently parsed fragment. Here, this might be somthing like `{"title"=>"Weirdo", "track"=>5}`.
|
325
378
|
|
326
379
|
If this is not enough, you may override the entire object creation process using `:instance`.
|
327
380
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
property :name
|
332
|
-
collection :songs,
|
333
|
-
:extend => ...,
|
334
|
-
:instance => lambda { |hsh| hsh.has_key?("copyright") ? CoverSong.new : Song.new(original: true) }
|
335
|
-
end
|
381
|
+
```ruby
|
382
|
+
module AlbumRepresenter
|
383
|
+
include Representable::JSON
|
336
384
|
|
385
|
+
property :name
|
386
|
+
collection :songs,
|
387
|
+
:extend => ...,
|
388
|
+
:instance => lambda { |hsh, *| hsh.has_key?("copyright") ? CoverSong.new : Song.new(original: true) }
|
389
|
+
end
|
390
|
+
```
|
337
391
|
|
338
392
|
## Hashes
|
339
393
|
|
340
394
|
As an addition to single properties and collections representable also offers to represent hash attributes.
|
341
395
|
|
342
|
-
|
343
|
-
|
396
|
+
```ruby
|
397
|
+
module SongRepresenter
|
398
|
+
include Representable::JSON
|
344
399
|
|
345
|
-
|
346
|
-
|
347
|
-
|
400
|
+
property :title
|
401
|
+
hash :ratings
|
402
|
+
end
|
348
403
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
#=> {"title":"Bliss","ratings":{"Rolling Stone":4.9,"FryZine":4.5}}
|
404
|
+
Song.new(title: "Bliss", ratings: {"Rolling Stone" => 4.9, "FryZine" => 4.5}).
|
405
|
+
extend(SongRepresenter).to_json
|
353
406
|
|
407
|
+
#=> {"title":"Bliss","ratings":{"Rolling Stone":4.9,"FryZine":4.5}}
|
408
|
+
```
|
354
409
|
|
355
410
|
## Lonely Hashes
|
356
411
|
|
357
412
|
Need to represent a bare hash without any container? Use the `JSON::Hash` representer (or XML::Hash).
|
358
413
|
|
359
|
-
|
414
|
+
```ruby
|
415
|
+
require 'representable/json/hash'
|
360
416
|
|
361
|
-
|
362
|
-
|
363
|
-
|
417
|
+
module FavoriteSongsRepresenter
|
418
|
+
include Representable::JSON::Hash
|
419
|
+
end
|
364
420
|
|
365
|
-
|
366
|
-
|
421
|
+
{"Nick" => "Hyper Music", "El" => "Blown In The Wind"}.extend(FavoriteSongsRepresenter).to_json
|
422
|
+
#=> {"Nick":"Hyper Music","El":"Blown In The Wind"}
|
423
|
+
```
|
367
424
|
|
368
425
|
Works both ways. The values are configurable and might be self-representing objects in turn. Tell the `Hash` by using `#values`.
|
369
426
|
|
370
|
-
|
371
|
-
|
427
|
+
```ruby
|
428
|
+
module FavoriteSongsRepresenter
|
429
|
+
include Representable::JSON::Hash
|
372
430
|
|
373
|
-
|
374
|
-
|
431
|
+
values extend: SongRepresenter, class: Song
|
432
|
+
end
|
375
433
|
|
376
|
-
|
434
|
+
{"Nick" => Song.new(title: "Hyper Music")}.extend(FavoriteSongsRepresenter).to_json
|
435
|
+
```
|
377
436
|
|
378
|
-
|
437
|
+
In XML, if you want to store hash attributes in tag attributes instead of dedicated nodes, use `XML::AttributeHash`.
|
379
438
|
|
380
439
|
## Lonely Collections
|
381
440
|
|
382
441
|
Same goes with arrays.
|
383
442
|
|
384
|
-
|
443
|
+
```ruby
|
444
|
+
require 'representable/json/collection'
|
385
445
|
|
386
|
-
|
387
|
-
|
446
|
+
module SongsRepresenter
|
447
|
+
include Representable::JSON::Collection
|
388
448
|
|
389
|
-
|
390
|
-
|
449
|
+
items extend: SongRepresenter, class: Song
|
450
|
+
end
|
451
|
+
```
|
391
452
|
|
392
453
|
The `#items` method lets you configure the contained entity representing here.
|
393
454
|
|
394
|
-
|
395
|
-
|
455
|
+
```ruby
|
456
|
+
[Song.new(title: "Hyper Music"), Song.new(title: "Screenager")].extend(SongsRepresenter).to_json
|
457
|
+
#=> [{"title":"Hyper Music"},{"title":"Screenager"}]
|
458
|
+
```
|
396
459
|
|
397
460
|
Note that this also works for XML.
|
398
461
|
|
@@ -401,22 +464,25 @@ Note that this also works for XML.
|
|
401
464
|
|
402
465
|
Representable also comes with a YAML representer.
|
403
466
|
|
404
|
-
|
405
|
-
|
467
|
+
```ruby
|
468
|
+
module SongRepresenter
|
469
|
+
include Representable::YAML
|
406
470
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
471
|
+
property :title
|
472
|
+
property :track
|
473
|
+
collection :composers, :style => :flow
|
474
|
+
end
|
475
|
+
```
|
411
476
|
|
412
477
|
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!
|
413
478
|
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
479
|
+
```ruby
|
480
|
+
song.extend(SongRepresenter).to_yaml
|
481
|
+
#=>
|
482
|
+
---
|
483
|
+
title: Fallout
|
484
|
+
composers: [Steward Copeland, Sting]
|
485
|
+
```
|
420
486
|
|
421
487
|
## More on XML
|
422
488
|
|
@@ -424,15 +490,17 @@ A nice feature is that `#collection` also accepts a `:style` option which helps
|
|
424
490
|
|
425
491
|
You can also map properties to tag attributes in representable.
|
426
492
|
|
427
|
-
|
428
|
-
|
493
|
+
```ruby
|
494
|
+
module SongRepresenter
|
495
|
+
include Representable::XML
|
429
496
|
|
430
|
-
|
431
|
-
|
432
|
-
|
497
|
+
property :title, attribute: true
|
498
|
+
property :track, attribute: true
|
499
|
+
end
|
433
500
|
|
434
|
-
|
435
|
-
|
501
|
+
Song.new(title: "American Idle").to_xml
|
502
|
+
#=> <song title="American Idle" />
|
503
|
+
```
|
436
504
|
|
437
505
|
Naturally, this works for both ways.
|
438
506
|
|
@@ -440,33 +508,38 @@ Naturally, this works for both ways.
|
|
440
508
|
|
441
509
|
It is sometimes unavoidable to wrap tag lists in a container tag.
|
442
510
|
|
443
|
-
|
444
|
-
|
511
|
+
```ruby
|
512
|
+
module AlbumRepresenter
|
513
|
+
include Representable::XML
|
445
514
|
|
446
|
-
|
447
|
-
|
515
|
+
collection :songs, :as => :song, :wrap => :songs
|
516
|
+
end
|
517
|
+
```
|
448
518
|
|
449
519
|
Note that `:wrap` defines the container tag name.
|
450
520
|
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
</
|
458
|
-
|
459
|
-
|
521
|
+
```xml
|
522
|
+
Album.new.to_xml #=>
|
523
|
+
<album>
|
524
|
+
<songs>
|
525
|
+
<song>Laundry Basket</song>
|
526
|
+
<song>Two Kevins</song>
|
527
|
+
<song>Wright and Rong</song>
|
528
|
+
</songs>
|
529
|
+
</album>
|
530
|
+
```
|
460
531
|
|
461
532
|
## Avoiding Modules
|
462
533
|
|
463
534
|
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.
|
464
535
|
|
465
|
-
|
466
|
-
|
536
|
+
```ruby
|
537
|
+
class Song < OpenStruct
|
538
|
+
include Representable::JSON
|
467
539
|
|
468
|
-
|
469
|
-
|
540
|
+
property :name
|
541
|
+
end
|
542
|
+
```
|
470
543
|
|
471
544
|
I do not recommend this approach as it bloats your domain classes with representation logic that is barely needed elsewhere.
|
472
545
|
|
@@ -480,19 +553,25 @@ Here's a quick overview about other available options for `#property` and its br
|
|
480
553
|
|
481
554
|
This can be handy if a property needs to be compiled from several fragments. The lambda has access to the entire object document (either hash or `Nokogiri` node) and user options.
|
482
555
|
|
483
|
-
|
556
|
+
```ruby
|
557
|
+
property :title, :writer => lambda { |doc, args| doc["title"] = title || original_title }
|
558
|
+
```
|
484
559
|
|
485
560
|
When using the `:writer` option it is up to you to add fragments to the `doc` - representable won't add anything for this property.
|
486
561
|
|
487
562
|
The same works for parsing using `:reader`.
|
488
563
|
|
489
|
-
|
564
|
+
```ruby
|
565
|
+
property :title, :reader => lambda { |doc, args| self.title = doc["title"] || doc["name"] }
|
566
|
+
```
|
490
567
|
|
491
568
|
### Read/Write Restrictions
|
492
569
|
|
493
570
|
Using the `:readable` and `:writeable` options access to properties can be restricted.
|
494
571
|
|
495
|
-
|
572
|
+
```ruby
|
573
|
+
property :title, :readable => false
|
574
|
+
```
|
496
575
|
|
497
576
|
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.
|
498
577
|
|
@@ -501,31 +580,37 @@ This will leave out the `title` property in the rendered document. Vice-versa, `
|
|
501
580
|
|
502
581
|
Representable also allows you to skip and include properties using the `:exclude` and `:include` options passed directly to the respective method.
|
503
582
|
|
504
|
-
|
505
|
-
|
506
|
-
|
583
|
+
```ruby
|
584
|
+
song.to_json(:include => :title)
|
585
|
+
#=> {"title":"Roxanne"}
|
586
|
+
```
|
507
587
|
|
508
588
|
### Conditions
|
509
589
|
|
510
590
|
You can also define conditions on properties using `:if`, making them being considered only when the block returns a true value.
|
511
591
|
|
512
|
-
|
513
|
-
|
592
|
+
```ruby
|
593
|
+
module SongRepresenter
|
594
|
+
include Representable::JSON
|
514
595
|
|
515
|
-
|
516
|
-
|
517
|
-
|
596
|
+
property :title
|
597
|
+
property :track, if: lambda { track > 0 }
|
598
|
+
end
|
599
|
+
```
|
518
600
|
|
519
601
|
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.
|
520
602
|
|
521
603
|
As always, the block retrieves your options. Given this render call
|
522
604
|
|
523
|
-
|
605
|
+
```ruby
|
606
|
+
song.to_json(minimum_track: 2)
|
607
|
+
```
|
524
608
|
|
525
609
|
your `:if` may process the options.
|
526
610
|
|
527
|
-
|
528
|
-
|
611
|
+
```ruby
|
612
|
+
property :track, if: lambda { |opts| track > opts[:minimum_track] }
|
613
|
+
```
|
529
614
|
|
530
615
|
### False and Nil Values
|
531
616
|
|
@@ -533,7 +618,9 @@ Since representable-1.2 `false` values _are_ considered when parsing and renderi
|
|
533
618
|
|
534
619
|
If you want `nil` values to be included when rendering, use the `:render_nil` option.
|
535
620
|
|
536
|
-
|
621
|
+
```ruby
|
622
|
+
property :track, render_nil: true
|
623
|
+
```
|
537
624
|
|
538
625
|
## Coercion
|
539
626
|
|
@@ -541,18 +628,21 @@ If you fancy coercion when parsing a document you can use the Coercion module wh
|
|
541
628
|
|
542
629
|
Include virtus in your Gemfile, first. Be sure to include virtus 0.5.0 or greater.
|
543
630
|
|
544
|
-
|
631
|
+
```ruby
|
632
|
+
gem 'virtus', ">= 0.5.0"
|
633
|
+
```
|
545
634
|
|
546
635
|
Use the `:type` option to specify the conversion target. Note that `:default` still works.
|
547
636
|
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
property :title
|
553
|
-
property :recorded_at, :type => DateTime, :default => "May 12th, 2012"
|
554
|
-
end
|
637
|
+
```ruby
|
638
|
+
module SongRepresenter
|
639
|
+
include Representable::JSON
|
640
|
+
include Representable::Coercion
|
555
641
|
|
642
|
+
property :title
|
643
|
+
property :recorded_at, :type => DateTime, :default => "May 12th, 2012"
|
644
|
+
end
|
645
|
+
```
|
556
646
|
|
557
647
|
## Undocumented Features
|
558
648
|
|
@@ -561,14 +651,18 @@ Use the `:type` option to specify the conversion target. Note that `:default` st
|
|
561
651
|
* If you need a special binding for a property you're free to create it using the `:binding` option.
|
562
652
|
|
563
653
|
<!-- here comes some code -->
|
564
|
-
|
654
|
+
```ruby
|
655
|
+
property :title, :binding => lambda { |*args| JSON::TitleBinding.new(*args) }
|
656
|
+
```
|
565
657
|
|
566
658
|
* Lambdas are usually executed in the represented object's context. If your writing a `Decorator` representer and you need to execute lambdas in its context use the `:representer_exec` option.
|
567
659
|
|
568
660
|
<!-- and some more in a beautiful cuddle -->
|
569
|
-
|
570
|
-
|
571
|
-
|
661
|
+
```ruby
|
662
|
+
class SongRepresenter < Representable::Decorator
|
663
|
+
property :title, :representer_exec => true, :getter => lambda {..}
|
664
|
+
end
|
665
|
+
```
|
572
666
|
|
573
667
|
You can still access the represented object in the lambda using `represented`. In a module representer this option is ignored.
|
574
668
|
|
@@ -579,4 +673,4 @@ Representable started as a heavily simplified fork of the ROXML gem. Big thanks
|
|
579
673
|
* Copyright (c) 2011-2013 Nick Sutterer <apotonick@gmail.com>
|
580
674
|
* ROXML is Copyright (c) 2004-2009 Ben Woosley, Zak Mandhro and Anders Engstrom.
|
581
675
|
|
582
|
-
Representable is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
676
|
+
Representable is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
data/TODO
CHANGED
@@ -36,17 +36,17 @@ module Representable
|
|
36
36
|
|
37
37
|
# Retrieve value and write fragment to the doc.
|
38
38
|
def compile_fragment(doc)
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
represented_exec_for(:writer, doc) do
|
40
|
+
write_fragment(doc, get)
|
41
|
+
end
|
42
42
|
end
|
43
43
|
|
44
44
|
# Parse value from doc and update the model property.
|
45
45
|
def uncompile_fragment(doc)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
represented_exec_for(:reader, doc) do
|
47
|
+
read_fragment(doc) do |value|
|
48
|
+
set(value)
|
49
|
+
end
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
@@ -77,20 +77,31 @@ module Representable
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def get
|
80
|
-
|
81
|
-
|
80
|
+
represented_exec_for(:getter) do
|
81
|
+
represented.send(getter)
|
82
|
+
end
|
82
83
|
end
|
83
84
|
|
84
85
|
def set(value)
|
85
|
-
|
86
|
-
|
86
|
+
represented_exec_for(:setter, value) do
|
87
|
+
represented.send(setter, value)
|
88
|
+
end
|
87
89
|
end
|
88
90
|
|
89
91
|
private
|
90
92
|
# Execute the block for +option_name+ on the represented object.
|
93
|
+
# Executes passed block when there's no lambda for option.
|
91
94
|
def represented_exec_for(option_name, *args)
|
92
|
-
return unless options[option_name]
|
93
|
-
|
95
|
+
return yield unless options[option_name]
|
96
|
+
call_proc_for(options[option_name], *args)
|
97
|
+
end
|
98
|
+
|
99
|
+
# All lambdas are executed on lambda_context which is either represented or the decorator instance.
|
100
|
+
def call_proc_for(proc, *args)
|
101
|
+
return proc unless proc.is_a?(Proc)
|
102
|
+
# TODO: call method when proc is sympbol.
|
103
|
+
args << user_options # DISCUSS: we assume user_options is a Hash!
|
104
|
+
lambda_context.instance_exec(*args, &proc)
|
94
105
|
end
|
95
106
|
|
96
107
|
|
@@ -117,12 +128,6 @@ module Representable
|
|
117
128
|
def representer_module_for(object, *args)
|
118
129
|
call_proc_for(representer_module, object) # TODO: how to pass additional data to the computing block?`
|
119
130
|
end
|
120
|
-
|
121
|
-
def call_proc_for(proc, *args)
|
122
|
-
return proc unless proc.is_a?(Proc)
|
123
|
-
# DISCUSS: use represented_exec_for here?
|
124
|
-
@represented.instance_exec(*args, &proc)
|
125
|
-
end
|
126
131
|
end
|
127
132
|
|
128
133
|
# Overrides #serialize/#deserialize to call #to_*/from_*.
|
data/test/representable_test.rb
CHANGED
@@ -558,7 +558,7 @@ class RepresentableTest < MiniTest::Spec
|
|
558
558
|
|
559
559
|
describe "lambda blocks" do
|
560
560
|
representer! do
|
561
|
-
property :name, :extend => lambda { |name
|
561
|
+
property :name, :extend => lambda { |name, *| compute_representer(name) }
|
562
562
|
end
|
563
563
|
|
564
564
|
it "executes lambda in represented instance context" do
|
@@ -575,7 +575,7 @@ class RepresentableTest < MiniTest::Spec
|
|
575
575
|
obj = String.new("Fate")
|
576
576
|
mod = Module.new { include Representable; def from_hash(*); self; end }
|
577
577
|
representer! do
|
578
|
-
property :name, :extend => mod, :instance => lambda {
|
578
|
+
property :name, :extend => mod, :instance => lambda { |*| obj }
|
579
579
|
end
|
580
580
|
|
581
581
|
it "uses object from :instance but still extends it" do
|
@@ -587,7 +587,7 @@ class RepresentableTest < MiniTest::Spec
|
|
587
587
|
|
588
588
|
describe "property with :extend" do
|
589
589
|
representer! do
|
590
|
-
property :name, :extend => lambda { |name
|
590
|
+
property :name, :extend => lambda { |name, *| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => String
|
591
591
|
end
|
592
592
|
|
593
593
|
it "uses lambda when rendering" do
|
@@ -602,8 +602,8 @@ class RepresentableTest < MiniTest::Spec
|
|
602
602
|
|
603
603
|
describe "with :class lambda" do
|
604
604
|
representer! do
|
605
|
-
property :name, :extend => lambda { |name
|
606
|
-
:class => lambda { |fragment
|
605
|
+
property :name, :extend => lambda { |name, *| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter },
|
606
|
+
:class => lambda { |fragment, *| fragment == "Still Failing?" ? String : UpcaseString }
|
607
607
|
end
|
608
608
|
|
609
609
|
it "creates instance from :class lambda when parsing" do
|
@@ -618,7 +618,7 @@ class RepresentableTest < MiniTest::Spec
|
|
618
618
|
|
619
619
|
describe "when :class lambda returns nil" do
|
620
620
|
representer! do
|
621
|
-
property :name, :extend => lambda {
|
621
|
+
property :name, :extend => lambda { |*| Module.new { include Representable; def from_hash(data, *args); data; end } },
|
622
622
|
:class => nil
|
623
623
|
end
|
624
624
|
|
@@ -633,7 +633,7 @@ class RepresentableTest < MiniTest::Spec
|
|
633
633
|
|
634
634
|
describe "collection with :extend" do
|
635
635
|
representer! do
|
636
|
-
collection :songs, :extend => lambda { |name
|
636
|
+
collection :songs, :extend => lambda { |name, *| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => String
|
637
637
|
end
|
638
638
|
|
639
639
|
it "uses lambda for each item when rendering" do
|
@@ -647,8 +647,8 @@ class RepresentableTest < MiniTest::Spec
|
|
647
647
|
|
648
648
|
describe "with :class lambda" do
|
649
649
|
representer! do
|
650
|
-
collection :songs, :extend => lambda { |name
|
651
|
-
:class => lambda { |fragment
|
650
|
+
collection :songs, :extend => lambda { |name, *| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter },
|
651
|
+
:class => lambda { |fragment, *| fragment == "Still Failing?" ? String : UpcaseString }
|
652
652
|
end
|
653
653
|
|
654
654
|
it "creates instance from :class lambda for each item when parsing" do
|
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.5.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-05-
|
12
|
+
date: 2013-05-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: nokogiri
|