aptible-resource 0.3.6 → 0.3.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 31f491541f4e8330accd97a53c1896fe7fca26e2
4
- data.tar.gz: fa2f72d22a0736be3d38d61d4731e7582ef64c78
3
+ metadata.gz: 077c3b938aed411e3c460ba3e0fe7f01ccf1cd52
4
+ data.tar.gz: 66a968ab57d1f73170cc960f9f547e9fd231dc2d
5
5
  SHA512:
6
- metadata.gz: ea9ed96d16623b924cc4bbd463e128bab5d8ce1463a10451faa3de64be1b079c88aec56efad7ad079b7fea3877cd906c9bae5b24b2048d08ad99bdfa5f23a4a0
7
- data.tar.gz: 406299c0d2a4cff3faca28f86c736374a27f38cde09ea6d050a127f74853e6d5916477d4bf03cc5feb228a4ee9cc7c8151151dbace303c8f44fab0a847c8333b
6
+ metadata.gz: 39b3ecc5f467482ea95b45f6408bb2c0e10fb8a0050ad249118d1604c9819d0349ea09c4662e18731a9e3598c2e6f7cd177049a8e9191a2ddc5fc23bd499eb31
7
+ data.tar.gz: 8fc0837539473ea9c07e6892f0b5748270c93ecf805d1b2ee33e54bc2eaec6572df337f5a00a86e0031f822d49a4baf5db3cf714f7747259392f6ac0cd2965c7
@@ -25,7 +25,8 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency 'json'
26
26
 
27
27
  spec.add_dependency 'fridge'
28
- spec.add_dependency 'activesupport'
28
+ spec.add_dependency 'activesupport', '~> 4.0'
29
+ spec.add_dependency 'gem_config', '~> 0.3.1'
29
30
 
30
31
  spec.add_development_dependency 'bundler', '~> 1.3'
31
32
  spec.add_development_dependency 'aptible-tasks'
@@ -1,7 +1,20 @@
1
1
  require 'aptible/resource/version'
2
2
  require 'aptible/resource/base'
3
+ require 'aptible/resource/default_retry_coordinator'
4
+ require 'gem_config'
3
5
 
4
6
  module Aptible
5
7
  module Resource
8
+ include GemConfig::Base
9
+
10
+ with_configuration do
11
+ has :retry_coordinator_class,
12
+ classes: [Class],
13
+ default: DefaultRetryCoordinator
14
+
15
+ has :user_agent,
16
+ classes: [String],
17
+ default: "aptible-resource #{Aptible::Resource::VERSION}"
18
+ end
6
19
  end
7
20
  end
@@ -19,7 +19,8 @@ module Aptible
19
19
  module Resource
20
20
  # rubocop:disable ClassLength
21
21
  class Base < HyperResource
22
- attr_accessor :token, :errors
22
+ attr_accessor :errors
23
+ attr_reader :token
23
24
 
24
25
  def self.get_data_type_from_response(response)
25
26
  return nil unless response && response.body
@@ -79,11 +80,8 @@ module Aptible
79
80
  # REVIEW: Should exception be raised if return type mismatch?
80
81
  new(options).find_by_url(url)
81
82
  rescue HyperResource::ClientError => e
82
- if e.response.status == 404
83
- return nil
84
- else
85
- raise e
86
- end
83
+ return nil if e.response.status == 404
84
+ raise e
87
85
  end
88
86
 
89
87
  def self.create!(params = {})
@@ -146,6 +144,7 @@ module Aptible
146
144
  memoized
147
145
  elsif links[relation]
148
146
  depaginated = self.class.all(href: links[relation].base_href,
147
+ token: token,
149
148
  headers: headers)
150
149
  instance_variable_set("@#{relation}", depaginated)
151
150
  end
@@ -154,6 +153,7 @@ module Aptible
154
153
  define_method "each_#{relation.to_s.singularize}" do |&block|
155
154
  return if block.nil?
156
155
  self.class.each_page(href: links[relation].base_href,
156
+ token: token,
157
157
  headers: headers) do |page|
158
158
  page.each { |entry| block.call entry }
