api-presenter 0.0.3.2 → 0.1.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.
- 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
|