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 ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - jruby-19mode
6
+ - rbx-19mode
7
+ - jruby-head
data/Gemfile CHANGED
@@ -1,3 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ group :test do
6
+ gem 'rake'
7
+ gem 'minitest', require: ['minitest/autorun']
8
+ end
data/README.md CHANGED
@@ -54,15 +54,16 @@ class PersonResource < Api::Presenter::Resource
54
54
  property :name
55
55
  property :age
56
56
 
57
- def self_link
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 self_link definition tell which is the link that represents itself.
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
- def self_link
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
- def self_link
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, you need to define
329
- two methods in your resource class:
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
- def custom_link
333
- "/path/to/custom_link"
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
- def custom_link?(options = {})
337
- a_condition_that_determins_if_it_should_be_displayed returning true or false
338
- end
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
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
 
4
+ task :default => [:spec]
5
+
4
6
  Rake::TestTask.new('spec') do |t|
5
7
  t.test_files = FileList['spec/*_spec.rb']
6
8
  t.verbose = true
@@ -17,35 +17,44 @@ module Api
17
17
  def present_properties(resource, representation)
18
18
  resource_properties = resource.class.properties.dup
19
19
 
20
- # Special treatment for entries
21
- entries_property = resource_properties.delete(:entries)
22
-
23
- if entries_property
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
- property_value = property_value.to_resource if property_value.respond_to? :to_resource
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.links(options) }
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 links(options = {})
48
+ def build_links(options = {})
31
49
  links = {}
32
50
 
33
- self.methods.grep(/_link$/).each do |link_method|
34
- link_name = link_method.to_s.split("_").first
35
- links[link_name] = { "href" => self.send(link_method) } if self.send(link_method.to_s + "?", options)
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
@@ -1,5 +1,5 @@
1
1
  module Api
2
2
  module Presenter
3
- VERSION = "0.0.3.2"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  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
- def self_link
46
- "/path/to/single_resource/#{@resource.number}"
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
- def self_link
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
- def self_link
74
- "/path/to/search_resource#{query_string}"
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
- describe "presenting a collection resource" do
161
-
162
- let(:mock_data_collection) do
163
- collection = []
164
- 4.times { |number| collection << MockData.new(number: number + 1, string: 'This is a string', date: Date.today) }
165
- Collection.new(collection)
166
- end
167
-
168
- let(:collection_resource) { MockCollectionResource.new mock_data_collection }
169
-
170
- let(:presented_collection_resource) { collection_resource.present }
171
-
172
- it "must respect the standard" do
173
- presented_collection_resource.must_equal collection_resource_standard
174
- end
175
-
176
- describe "presenting a search resource" do
177
-
178
- let(:search_resource) { MockSearchResource.new mock_data_collection, "page" => 1, "param1" => 1, "param2" => "string" }
179
-
180
- let(:presented_search_resource) { search_resource.present }
181
-
182
- it "must respect the standard" do
183
- presented_search_resource.must_equal search_resource_standard
184
- end
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
@@ -1,6 +1,5 @@
1
1
  require 'bundler'
2
- require 'minitest/autorun'
3
2
  require 'date'
4
3
  require 'time'
5
4
 
6
- Bundler.require
5
+ Bundler.require :default, :test
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.3.2
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-10 00:00:00.000000000 Z
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