159
159
  end
@@ -225,12 +225,11 @@ module Aptible
225
225
  end
226
226
 
227
227
  def initialize(options = {})
228
- if options.is_a?(Hash)
229
- self.token = options[:token]
230
- populate_default_options!(options)
231
- end
228
+ return super(options) unless options.is_a?(Hash)
232
229
 
230
+ populate_default_options!(options)
233
231
  super(options)
232
+ self.token = options[:token] if options[:token]
234
233
  end
235
234
 
236
235
  def populate_default_options!(options)
@@ -238,8 +237,11 @@ module Aptible
238
237
  options[:namespace] ||= namespace
239
238
  options[:headers] ||= {}
240
239
  options[:headers]['Content-Type'] = 'application/json'
241
- return unless options[:token]
242
- options[:headers]['Authorization'] = "Bearer #{bearer_token}"
240
+ end
241
+
242
+ def token=(val)
243
+ @token = val
244
+ headers['Authorization'] = "Bearer #{bearer_token}"
243
245
  end
244
246
 
245
247
  def adapter
@@ -247,11 +249,11 @@ module Aptible
247
249
  end
248
250
 
249
251
  def namespace
250
- fail 'Resource server namespace must be defined by subclass'
252
+ raise 'Resource server namespace must be defined by subclass'
251
253
  end
252
254
 
253
255
  def root_url
254
- fail 'Resource server root URL must be defined by subclass'
256
+ raise 'Resource server root URL must be defined by subclass'
255
257
  end
256
258
 
257
259
  def find_by_url(url_or_href)
@@ -268,7 +270,10 @@ module Aptible
268
270
  end
269
271
  end
270
272
 
273
+ # rubocop:disable Style/Alias
271
274
  alias_method :_hyperresource_update, :update
275
+ # rubocop:enable Style/Alias
276
+
272
277
  def update!(params)
273
278
  _hyperresource_update(self.class.normalize_params(params))
274
279
  rescue HyperResource::ResponseError => e
@@ -288,11 +293,14 @@ module Aptible
288
293
  # HyperResource/Faraday choke on empty response bodies
289
294
  nil
290
295
  end
296
+
297
+ # rubocop:disable Style/Alias
291
298
  alias_method :destroy, :delete
299
+ # rubocop:enable Style/Alias
292
300
 
293
301
  # NOTE: The following does not update the object in-place
294
302
  def reload
295
- self.class.find_by_url(href, headers: headers)
303
+ self.class.find_by_url(href, token: token, headers: headers)
296
304
  end
297
305
 
298
306
  def errors
@@ -0,0 +1,15 @@
1
+ module Aptible
2
+ module Resource
3
+ class DefaultRetryCoordinator
4
+ attr_reader :resource
5
+
6
+ def initialize(resource)
7
+ @resource = resource
8
+ end
9
+
10
+ def retry?(_error)
11
+ false
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module Resource
3
- VERSION = '0.3.6'
3
+ VERSION = '0.3.7'.freeze
4
4
  end
5
5
  end
@@ -199,6 +199,7 @@ public
199
199
  :headers => self.headers,
200
200
  :namespace => self.namespace,
201
201
  :faraday_options => self.faraday_options,
202
+ :token => self.token,
202
203
  :href => href)
203
204
  end
204
205
 
@@ -48,6 +48,7 @@ class HyperResource
48
48
  if collection.is_a? Hash
49
49
  r = rc.new(:root => rsrc.root,
50
50
  :headers => rsrc.headers,
51
+ :token => rsrc.token,
51
52
  :namespace => rsrc.namespace)
52
53
  r.body = collection
53
54
  r = classify(collection, r)
@@ -56,6 +57,7 @@ class HyperResource
56
57
  objs[name] = collection.map do |obj|
57
58
  r = rc.new(:root => rsrc.root,
58
59
  :headers => rsrc.headers,
60
+ :token => rsrc.token,
59
61
  :namespace => rsrc.namespace)
60
62
  r.body = obj
61
63
  r = classify(obj, r)
