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 +4 -4
- data/aptible-resource.gemspec +2 -1
- data/lib/aptible/resource.rb +13 -0
- data/lib/aptible/resource/base.rb +23 -15
- data/lib/aptible/resource/default_retry_coordinator.rb +15 -0
- data/lib/aptible/resource/version.rb +1 -1
- data/lib/hyper_resource.rb +1 -0
- data/lib/hyper_resource/adapter/hal_json.rb +4 -0
- data/lib/hyper_resource/modules/http.rb +67 -57
- data/lib/hyper_resource/modules/internal_attributes.rb +1 -0
- data/lib/hyper_resource/version.rb +2 -2
- data/spec/aptible/resource/base_spec.rb +207 -13
- data/spec/aptible/resource/network_spec.rb +6 -6
- data/spec/fixtures/api.rb +3 -0
- data/spec/spec_helper.rb +6 -1
- metadata +22 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 077c3b938aed411e3c460ba3e0fe7f01ccf1cd52
|
4
|
+
data.tar.gz: 66a968ab57d1f73170cc960f9f547e9fd231dc2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39b3ecc5f467482ea95b45f6408bb2c0e10fb8a0050ad249118d1604c9819d0349ea09c4662e18731a9e3598c2e6f7cd177049a8e9191a2ddc5fc23bd499eb31
|
7
|
+
data.tar.gz: 8fc0837539473ea9c07e6892f0b5748270c93ecf805d1b2ee33e54bc2eaec6572df337f5a00a86e0031f822d49a4baf5db3cf714f7747259392f6ac0cd2965c7
|
data/aptible-resource.gemspec
CHANGED
@@ -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'
|
data/lib/aptible/resource.rb
CHANGED
@@ -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 :
|
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
|
-
|
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
|
-
|
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
|
-
|
242
|
-
|
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
|
-
|
252
|
+
raise 'Resource server namespace must be defined by subclass'
|
251
253
|
end
|
252
254
|
|
253
255
|
def root_url
|
254
|
-
|
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
|
data/lib/hyper_resource.rb
CHANGED
@@ -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
|
-
|
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
|
28
|
-
|
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 ||=
|
45
|
-
|
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 ||=
|
56
|
-
|
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
|
-
|
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).
|
70
|
-
def faraday_connection(url=nil)
|
71
|
-
url ||= URI.join(
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
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
|
-
|
107
|
+
body = adapter.deserialize(response.body) unless response.body.nil?
|
101
108
|
rescue StandardError => e
|
102
109
|
raise HyperResource::ResponseError.new(
|
103
|
-
|
104
|
-
:
|
105
|
-
:
|
110
|
+
'Error when deserializing response body',
|
111
|
+
response: response,
|
112
|
+
cause: e
|
106
113
|
)
|
107
114
|
end
|
108
115
|
|
109
|
-
|
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
|
-
|
119
|
+
raise 'HyperResource does not handle redirects'
|
117
120
|
elsif status / 100 == 4
|
118
121
|
raise HyperResource::ClientError.new(status.to_s,
|
119
|
-
:
|
120
|
-
:
|
122
|
+
response: response,
|
123
|
+
body: body)
|
121
124
|
elsif status / 100 == 5
|
122
125
|
raise HyperResource::ServerError.new(status.to_s,
|
123
|
-
:
|
124
|
-
:
|
126
|
+
response: response,
|
127
|
+
body: body)
|
125
128
|
|
126
129
|
else ## 1xx? really?
|
127
130
|
raise HyperResource::ResponseError.new("Got status #{status}, wtf?",
|
128
|
-
:
|
129
|
-
:
|
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
|
-
|
@@ -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
|
-
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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
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.
|
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.
|
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-
|
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.
|
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
|