api-presenter 0.0.1 → 0.0.2
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.
- data/README.md +336 -2
- data/lib/api/presenter/hypermedia.rb +1 -1
- data/lib/api/presenter/version.rb +1 -1
- data/spec/hypertext_presenter_spec.rb +44 -18
- metadata +2 -2
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Api::Presenter
|
2
2
|
|
3
|
-
|
3
|
+
This gem builds the basics for presenting your data using the media type described in the [api doc](https://github.com/ncuesta/api-doc). Here you will find classes to represent your resources
|
4
|
+
and also the functions to convert them to json.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
@@ -18,7 +19,340 @@ Or install it yourself as:
|
|
18
19
|
|
19
20
|
## Usage
|
20
21
|
|
21
|
-
|
22
|
+
So, first you should now that there are three kinds of resources:
|
23
|
+
|
24
|
+
* A simple resource (Api::Presenter::Resource) that represents a single unit of information.
|
25
|
+
* A collection resource (Api::Presenter::CollectionResource) that represents an homogeneous group of resources.
|
26
|
+
* A search resource (Api::Presenter::SearchResource) it's like a collection resource but adds the query information.
|
27
|
+
|
28
|
+
### Simple Resource
|
29
|
+
|
30
|
+
Now, most probably you are needing to represent some class that contains your model information using the media type
|
31
|
+
described by [api-doc](https://github.com/ncuesta/api-doc). For this you will be needing two things:
|
32
|
+
|
33
|
+
1. Create a class that represents your resource.
|
34
|
+
2. Let your model class know how to turn into a resource.
|
35
|
+
|
36
|
+
Let's say you have a Person model:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class Person
|
40
|
+
attr_accessor :name, :age
|
41
|
+
|
42
|
+
def initialize(name, age)
|
43
|
+
@name = name
|
44
|
+
@age = age
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
In order to be able to represent this resource we need to have a PersonResource class
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class PersonResource < Api::Presenter::Resource
|
53
|
+
def self.hypermedia_properties
|
54
|
+
{
|
55
|
+
simple: [:name, :age],
|
56
|
+
resource: []
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def self_link
|
61
|
+
"/person/#{@resource.name}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
The class method ```hypermedia_properties``` describes the model properties:
|
67
|
+
|
68
|
+
* simple: An array containing method names that represent simple data (integers, strings, dates, etc.).
|
69
|
+
* resource: An array containing method names thar represent related resources.
|
70
|
+
|
71
|
+
The self_link definition tell which is the link that represents itself.
|
72
|
+
|
73
|
+
So now there is only one more thing left to do: Add the to_resource method used to convert the model
|
74
|
+
into a resource.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
class Person
|
78
|
+
def to_resource
|
79
|
+
PersonResource.new(self)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
And that's it, now we can get the representation in json using.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
data = Person.new("Alvaro", 27)
|
88
|
+
|
89
|
+
resource = data.to_resource # or just PersonResource.new(data)
|
90
|
+
|
91
|
+
Api::Presenter::Hypermedia.present resource
|
92
|
+
```
|
93
|
+
|
94
|
+
It will look like this:
|
95
|
+
```json
|
96
|
+
{
|
97
|
+
"links":
|
98
|
+
{
|
99
|
+
"self":
|
100
|
+
{
|
101
|
+
"href": "/person/Alvaro"
|
102
|
+
}
|
103
|
+
}
|
104
|
+
"name": "Alvaro",
|
105
|
+
"age": 27
|
106
|
+
}
|
107
|
+
```
|
108
|
+
### Related resources
|
109
|
+
|
110
|
+
It's very common that our model is related to others, and we may want to show this in our representation.
|
111
|
+
To do so, need to create a resource class for each one and add them to our ```resource``` array in
|
112
|
+
hypermedia_properties.
|
113
|
+
|
114
|
+
Using the example above, now our person has a dog. So:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class DogResource < Api::Presenter::Resource
|
118
|
+
def self.hypermedia_properties
|
119
|
+
{
|
120
|
+
simple: [:name],
|
121
|
+
resource: [:owner]
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def self_link
|
126
|
+
"/dog/#{@resource.name}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Dog
|
131
|
+
attr_accessor :name, :owner
|
132
|
+
|
133
|
+
def initialize(name, owner)
|
134
|
+
@name = name
|
135
|
+
@owner = owner
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_resource
|
139
|
+
DogResource.new(self)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class PersonResource < Api::Presenter::Resource
|
144
|
+
def self.hypermedia_properties
|
145
|
+
{
|
146
|
+
simple: [:name, :age],
|
147
|
+
resource: [:dog]
|
148
|
+
}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
Finally we present it:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
person = Person.new("Alvaro", 27)
|
157
|
+
|
158
|
+
dog = Dog.new("Cleo", person)
|
159
|
+
|
160
|
+
person_resource = person.to_resource # or just PersonResource.new(person)
|
161
|
+
|
162
|
+
Api::Presenter::Hypermedia.present person_resource
|
163
|
+
```
|
164
|
+
|
165
|
+
It will look like this:
|
166
|
+
|
167
|
+
```json
|
168
|
+
{
|
169
|
+
"links":
|
170
|
+
{
|
171
|
+
"self":
|
172
|
+
{
|
173
|
+
"href": "/person/Alvaro"
|
174
|
+
},
|
175
|
+
"dog":
|
176
|
+
{
|
177
|
+
"href": "/dog/Cleo"
|
178
|
+
}
|
179
|
+
}
|
180
|
+
"name": "Alvaro",
|
181
|
+
"age": 27
|
182
|
+
}
|
183
|
+
```
|
184
|
+
|
185
|
+
### Collection resource
|
186
|
+
|
187
|
+
A collection resource it's basically any collection that contains objects that responds to ```to_resource```,
|
188
|
+
also must respond to:
|
189
|
+
|
190
|
+
* total
|
191
|
+
* offset
|
192
|
+
* limit
|
193
|
+
* each
|
194
|
+
|
195
|
+
Now, using the Person example:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
# building a collection on top of array that responds to each, total, limit and offset
|
199
|
+
class Collection
|
200
|
+
attr_reader :col
|
201
|
+
|
202
|
+
def initialize(col = [])
|
203
|
+
@col = col
|
204
|
+
end
|
205
|
+
|
206
|
+
def each(&block)
|
207
|
+
@col.each(&block)
|
208
|
+
end
|
209
|
+
|
210
|
+
def total
|
211
|
+
@col.count
|
212
|
+
end
|
213
|
+
|
214
|
+
def limit
|
215
|
+
10
|
216
|
+
end
|
217
|
+
|
218
|
+
def offset
|
219
|
+
0
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
family = Collection.new([Person.new("Joe", 50), Person.new("Jane", 45), Person.new("Timmy", 10), Person.new("Sussie", 12)])
|
224
|
+
|
225
|
+
class FamilyResource < Api::Presenter::CollectionResource
|
226
|
+
def self_link
|
227
|
+
"/family"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
Api::Presenter::Hypermedia.present FamilyResource.new(family)
|
232
|
+
```
|
233
|
+
|
234
|
+
It will look like this:
|
235
|
+
|
236
|
+
```json
|
237
|
+
{
|
238
|
+
"links":
|
239
|
+
{
|
240
|
+
"self":
|
241
|
+
{
|
242
|
+
"href": "/family"
|
243
|
+
},
|
244
|
+
}
|
245
|
+
"offset": 0,
|
246
|
+
"limit": 10,
|
247
|
+
"total": 4,
|
248
|
+
"entries":
|
249
|
+
[
|
250
|
+
{
|
251
|
+
"self":
|
252
|
+
{
|
253
|
+
"href" : "/person/Joe"
|
254
|
+
}
|
255
|
+
},
|
256
|
+
{
|
257
|
+
"self":
|
258
|
+
{
|
259
|
+
"href" : "/person/Jane"
|
260
|
+
}
|
261
|
+
},
|
262
|
+
{
|
263
|
+
"self":
|
264
|
+
{
|
265
|
+
"href" : "/person/Timmy"
|
266
|
+
},
|
267
|
+
},
|
268
|
+
{
|
269
|
+
"self":
|
270
|
+
{
|
271
|
+
"href" : "/person/Sussie"
|
272
|
+
}
|
273
|
+
}
|
274
|
+
]
|
275
|
+
}
|
276
|
+
```
|
277
|
+
|
278
|
+
### Search resource
|
279
|
+
|
280
|
+
This is a special case of ```Api::Presenter::CollectionResource``` where it also has a query string and parameters.
|
281
|
+
The main difference with a Collection is that it receives as a parameter, the parameters which where used to build
|
282
|
+
the collection and also adds them to the response.
|
283
|
+
|
284
|
+
The method ```self.hypermedia_query_parameters``` determins which parameters are used in the search. It's later used
|
285
|
+
by the helper method ```query_string``` to build the query string.
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
class PersonSearchResource < Api::Presenter::SearchResource
|
289
|
+
def self.hypermedia_query_parameters
|
290
|
+
["name", "age"]
|
291
|
+
end
|
292
|
+
|
293
|
+
def self_link
|
294
|
+
"/search_person#{query_string}"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
search = PersonSearchResource.new(Collection.new([Person.new("Joe", 50), Person.new("Jane", 45)]), age: 45)
|
299
|
+
|
300
|
+
Api::Presenter::Hypermedia.present search
|
301
|
+
```
|
302
|
+
|
303
|
+
It will look like this:
|
304
|
+
|
305
|
+
```json
|
306
|
+
{
|
307
|
+
"links":
|
308
|
+
{
|
309
|
+
"self":
|
310
|
+
{
|
311
|
+
"href": "/search_person?query[age]=45query[name]="
|
312
|
+
},
|
313
|
+
}
|
314
|
+
"offset": 0,
|
315
|
+
"limit": 10,
|
316
|
+
"total": 2,
|
317
|
+
"query":
|
318
|
+
{
|
319
|
+
"age": 45,
|
320
|
+
"name": nil
|
321
|
+
},
|
322
|
+
"entries":
|
323
|
+
[
|
324
|
+
{
|
325
|
+
"self":
|
326
|
+
{
|
327
|
+
"href" : "/person/Joe"
|
328
|
+
}
|
329
|
+
},
|
330
|
+
{
|
331
|
+
"self":
|
332
|
+
{
|
333
|
+
"href" : "/person/Jane"
|
334
|
+
}
|
335
|
+
}
|
336
|
+
]
|
337
|
+
}
|
338
|
+
```
|
339
|
+
|
340
|
+
### Adding additional links
|
341
|
+
|
342
|
+
When building a resource there may be the need to build custom links. In order to do so, you need to define
|
343
|
+
two methods in your resource class:
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
def custom_link
|
347
|
+
"/path/to/custom_link"
|
348
|
+
end
|
349
|
+
|
350
|
+
|
351
|
+
def custom_link?(options = {})
|
352
|
+
a_condition_that_determins_if_it_should_be_displayed returning true or false
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
22
356
|
|
23
357
|
## Contributing
|
24
358
|
|
@@ -27,7 +27,7 @@ module Api
|
|
27
27
|
if entries_property
|
28
28
|
representation[entries_property.to_s] = []
|
29
29
|
resource.send(entries_property).each do |nested_resource|
|
30
|
-
representation[entries_property.to_s] << nested_resource.to_resource
|
30
|
+
representation[entries_property.to_s] << build_links(nested_resource.to_resource, embed: true)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -21,27 +21,39 @@ describe Api::Presenter::Hypermedia do
|
|
21
21
|
"entries" =>
|
22
22
|
[
|
23
23
|
{
|
24
|
-
"
|
24
|
+
"links" =>
|
25
25
|
{
|
26
|
-
"
|
26
|
+
"self" =>
|
27
|
+
{
|
28
|
+
"href" => "/path/to/single_resource/1"
|
29
|
+
}
|
27
30
|
}
|
28
31
|
},
|
29
32
|
{
|
30
|
-
"
|
33
|
+
"links" =>
|
31
34
|
{
|
32
|
-
"
|
35
|
+
"self" =>
|
36
|
+
{
|
37
|
+
"href" => "/path/to/single_resource/2"
|
38
|
+
}
|
33
39
|
}
|
34
40
|
},
|
35
41
|
{
|
36
|
-
"
|
42
|
+
"links" =>
|
37
43
|
{
|
38
|
-
"
|
44
|
+
"self" =>
|
45
|
+
{
|
46
|
+
"href" => "/path/to/single_resource/3"
|
47
|
+
}
|
39
48
|
}
|
40
49
|
},
|
41
50
|
{
|
42
|
-
"
|
51
|
+
"links" =>
|
43
52
|
{
|
44
|
-
"
|
53
|
+
"self" =>
|
54
|
+
{
|
55
|
+
"href" => "/path/to/single_resource/4"
|
56
|
+
}
|
45
57
|
}
|
46
58
|
}
|
47
59
|
]
|
@@ -91,30 +103,44 @@ describe Api::Presenter::Hypermedia do
|
|
91
103
|
},
|
92
104
|
|
93
105
|
"entries" =>
|
94
|
-
[
|
95
|
-
|
106
|
+
[
|
107
|
+
{
|
108
|
+
"links" =>
|
96
109
|
{
|
97
|
-
"
|
110
|
+
"self" =>
|
111
|
+
{
|
112
|
+
"href" => "/path/to/single_resource/1"
|
113
|
+
}
|
98
114
|
}
|
99
115
|
},
|
100
116
|
{
|
101
|
-
"
|
117
|
+
"links" =>
|
102
118
|
{
|
103
|
-
"
|
119
|
+
"self" =>
|
120
|
+
{
|
121
|
+
"href" => "/path/to/single_resource/2"
|
122
|
+
}
|
104
123
|
}
|
105
124
|
},
|
106
125
|
{
|
107
|
-
"
|
126
|
+
"links" =>
|
108
127
|
{
|
109
|
-
"
|
128
|
+
"self" =>
|
129
|
+
{
|
130
|
+
"href" => "/path/to/single_resource/3"
|
131
|
+
}
|
110
132
|
}
|
111
133
|
},
|
112
134
|
{
|
113
|
-
"
|
135
|
+
"links" =>
|
114
136
|
{
|
115
|
-
"
|
137
|
+
"self" =>
|
138
|
+
{
|
139
|
+
"href" => "/path/to/single_resource/4"
|
140
|
+
}
|
116
141
|
}
|
117
|
-
}
|
142
|
+
}
|
143
|
+
]
|
118
144
|
}
|
119
145
|
end
|
120
146
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api-presenter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
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-06-
|
12
|
+
date: 2013-06-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|