@@ -73,8 +75,10 @@ class HyperResource
73
75
  klass = rsrc.class.namespaced_class(type_name, namespace)
74
76
 
75
77
  if klass
78
+ # TODO: Why does this not use klass.new(rsrc)?
76
79
  rsrc = klass.new(:root => rsrc.root,
77
80
  :headers => rsrc.headers,
81
+ :token => rsrc.token,
78
82
  :namespace => rsrc.namespace)
79
83
  rsrc.body = resp
80
84
  end
@@ -6,13 +6,16 @@ require 'digest/md5'
6
6
  class HyperResource
7
7
  module Modules
8
8
  module HTTP
9
+ # A (high) limit to the number of retries a coordinator can ask for. This
10
+ # is to avoid breaking things if we have a buggy coordinator that retries
11
+ # things over and over again.
12
+ MAX_COORDINATOR_RETRIES = 16
9
13
 
10
14
  ## Loads and returns the resource pointed to by +href+. The returned
11
15
  ## resource will be blessed into its "proper" class, if
12
16
  ## +self.class.namespace != nil+.
13
17
  def get
14
- self.response = faraday_connection.get(self.href || '')
15
- finish_up
18
+ execute_request { faraday_connection.get(href || '') }
16
19
  end
17
20
 
18
21
  ## By default, calls +post+ with the given arguments. Override to
@@ -23,12 +26,11 @@ class HyperResource
23
26
 
24
27
  ## POSTs the given attributes to this resource's href, and returns
25
28
  ## the response resource.
26
- def post(attrs=nil)
27
- attrs || self.attributes
28
- self.response = faraday_connection.post do |req|
29
- req.body = adapter.serialize(attrs)
29
+ def post(attrs = nil)
30
+ attrs ||= attributes
31
+ execute_request do
32
+ faraday_connection.post { |req| req.body = adapter.serialize(attrs) }
30
33
  end
31
- finish_up
32
34
  end
33
35
 
34
36
  ## By default, calls +put+ with the given arguments. Override to
@@ -40,48 +42,39 @@ class HyperResource
40
42
  ## PUTs this resource's attributes to this resource's href, and returns
41
43
  ## the response resource. If attributes are given, +put+ uses those
42
44
  ## instead.
43
- def put(attrs=nil)
44
- attrs ||= self.attributes
45
- self.response = faraday_connection.put do |req|
46
- req.body = adapter.serialize(attrs)
45
+ def put(attrs = nil)
46
+ attrs ||= attributes
47
+ execute_request do
48
+ faraday_connection.put { |req| req.body = adapter.serialize(attrs) }
47
49
  end
48
- finish_up
49
50
  end
50
51
 
51
52
  ## PATCHes this resource's changed attributes to this resource's href,
52
53
  ## and returns the response resource. If attributes are given, +patch+
53
54
  ## uses those instead.
54
- def patch(attrs=nil)
55
- attrs ||= self.attributes.changed_attributes
56
- self.response = faraday_connection.patch do |req|
57
- req.body = adapter.serialize(attrs)
55
+ def patch(attrs = nil)
56
+ attrs ||= attributes.changed_attributes
57
+ execute_request do
58
+ faraday_connection.patch { |req| req.body = adapter.serialize(attrs) }
58
59
  end
59
- finish_up
60
60
  end
61
61
 
62
62
  ## DELETEs this resource's href, and returns the response resource.
63
63
  def delete
64
- self.response = faraday_connection.delete
65
- finish_up
64
+ execute_request { faraday_connection.delete }
66
65
  end
67
66
 
68
67
  ## Returns a raw Faraday connection to this resource's URL, with proper
