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 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