api-presenter 0.0.3.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +7 -0
- data/Gemfile +5 -0
- data/README.md +59 -20
- data/Rakefile +3 -1
- data/lib/api/presenter/hypermedia.rb +25 -16
- data/lib/api/presenter/resource.rb +30 -4
- data/lib/api/presenter/search_resource.rb +14 -2
- data/lib/api/presenter/version.rb +1 -1
- data/spec/hypertext_presenter_mocks.rb +8 -22
- data/spec/hypertext_presenter_spec.rb +90 -30
- data/spec/spec_helper.rb +1 -2
- metadata +3 -2
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -54,15 +54,16 @@ class PersonResource < Api::Presenter::Resource
|
|
54
54
|
property :name
|
55
55
|
property :age
|
56
56
|
|
57
|
-
|
58
|
-
"/person/#{@resource.name}"
|
59
|
-
end
|
57
|
+
link "self", "/person/{{name}}"
|
60
58
|
end
|
61
59
|
```
|
62
60
|
|
63
61
|
We should use the property method to define each of our resource properties.
|
64
62
|
|
65
|
-
The
|
63
|
+
The ```link``` method defines there is a "self" link which represents itself. It receives, the name of the link,
|
64
|
+
the url representing it and optionally a block, that may receive a hash with options and should return a boolean.
|
65
|
+
|
66
|
+
The stubs that look like ```{{method_name}}``` will be latter replaced with a method call to ```method_name``` on the resource.
|
66
67
|
|
67
68
|
So now there is only one more thing left to do: Add the to_resource method used to convert the model
|
68
69
|
into a resource.
|
@@ -111,9 +112,7 @@ class DogResource < Api::Presenter::Resource
|
|
111
112
|
property :name
|
112
113
|
property :owner
|
113
114
|
|
114
|
-
|
115
|
-
"/dog/#{@resource.name}"
|
116
|
-
end
|
115
|
+
link "self", "/dog/{{name}}"
|
117
116
|
end
|
118
117
|
|
119
118
|
class Dog
|
@@ -267,8 +266,7 @@ This is a special case of ```Api::Presenter::CollectionResource``` where it also
|
|
267
266
|
The main difference with a Collection is that it receives as a parameter, the parameters which where used to build
|
268
267
|
the collection and also adds them to the response.
|
269
268
|
|
270
|
-
The method ```self.hypermedia_query_parameters``` determins which parameters are used in the search. It's later used
|
271
|
-
by the helper method ```query_string``` to build the query string.
|
269
|
+
The method ```self.hypermedia_query_parameters``` determins which parameters are used in the search. It's later used to build the query string for the url.
|
272
270
|
|
273
271
|
```ruby
|
274
272
|
class PersonSearchResource < Api::Presenter::SearchResource
|
@@ -276,9 +274,7 @@ class PersonSearchResource < Api::Presenter::SearchResource
|
|
276
274
|
["name", "age"]
|
277
275
|
end
|
278
276
|
|
279
|
-
|
280
|
-
"/search_person#{query_string}"
|
281
|
-
end
|
277
|
+
link "self", "/search_person"
|
282
278
|
end
|
283
279
|
|
284
280
|
search = PersonSearchResource.new(Collection.new([Person.new("Joe", 50), Person.new("Jane", 45)]), age: 45)
|
@@ -325,19 +321,62 @@ It will look like this:
|
|
325
321
|
|
326
322
|
### Adding additional links
|
327
323
|
|
328
|
-
When building a resource there may be the need to build custom links. In order to do so,
|
329
|
-
|
324
|
+
When building a resource there may be the need to build custom links. In order to do so, can use
|
325
|
+
the ```link``` class method:
|
330
326
|
|
331
327
|
```ruby
|
332
|
-
|
333
|
-
|
334
|
-
end
|
328
|
+
link("custom", "/path/to/custom_link") { |options = {}| a_condition_that_determins_if_it_should_be_displayed returning true or false }
|
329
|
+
```
|
335
330
|
|
336
|
-
|
337
|
-
|
338
|
-
|
331
|
+
You can also use special stubs (enclosed with ```{{method_name}}``` ) that will latter be resolved in call to the resource, like this:
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
link("custom", "/path/to/custom_link/{{method_call}}") { |options = {}| a_condition_that_determins_if_it_should_be_displayed returning true or false }
|
339
335
|
```
|
340
336
|
|
337
|
+
### Using full links
|
338
|
+
If you want to use full links and not parcial, you can say:
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
Api::Presenter::Resource.host = "http://you.domain.goes.here:port"
|
342
|
+
```
|
343
|
+
|
344
|
+
Now links will be display like this:
|
345
|
+
|
346
|
+
```json
|
347
|
+
{
|
348
|
+
"links":
|
349
|
+
{
|
350
|
+
"self":
|
351
|
+
{
|
352
|
+
"href": "http://you.domain.goes.here:port/search_person?query[age]=45query[name]="
|
353
|
+
},
|
354
|
+
}
|
355
|
+
"offset": 0,
|
356
|
+
"limit": 10,
|
357
|
+
"total": 2,
|
358
|
+
"query":
|
359
|
+
{
|
360
|
+
"age": 45,
|
361
|
+
"name": nil
|
362
|
+
},
|
363
|
+
"entries":
|
364
|
+
[
|
365
|
+
{
|
366
|
+
"self":
|
367
|
+
{
|
368
|
+
"href" : "http://you.domain.goes.here:port/person/Joe"
|
369
|
+
}
|
370
|
+
},
|
371
|
+
{
|
372
|
+
"self":
|
373
|
+
{
|
374
|
+
"href" : "http://you.domain.goes.here:port/person/Jane"
|
375
|
+
}
|
376
|
+
}
|
377
|
+
]
|
378
|
+
}
|
379
|
+
```
|
341
380
|
|
342
381
|
## Contributing
|
343
382
|
|
data/Rakefile
CHANGED
@@ -17,35 +17,44 @@ module Api
|
|
17
17
|
def present_properties(resource, representation)
|
18
18
|
resource_properties = resource.class.properties.dup
|
19
19
|
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
representation[entries_property.to_s] = []
|
25
|
-
resource.send(entries_property).each do |nested_resource|
|
26
|
-
representation[entries_property.to_s] << build_links(nested_resource.to_resource, embed: true)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# Now the other muggles
|
20
|
+
# entries property receives special treatment
|
21
|
+
present_entries_property resource, representation, resource_properties if resource_properties.include? :entries
|
22
|
+
|
23
|
+
# Now the other muggles
|
31
24
|
resource_properties.each do |property_name|
|
32
25
|
property_value = resource.send(property_name)
|
33
26
|
|
34
27
|
if property_value.kind_of?(Resource) || property_value.respond_to?(:to_resource)
|
35
28
|
# Resource like properties
|
36
|
-
|
37
|
-
representation["links"][property_name.to_s] = property_value.links(embed: resource.kind_of?(CollectionResource))
|
38
|
-
# we only need the "self" contents
|
39
|
-
representation["links"][property_name.to_s] = representation["links"][property_name.to_s]["self"] if representation["links"][property_name.to_s]["self"]
|
29
|
+
present_resoure_property property_name, property_value, resource, representation
|
40
30
|
else
|
41
31
|
# Non-Resource like properties
|
42
32
|
representation[property_name.to_s] = property_value
|
43
33
|
end
|
44
34
|
end
|
45
35
|
end
|
36
|
+
|
37
|
+
def present_entries_property(resource, representation, resource_properties)
|
38
|
+
entries_property = resource_properties.delete(:entries)
|
39
|
+
|
40
|
+
representation[entries_property.to_s] = []
|
41
|
+
|
42
|
+
resource.send(entries_property).each do |nested_resource|
|
43
|
+
representation[entries_property.to_s] << build_links(nested_resource.to_resource, embed: true)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def present_resoure_property(property_name, property_value, resource, representation)
|
48
|
+
property_value = property_value.to_resource if property_value.respond_to? :to_resource
|
49
|
+
|
50
|
+
representation["links"][property_name.to_s] = property_value.build_links(embed: resource.kind_of?(CollectionResource))
|
51
|
+
|
52
|
+
# we only need the "self" contents
|
53
|
+
representation["links"][property_name.to_s] = representation["links"][property_name.to_s]["self"] if representation["links"][property_name.to_s]["self"]
|
54
|
+
end
|
46
55
|
|
47
56
|
def build_links(resource, options = {})
|
48
|
-
{ "links" => resource.
|
57
|
+
{ "links" => resource.build_links(options) }
|
49
58
|
end
|
50
59
|
end
|
51
60
|
end
|
@@ -14,6 +14,24 @@ module Api
|
|
14
14
|
@properties ||= []
|
15
15
|
end
|
16
16
|
|
17
|
+
def link(name, value, &condition)
|
18
|
+
condition = Proc.new { true } unless block_given?
|
19
|
+
|
20
|
+
links[name] = { value: value, condition: condition }
|
21
|
+
end
|
22
|
+
|
23
|
+
def links
|
24
|
+
@links ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def host
|
28
|
+
@@host ||= ''
|
29
|
+
end
|
30
|
+
|
31
|
+
def host=(v)
|
32
|
+
@@host = v
|
33
|
+
end
|
34
|
+
|
17
35
|
def inherited(subclass)
|
18
36
|
(subclass.properties << properties).flatten!
|
19
37
|
end
|
@@ -27,12 +45,20 @@ module Api
|
|
27
45
|
end
|
28
46
|
end
|
29
47
|
|
30
|
-
def
|
48
|
+
def build_links(options = {})
|
31
49
|
links = {}
|
32
50
|
|
33
|
-
self.
|
34
|
-
|
35
|
-
|
51
|
+
self.class.links.each do |link_name, link_value|
|
52
|
+
link_actual_value = link_value[:value].dup
|
53
|
+
|
54
|
+
# retrieve stubs to replace looking for {{method_to_call}}
|
55
|
+
stubs = link_actual_value.scan(/\{\{(\w+)\}\}/).flatten
|
56
|
+
|
57
|
+
# now we replace them
|
58
|
+
stubs.each{ |stub| link_actual_value.gsub!(/\{\{#{stub}\}\}/, self.send(stub.to_sym).to_s) }
|
59
|
+
|
60
|
+
# and finish the url
|
61
|
+
links[link_name.to_s] = { "href" => "#{self.class.host}#{link_actual_value}" } if link_value[:condition].call(options)
|
36
62
|
end
|
37
63
|
|
38
64
|
links
|
@@ -2,18 +2,30 @@ module Api
|
|
2
2
|
module Presenter
|
3
3
|
class SearchResource < CollectionResource
|
4
4
|
attr_reader :query
|
5
|
-
|
5
|
+
|
6
|
+
property :query
|
7
|
+
|
6
8
|
def initialize(resource, query)
|
7
9
|
@resource = resource
|
8
10
|
@query = query
|
9
11
|
end
|
10
12
|
|
11
|
-
property :query
|
12
13
|
|
13
14
|
def query_string
|
14
15
|
result = self.class.hypermedia_query_parameters.inject([]) { |col, query_parameter| col << "query[#{query_parameter}]=#{@query[query_parameter]}" }
|
15
16
|
"?" + result.join("&")
|
16
17
|
end
|
18
|
+
|
19
|
+
def build_links(options = {})
|
20
|
+
links = super
|
21
|
+
|
22
|
+
# this adds the query string
|
23
|
+
links.each do |link_name, link_value|
|
24
|
+
links[link_name]['href'] = "#{link_value['href']}#{query_string}"
|
25
|
+
end
|
26
|
+
|
27
|
+
links
|
28
|
+
end
|
17
29
|
end
|
18
30
|
end
|
19
31
|
end
|
@@ -36,23 +36,13 @@ module HypertextPresenterMocks
|
|
36
36
|
end
|
37
37
|
|
38
38
|
class MockSingleResource < Api::Presenter::Resource
|
39
|
-
|
40
39
|
property :number
|
41
40
|
property :string
|
42
41
|
property :date
|
43
42
|
property :sibling
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
def custom_link
|
50
|
-
"/path/to/custom_link"
|
51
|
-
end
|
52
|
-
|
53
|
-
def custom_link?(options = {})
|
54
|
-
options[:embed].nil? || !options[:embed]
|
55
|
-
end
|
43
|
+
|
44
|
+
link "self", "/path/to/single_resource/{{number}}"
|
45
|
+
link("custom", "/path/to/custom_link") { |options = {}| options[:embed].nil? || !options[:embed] }
|
56
46
|
|
57
47
|
def sibling
|
58
48
|
MockSingleResource.new(MockData.new(number: 20, string: "I'm a sibling of someone", date: (Date.today + 1)))
|
@@ -60,22 +50,18 @@ module HypertextPresenterMocks
|
|
60
50
|
end
|
61
51
|
|
62
52
|
class MockCollectionResource < Api::Presenter::CollectionResource
|
63
|
-
|
64
|
-
"/path/to/collection_resource"
|
65
|
-
end
|
53
|
+
link "self", "/path/to/collection_resource"
|
66
54
|
end
|
67
55
|
|
68
56
|
class MockSearchResource < Api::Presenter::SearchResource
|
69
57
|
def self.hypermedia_query_parameters
|
70
58
|
["page", "param1","param2"]
|
71
59
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
76
|
-
|
60
|
+
|
61
|
+
link "self", "/path/to/search_resource"
|
62
|
+
|
77
63
|
def current_page
|
78
64
|
1
|
79
65
|
end
|
80
66
|
end
|
81
|
-
end
|
67
|
+
end
|
@@ -145,43 +145,103 @@ describe Api::Presenter::Hypermedia do
|
|
145
145
|
]
|
146
146
|
}
|
147
147
|
end
|
148
|
+
|
149
|
+
let(:mock_data) { MockData.new(number: 1, string: 'This is a string', date: Date.today) }
|
150
|
+
|
151
|
+
describe "when defining properties" do
|
152
|
+
it "must add and display a property" do
|
153
|
+
class MockSpecificData < MockData
|
154
|
+
def floating_point_value
|
155
|
+
10.3
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_resource
|
159
|
+
MockSingleSpecificResource.new self
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class MockSingleSpecificResource < MockSingleResource
|
164
|
+
property :floating_point_value
|
165
|
+
|
166
|
+
link "self", "/path/to/single_resource/{{number}}"
|
167
|
+
link("custom", "/path/to/custom_link") { |options = {}| options[:embed].nil? || !options[:embed] }
|
168
|
+
end
|
169
|
+
|
170
|
+
new_single_resource_standard = single_resource_standard
|
171
|
+
new_single_resource_standard['floating_point_value'] = 10.3
|
172
|
+
|
173
|
+
mock_data = MockSpecificData.new(number: 1, string: 'This is a string', date: Date.today)
|
174
|
+
|
175
|
+
mock_data.to_resource.present.must_equal new_single_resource_standard
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe "when using host" do
|
180
|
+
it "must display a complete url" do
|
181
|
+
Api::Presenter::Resource.host = "http://localhost:9292"
|
182
|
+
|
183
|
+
mock_data.to_resource.present.wont_equal single_resource_standard
|
184
|
+
|
185
|
+
new_single_resource_standard = {
|
186
|
+
"links" =>
|
187
|
+
{
|
188
|
+
"self" =>
|
189
|
+
{
|
190
|
+
"href" => "http://localhost:9292/path/to/single_resource/1"
|
191
|
+
},
|
192
|
+
"custom" =>
|
193
|
+
{
|
194
|
+
"href" => "http://localhost:9292/path/to/custom_link"
|
195
|
+
},
|
196
|
+
"sibling" =>
|
197
|
+
{
|
198
|
+
"href" => "http://localhost:9292/path/to/single_resource/20"
|
199
|
+
}
|
200
|
+
},
|
201
|
+
"number" => 1,
|
202
|
+
"string" => 'This is a string',
|
203
|
+
"date" => Date.today
|
204
|
+
}
|
205
|
+
|
206
|
+
mock_data.to_resource.present.must_equal new_single_resource_standard
|
207
|
+
|
208
|
+
Api::Presenter::Resource.host = ''
|
209
|
+
end
|
210
|
+
end
|
148
211
|
|
149
212
|
describe "when presenting a single resource" do
|
150
|
-
|
151
|
-
let(:mock_data) { MockData.new(number: 1, string: 'This is a string', date: Date.today) }
|
152
|
-
let(:single_resource) { MockSingleResource.new mock_data }
|
153
|
-
let(:presented_single_resource) { single_resource.present }
|
213
|
+
let(:presented_single_resource) { mock_data.to_resource.present }
|
154
214
|
|
155
215
|
it "must respect the standard" do
|
156
216
|
presented_single_resource.must_equal single_resource_standard
|
157
217
|
end
|
158
218
|
end
|
159
219
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
end
|
220
|
+
describe "presenting a collection resource" do
|
221
|
+
|
222
|
+
let(:mock_data_collection) do
|
223
|
+
collection = []
|
224
|
+
4.times { |number| collection << MockData.new(number: number + 1, string: 'This is a string', date: Date.today) }
|
225
|
+
Collection.new(collection)
|
226
|
+
end
|
227
|
+
|
228
|
+
let(:collection_resource) { MockCollectionResource.new mock_data_collection }
|
229
|
+
|
230
|
+
let(:presented_collection_resource) { collection_resource.present }
|
231
|
+
|
232
|
+
it "must respect the standard" do
|
233
|
+
presented_collection_resource.must_equal collection_resource_standard
|
234
|
+
end
|
235
|
+
|
236
|
+
describe "presenting a search resource" do
|
237
|
+
|
238
|
+
let(:search_resource) { MockSearchResource.new mock_data_collection, "page" => 1, "param1" => 1, "param2" => "string" }
|
239
|
+
|
240
|
+
let(:presented_search_resource) { search_resource.present }
|
241
|
+
|
242
|
+
it "must respect the standard" do
|
243
|
+
presented_search_resource.must_equal search_resource_standard
|
244
|
+
end
|
186
245
|
end
|
246
|
+
end
|
187
247
|
end
|
data/spec/spec_helper.rb
CHANGED
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.1.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-06-
|
12
|
+
date: 2013-06-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -51,6 +51,7 @@ extensions: []
|
|
51
51
|
extra_rdoc_files: []
|
52
52
|
files:
|
53
53
|
- .gitignore
|
54
|
+
- .travis.yml
|
54
55
|
- Gemfile
|
55
56
|
- LICENSE.txt
|
56
57
|
- README.md
|