69
- ## headers (including auth). Threadsafe.
70
- def faraday_connection(url=nil)
71
- url ||= URI.join(self.root, self.href)
72
- key = Digest::MD5.hexdigest({
73
- 'faraday_connection' => {
74
- 'url' => url,
75
- 'headers' => self.headers,
76
- 'ba' => self.auth[:basic]
77
- }
78
- }.to_json)
79
- return Thread.current[key] if Thread.current[key]
80
-
81
- fc = Faraday.new(self.faraday_options.merge(:url => url)) do |builder|
82
- builder.headers.merge!('User-Agent' => "HyperResource #{HyperResource::VERSION}")
83
- builder.headers.merge!(self.headers || {})
84
- if (ba = self.auth[:basic])
68
+ ## headers (including auth).
69
+ def faraday_connection(url = nil)
70
+ url ||= URI.join(root, href)
71
+
72
+ Faraday.new(faraday_options.merge(url: url)) do |builder|
73
+ builder.headers.merge!(headers || {})
74
+ builder.headers['User-Agent'] = Aptible::Resource.configuration
75
+ .user_agent
76
+
77
+ if (ba = auth[:basic])
85
78
  builder.basic_auth(*ba)
86
79
  end
87
80
 
@@ -89,49 +82,66 @@ class HyperResource
89
82
  builder.request :retry
90
83
  builder.adapter Faraday.default_adapter
91
84
  end
92
-
93
- Thread.current[key] = fc
94
85
  end
95
86
 
96
- private
87
+ private
88
+
89
+ def execute_request
90
+ raise 'execute_request needs a block!' unless block_given?
91
+ retry_coordinator = Aptible::Resource.configuration
92
+ .retry_coordinator_class.new(self)
93
+
94
+ n_retry = 0
95
+ begin
96
+ finish_up(yield)
97
+ rescue HyperResource::ResponseError => e
98
+ n_retry += 1
99
+ raise e if n_retry > MAX_COORDINATOR_RETRIES
100
+ retry if retry_coordinator.retry?(e)
101
+ raise e
102
+ end
103
+ end
97
104
 
98
- def finish_up
105
+ def finish_up(response)
99
106
  begin
100
- self.body = self.adapter.deserialize(self.response.body) unless self.response.body.nil?
107
+ body = adapter.deserialize(response.body) unless response.body.nil?
101
108
  rescue StandardError => e
102
109
  raise HyperResource::ResponseError.new(
103
- "Error when deserializing response body",
104
- :response => self.response,
105
- :cause => e
110
+ 'Error when deserializing response body',
111
+ response: response,
112
+ cause: e
106
113
  )
107
114
  end
108
115
 
109
- self.adapter.apply(self.body, self)
110
- self.loaded = true
111
-
112
- status = self.response.status
116
+ status = response.status
113
117
  if status / 100 == 2
114
- return to_response_class
115
118
  elsif status / 100 == 3
116
- ## TODO redirect logic?
119
+ raise 'HyperResource does not handle redirects'
117
120
  elsif status / 100 == 4
118
121
  raise HyperResource::ClientError.new(status.to_s,
119
- :response => self.response,
120
- :body => self.body)
122
+ response: response,
123
+ body: body)
121
124
  elsif status / 100 == 5
122
125
  raise HyperResource::ServerError.new(status.to_s,
123
- :response => self.response,
124
- :body => self.body)
126
+ response: response,
127
+ body: body)
125
128
 
126
129
  else ## 1xx? really?
127
130
  raise HyperResource::ResponseError.new("Got status #{status}, wtf?",
128
- :response => self.response,
129
- :body => self.body)
131
+ response: response,
132
+ body: body)
130
133
 
131
134
  end
132
- end
133
135
 
136
+ # Unfortunately, HyperResource insists on having response and body
137
+ # be attributes..
138
+ self.response = response
139
+ self.body = body
140
+ adapter.apply(body, self)
141
+ self.loaded = true
142
+
143
+ to_response_class
144
+ end
134
145
  end
135
146
  end
136
147
  end
137
-
@@ -38,6 +38,7 @@ module HyperResource::Modules
38
38
  :namespace,
39
39
  :adapter,
40
40
  :faraday_options,
41
+ :token,
41
42
 
42
43
  :request,
43
44
  :response,
@@ -1,4 +1,4 @@
1
1
  class HyperResource
2
- VERSION = '0.2.5'
3
- VERSION_DATE = '2014-04-02'
2
+ VERSION = '0.2.5'.freeze
3
+ VERSION_DATE = '2014-04-02'.freeze
4
4
  end
@@ -35,7 +35,7 @@ describe Aptible::Resource::Base do
35
35
  allow_any_instance_of(klass).to receive(:find_by_url) do |u, _|
36
36
  calls << u
37
37
  page = pages[u]
38
- fail "Accessed unexpected URL #{u}" if page.nil?
38
+ raise "Accessed unexpected URL #{u}" if page.nil?
39
39
  page
40
40
  end
41
41
  end
@@ -146,14 +146,14 @@ describe Aptible::Resource::Base do
146
146
  end
147
147
 
148
148
  it 'should populate #errors in the event of an error' do
149
- mainframes_link.stub(:create) { fail hyperresource_exception }
149
+ mainframes_link.stub(:create) { raise hyperresource_exception }
150
150
  mainframe = Api::Mainframe.create
151
151
  expect(mainframe.errors.messages).to eq(base: 'Forbidden')
152
152
  expect(mainframe.errors.full_messages).to eq(['Forbidden'])
153
153
  end
154
154
 
155
155
  it 'should return a Base-classed resource on error' do
156
- mainframes_link.stub(:create) { fail hyperresource_exception }
156
+ mainframes_link.stub(:create) { raise hyperresource_exception }
157
157
  expect(Api::Mainframe.create).to be_a Api::Mainframe
158
158
  end
159
159
 
@@ -171,7 +171,7 @@ describe Aptible::Resource::Base do
171
171
  before { mainframes_link.stub(:create) { mainframe } }
172
172
 
173
173
  it 'should pass through any exceptions' do
174
- mainframes_link.stub(:create) { fail hyperresource_exception }
174
+ mainframes_link.stub(:create) { raise hyperresource_exception }
175
175
  expect do
176
176
  Api::Mainframe.create!
177
177
  end.to raise_error HyperResource::ResponseError
@@ -224,14 +224,14 @@ describe Aptible::Resource::Base do
224
224
 
225
225
  describe '#update' do
226
226
  it 'should populate #errors in the event of an error' do
227
- HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
227
+ HyperResource.any_instance.stub(:put) { raise hyperresource_exception }
228
228
  subject.update({})
229
229
  expect(subject.errors.messages).to eq(base: 'Forbidden')
230
230
  expect(subject.errors.full_messages).to eq(['Forbidden'])
231
231
  end
232
232
 
233
233
  it 'should return false in the event of an error' do
234
- HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
234
+ HyperResource.any_instance.stub(:put) { raise hyperresource_exception }
235
235
  expect(subject.update({})).to eq false
236
236
  end
237
237
 
@@ -243,7 +243,7 @@ describe Aptible::Resource::Base do
243
243
 
244
244
  describe '#update!' do
245
245
  it 'should populate #errors in the event of an error' do
246
- HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
246
+ HyperResource.any_instance.stub(:put) { raise hyperresource_exception }
247
247
  begin
248
248
  subject.update!({})
249
249
  rescue
@@ -255,7 +255,7 @@ describe Aptible::Resource::Base do
255
255
  end
256
256
 
257
257
  it 'should pass through any exceptions' do
258
- HyperResource.any_instance.stub(:put) { fail hyperresource_exception }
258
+ HyperResource.any_instance.stub(:put) { raise hyperresource_exception }
259
259
  expect do
260
260
  subject.update!({})
261
261
  end.to raise_error HyperResource::ResponseError
@@ -271,7 +271,6 @@ describe Aptible::Resource::Base do
271
271
  let(:mainframe) { Api::Mainframe.new }
272
272
  let(:mainframes_link) { HyperResource::Link.new(href: '/mainframes') }
273
273
 
274
- before { Api.has_many :mainframes }
275
274
  before { subject.stub(:loaded) { true } }
276
275
  before { subject.stub(:links) { { mainframes: mainframes_link } } }
277
276
  before { mainframes_link.stub(:entries) { [mainframe] } }
@@ -279,19 +278,19 @@ describe Aptible::Resource::Base do
279
278
 
280
279
  describe '#create_#{relation}' do
281
280
  it 'should populate #errors in the event of an error' do
282
- mainframes_link.stub(:create) { fail hyperresource_exception }
281
+ mainframes_link.stub(:create) { raise hyperresource_exception }
283
282
  mainframe = subject.create_mainframe({})
284
283
  expect(mainframe.errors.messages).to eq(base: 'Forbidden')
285
284
  expect(mainframe.errors.full_messages).to eq(['Forbidden'])
286
285
  end
287
286
 
288
287
  it 'should return a Base-classed resource on error' do
289
- mainframes_link.stub(:create) { fail hyperresource_exception }
288
+ mainframes_link.stub(:create) { raise hyperresource_exception }
290
289
  expect(subject.create_mainframe.class).to eq Aptible::Resource::Base
291
290
  end
292
291
 
293
292
  it 'should have errors present on error' do
294
- mainframes_link.stub(:create) { fail hyperresource_exception }
293
+ mainframes_link.stub(:create) { raise hyperresource_exception }
295
294
  expect(subject.create_mainframe.errors.any?).to be true
296
295
  end
297
296
 
@@ -308,7 +307,7 @@ describe Aptible::Resource::Base do
308
307
 
309
308
  describe '#create_#{relation}!' do
310
309
  it 'should pass through any exceptions' do
311
- mainframes_link.stub(:create) { fail hyperresource_exception }
310
+ mainframes_link.stub(:create) { raise hyperresource_exception }
312
311
  expect do
313
312
  subject.create_mainframe!({})
314
313
  end.to raise_error HyperResource::ResponseError
@@ -387,4 +386,199 @@ describe Aptible::Resource::Base do
387
386
  expect(subject.awesome?).to be true
388
387
  end
389
388
  end
389
+
390
+ context 'configuration' do
391
+ subject { Api.new(root: 'http://foo.com') }
392
+
393
+ def configure_new_coordinator(&block)
394
+ Aptible::Resource.configure do |config|
395
+ config.retry_coordinator_class = \
396
+ Class.new(Aptible::Resource::DefaultRetryCoordinator) do
397
+ instance_exec(&block)
398
+ end
399
+ end
400
+ end
401
+
402
+ context 'retry_coordinator_class' do
403
+ it 'should not retry if the proc returns false' do
404
+ configure_new_coordinator { define_method(:retry?) { |_e| false } }
405
+
406
+ stub_request(:get, 'foo.com')
407
+ .to_return(body: { error: 'foo' }.to_json, status: 401).then
408
+ .to_return(body: { status: 'ok' }.to_json, status: 200)
409
+
410
+ expect { subject.get.body }
411
+ .to raise_error(HyperResource::ClientError, /foo/)
412
+ end
413
+
414
+ it 'should retry if the proc returns true' do
415
+ configure_new_coordinator { define_method(:retry?) { |_e| true } }
416
+
417
+ stub_request(:get, 'foo.com')
418
+ .to_return(body: { error: 'foo' }.to_json, status: 401).then
419
+ .to_return(body: { error: 'foo' }.to_json, status: 401).then
420
+ .to_return(body: { status: 'ok' }.to_json, status: 200)
421
+
422
+ expect(subject.get.body).to eq('status' => 'ok')
423
+ end
424
+
425
+ it 'should not retry if the request succeeds' do
426
+ failures = 0
427
+
428
+ configure_new_coordinator do
429
+ define_method(:retry?) { |_e| failures += 1 || true }
430
+ end
431
+
432
+ stub_request(:get, 'foo.com')
433
+ .to_return(body: { error: 'foo' }.to_json, status: 401).then
434
+ .to_return(body: { status: 'ok' }.to_json, status: 200).then
435
+ .to_return(body: { error: 'foo' }.to_json, status: 401)
436
+
437
+ expect(subject.get.body).to eq('status' => 'ok')
438
+
439
+ expect(failures).to eq(1)
440
+ end
441
+
442
+ it 'should not retry with the default proc' do
443
+ stub_request(:get, 'foo.com')
444
+ .to_return(body: { error: 'foo' }.to_json, status: 401).then
445
+ .to_return(body: { status: 'ok' }.to_json, status: 200)
446
+
447
+ expect { subject.get.body }
448
+ .to raise_error(HyperResource::ClientError, /foo/)
449
+ end
450
+
451
+ it 'should pass the resource in constructor and exception in method' do
452
+ resource = nil
453
+ exception = nil
454
+
455
+ configure_new_coordinator do
456
+ define_method(:initialize) { |r| resource = r }
457
+ define_method(:retry?) { |e| (exception = e) && false }
458
+ end
459
+
460
+ stub_request(:get, 'foo.com')
461
+ .to_return(body: { error: 'foo' }.to_json, status: 401)
462
+
463
+ expect { subject.get.body }
464
+ .to raise_error(HyperResource::ClientError, /foo/)
465
+
466
+ expect(resource).to be_a(Api)
467
+ expect(exception).to be_a(HyperResource::ClientError)
468
+ end
469
+
470
+ it 'should let the coordinator change e.g. the request token' do
471
+ subject.token = 'foo'
472
+ retry_was_called = false
473
+
474
+ configure_new_coordinator do
475
+ define_method(:retry?) do |_e|
476
+ resource.token = 'bar'
477
+ # resource.headers['Authorization'] = 'Bearer bar'
478
+ retry_was_called = true
479
+ end
480
+ end
481
+
482
+ stub_request(:get, 'foo.com')
483
+ .with(headers: { 'Authorization' => /foo/ })
484
+ .to_return(body: { error: 'foo' }.to_json, status: 401)
485
+
486
+ stub_request(:get, 'foo.com')
487
+ .with(headers: { 'Authorization' => /bar/ })
488
+ .to_return(body: { status: 'ok' }.to_json, status: 200)
489
+
490
+ expect(subject.get.body).to eq('status' => 'ok')
491
+ expect(retry_was_called).to be_truthy
492
+ end
493
+
494
+ it 'should eventually fail even if the coordinator wants to retry' do
495
+ n = 0
496
+
497
+ configure_new_coordinator do
498
+ define_method(:retry?) { |_e| n += 1 || true }
499
+ end
500
+
501
+ stub_request(:get, 'foo.com')
502
+ .to_return(body: { error: 'foo' }.to_json, status: 401)
503
+
504
+ expect { subject.get.body }
505
+ .to raise_error(HyperResource::ClientError, /foo/)
506
+
507
+ expect(n).to eq(HyperResource::Modules::HTTP::MAX_COORDINATOR_RETRIES)
508
+ end
509
+ end
510
+
511
+ context 'user_agent' do
512
+ it 'should update the user agent' do
513
+ Aptible::Resource.configure do |config|
514
+ config.user_agent = 'foo ua'
515
+ end
516
+
517
+ stub_request(:get, 'foo.com')
518
+ .with(headers: { 'User-Agent' => 'foo ua' })
519
+ .to_return(body: { status: 'ok' }.to_json, status: 200)
520
+
521
+ expect(subject.get.body).to eq('status' => 'ok')
522
+ end
523
+ end
524
+ end
525
+
526
+ context 'token' do
527
+ subject { Api.new(root: 'http://foo.com', token: 'bar') }
528
+
529
+ before do
530
+ stub_request(:get, 'foo.com/')
531
+ .with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
532
+ .to_return(body: {
533
+ _links: { some: { href: 'http://foo.com/some' },
534
+ mainframes: { href: 'http://foo.com/mainframes' } },
535
+ _embedded: { best_mainframe: { _type: 'mainframe', status: 'ok' } }
536
+ }.to_json, status: 200)
537
+
538
+ stub_request(:get, 'foo.com/some')
539
+ .with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
540
+ .to_return(body: { status: 'ok' }.to_json, status: 200)
541
+
542
+ stub_request(:get, 'foo.com/mainframes')
543
+ .with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
544
+ .to_return(body: { _embedded: {
545
+ mainframes: [{ status: 'ok' }]
546
+ } }.to_json, status: 200)
547
+ end
548
+
549
+ it 'should persist the Authorization header when following links' do
550
+ expect(subject.some.get.body).to eq('status' => 'ok')
551
+ end
552
+
553
+ it 'should persist the token when following links' do
554
+ expect(subject.some.token).to eq('bar')
555
+ expect(subject.some.headers['Authorization']).to eq('Bearer bar')
556
+ end
557
+
558
+ it 'should set the Authorization header when setting the token' do
559
+ subject.token = 'foo'
560
+
561
+ expect(subject.some.token).to eq('foo')
562
+ expect(subject.some.headers['Authorization']).to eq('Bearer foo')
563
+ end
564
+
565
+ it 'should persist the token and set the Authorization header when ' \
566
+ 'initializing from resource' do
567
+ subject.get
568
+ r = Api.new(subject)
569
+
570
+ expect(r.token).to eq('bar')
571
+ expect(r.headers['Authorization']).to eq('Bearer bar')
572
+ end
573
+
574
+ it 'should persist the token when accessing a related collection' do
575
+ m = subject.mainframes.first
576
+ expect(m.token).to eq('bar')
577
+ end
578
+
579
+ it 'should persist the token when accessing a named embedded object' do
580
+ m = subject.best_mainframe
581
+ expect(m.token).to eq('bar')
582
+ end
583
+ end
390
584
  end
@@ -9,12 +9,6 @@ describe Aptible::Resource::Base do
9
9
  subject { Api.new(root: "http://#{domain}") }
10
10
 
11
11
  context 'with mock connections' do
12
- around do |example|
13
- WebMock.disable_net_connect!
14
- example.run
15
- WebMock.allow_net_connect!
16
- end
17
-
18
12
  it 'should retry timeout errors' do
19
13
  stub_request(:get, domain)
20
14
  .to_timeout.then
@@ -34,6 +28,12 @@ describe Aptible::Resource::Base do
34
28
  end
35
29
 
36
30
  context 'without connections' do
31
+ around do |example|
32
+ WebMock.allow_net_connect!
33
+ example.run
34
+ WebMock.disable_net_connect!
35
+ end
36
+
37
37
  it 'default to 10 seconds of timeout and retry 3 times' do
38
38
  # This really relies on how exactly MRI implements Net::HTTP open timeouts
39
39
  skip 'MRI implementation-specific' if RUBY_PLATFORM == 'java'
data/spec/fixtures/api.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'aptible/resource'
2
2
 
3
3
  class Api < Aptible::Resource::Base
4
+ has_many :mainframes
5
+ embeds_one :best_mainframe
6
+
4
7
  def namespace
5
8
  'Api'
6
9
  end
data/spec/spec_helper.rb CHANGED
@@ -15,4 +15,9 @@ require 'aptible/resource'
15
15
 
16
16
  # Webmock
17
17
  require 'webmock/rspec'
18
- WebMock.allow_net_connect!
18
+ WebMock.disable_net_connect!
19
+
20
+ RSpec.configure do |config|
21
+ config.before { Aptible::Resource.configuration.reset }
22
+ config.before { WebMock.reset! }
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-16 00:00:00.000000000 Z
11
+ date: 2016-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uri_template
@@ -70,16 +70,30 @@ dependencies:
70
70
  name: activesupport
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '4.0'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '4.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: gem_config
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.3.1
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.3.1
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: bundler
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +198,7 @@ files:
184
198
  - lib/aptible/resource/adapter.rb
185
199
  - lib/aptible/resource/base.rb
186
200
  - lib/aptible/resource/boolean.rb
201
+ - lib/aptible/resource/default_retry_coordinator.rb
187
202
  - lib/aptible/resource/errors.rb
188
203
  - lib/aptible/resource/ext/faraday.rb
189
204
  - lib/aptible/resource/version.rb
@@ -225,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
225
240
  version: '0'
226
241
  requirements: []
227
242
  rubyforge_project:
228
- rubygems_version: 2.6.4
243
+ rubygems_version: 2.4.5.1
229
244
  signing_key:
230
245
  specification_version: 4
231
246
  summary: Foundation classes for Aptible resource server gems