aptible-resource 1.0.2 → 1.1.0.pre.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.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/aptible-resource.gemspec +1 -2
- data/lib/aptible/resource.rb +5 -0
- data/lib/aptible/resource/base.rb +1 -11
- data/lib/aptible/resource/default_retry_coordinator.rb +19 -3
- data/lib/aptible/resource/version.rb +1 -1
- data/lib/hyper_resource.rb +2 -9
- data/lib/hyper_resource/exceptions.rb +1 -1
- data/lib/hyper_resource/link.rb +2 -2
- data/lib/hyper_resource/modules/http.rb +101 -42
- data/lib/hyper_resource/modules/http/wrap_errors.rb +1 -7
- data/lib/hyper_resource/modules/internal_attributes.rb +0 -2
- data/spec/aptible/resource/base_spec.rb +120 -24
- data/spec/aptible/resource/retry_spec.rb +2 -32
- data/spec/fixtures/api.rb +1 -0
- data/spec/spec_helper.rb +2 -0
- metadata +13 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a73b16e30cf7b0bb43875b1836684ef93844e8b6
|
4
|
+
data.tar.gz: 8f1ab370731c0eab7af7ad16e672186f9a21e265
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebfa485b577102776eee1481fbf4da3e0cb220dbb36e95a5af39148af7b72150645d6b0ffcdc2125d832fa1ae6613d06edfc9f46c70970ccd34fd8ceee5071ac
|
7
|
+
data.tar.gz: dde6aa360f1567af93327340ddd05493cdfbb9d19953692f72dff5fea8bed638c3a06d44061885ddb0f63108b56b90c21b072fb497d55d710b10b8dade111462
|
data/.travis.yml
CHANGED
data/aptible-resource.gemspec
CHANGED
@@ -21,9 +21,8 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
# HyperResource dependencies
|
23
23
|
spec.add_dependency 'uri_template', '>= 0.5.2'
|
24
|
-
spec.add_dependency 'faraday', '>= 0.9.2', '< 0.14'
|
25
24
|
spec.add_dependency 'json'
|
26
|
-
|
25
|
+
spec.add_dependency 'httpclient', '~> 2.8'
|
27
26
|
spec.add_dependency 'fridge'
|
28
27
|
spec.add_dependency 'activesupport', '>= 4.0', '< 6.0'
|
29
28
|
spec.add_dependency 'gem_config', '~> 0.3.1'
|
data/lib/aptible/resource.rb
CHANGED
@@ -3,6 +3,7 @@ require 'aptible/resource/base'
|
|
3
3
|
require 'aptible/resource/default_retry_coordinator'
|
4
4
|
require 'aptible/resource/null_retry_coordinator'
|
5
5
|
require 'gem_config'
|
6
|
+
require 'logger'
|
6
7
|
|
7
8
|
module Aptible
|
8
9
|
module Resource
|
@@ -18,6 +19,10 @@ module Aptible
|
|
18
19
|
has :user_agent,
|
19
20
|
classes: [String],
|
20
21
|
default: "aptible-resource #{Aptible::Resource::VERSION}"
|
22
|
+
|
23
|
+
has :logger,
|
24
|
+
classes: [Logger],
|
25
|
+
default: Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
|
21
26
|
end
|
22
27
|
|
23
28
|
class << self
|
@@ -224,16 +224,6 @@ module Aptible
|
|
224
224
|
end
|
225
225
|
end
|
226
226
|
|
227
|
-
def self.faraday_options
|
228
|
-
# Default Faraday options. May be overridden by passing
|
229
|
-
# faraday_options to the initializer.
|
230
|
-
{
|
231
|
-
request: {
|
232
|
-
open_timeout: 10
|
233
|
-
}
|
234
|
-
}
|
235
|
-
end
|
236
|
-
|
237
227
|
def initialize(options = {})
|
238
228
|
return super(options) unless options.is_a?(Hash)
|
239
229
|
|
@@ -305,7 +295,7 @@ module Aptible
|
|
305
295
|
# Already deleted
|
306
296
|
raise unless e.response.status == 404
|
307
297
|
rescue HyperResource::ResponseError
|
308
|
-
# HyperResource
|
298
|
+
# HyperResource chokes on empty response bodies
|
309
299
|
nil
|
310
300
|
end
|
311
301
|
|
@@ -5,11 +5,27 @@ module Aptible
|
|
5
5
|
|
6
6
|
IDEMPOTENT_METHODS = [
|
7
7
|
# Idempotent as per RFC
|
8
|
-
|
8
|
+
'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT',
|
9
|
+
|
9
10
|
# Idempotent on our APIs
|
10
|
-
|
11
|
+
'PATCH'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
RETRY_ERRORS = [
|
15
|
+
# Ancestor for Errno::X
|
16
|
+
SystemCallError,
|
17
|
+
|
18
|
+
# Might be caused by e.g. DNS failure
|
19
|
+
SocketError,
|
20
|
+
|
21
|
+
# HTTPClient transfer error
|
22
|
+
HTTPClient::TimeoutError,
|
23
|
+
HTTPClient::KeepAliveDisconnected,
|
24
|
+
HTTPClient::BadResponseError,
|
25
|
+
|
26
|
+
# Bad response
|
27
|
+
HyperResource::ServerError
|
11
28
|
].freeze
|
12
|
-
RETRY_ERRORS = [Faraday::Error, HyperResource::ServerError].freeze
|
13
29
|
|
14
30
|
def initialize(resource)
|
15
31
|
@resource = resource
|
data/lib/hyper_resource.rb
CHANGED
@@ -46,9 +46,6 @@ public
|
|
46
46
|
## [headers] Headers to send along with requests for this resource (as
|
47
47
|
## well as its eventual child resources, if any).
|
48
48
|
##
|
49
|
-
## [faraday_options] Configuration passed to +Faraday::Connection.initialize+,
|
50
|
-
## such as +{request: {timeout: 30}}+.
|
51
|
-
##
|
52
49
|
def initialize(opts={})
|
53
50
|
return init_from_resource(opts) if opts.kind_of?(HyperResource)
|
54
51
|
|
@@ -58,8 +55,6 @@ public
|
|
58
55
|
self.namespace = opts[:namespace] || self.class.namespace
|
59
56
|
self.headers = DEFAULT_HEADERS.merge(self.class.headers || {}).
|
60
57
|
merge(opts[:headers] || {})
|
61
|
-
self.faraday_options = opts[:faraday_options] ||
|
62
|
-
self.class.faraday_options || {}
|
63
58
|
|
64
59
|
## There's a little acrobatics in getting Attributes, Links, and Objects
|
65
60
|
## into the correct subclass.
|
@@ -130,14 +125,14 @@ public
|
|
130
125
|
## in this resource. Returns nil on failure.
|
131
126
|
def [](i)
|
132
127
|
get unless loaded
|
133
|
-
self.objects.first[1][i]
|
128
|
+
self.objects.first[1][i]
|
134
129
|
end
|
135
130
|
|
136
131
|
## Iterates over the objects in the first collection of embedded objects
|
137
132
|
## in this resource.
|
138
133
|
def each(&block)
|
139
134
|
get unless loaded
|
140
|
-
self.objects.first[1].each(&block)
|
135
|
+
self.objects.first[1].each(&block)
|
141
136
|
end
|
142
137
|
|
143
138
|
#### Magic
|
@@ -199,7 +194,6 @@ public
|
|
199
194
|
:auth => self.auth,
|
200
195
|
:headers => self.headers,
|
201
196
|
:namespace => self.namespace,
|
202
|
-
:faraday_options => self.faraday_options,
|
203
197
|
:token => self.token,
|
204
198
|
:href => href)
|
205
199
|
end
|
@@ -253,7 +247,6 @@ public
|
|
253
247
|
self.class.response_class(self.response, self.namespace)
|
254
248
|
end
|
255
249
|
|
256
|
-
|
257
250
|
## Inspects the given Faraday::Response, and returns a string describing
|
258
251
|
## this resource's data type.
|
259
252
|
##
|
@@ -10,7 +10,7 @@ class HyperResource
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class ResponseError < Exception
|
13
|
-
## The +
|
13
|
+
## The +HTTPClient::Message+ object which led to this exception.
|
14
14
|
attr_accessor :response
|
15
15
|
|
16
16
|
## The deserialized response body which led to this exception.
|
data/lib/hyper_resource/link.rb
CHANGED
@@ -53,7 +53,7 @@ class HyperResource::Link
|
|
53
53
|
|
54
54
|
## If we were called with a method we don't know, load this resource
|
55
55
|
## and pass the message along. This achieves implicit loading.
|
56
|
-
def method_missing(method, *args)
|
57
|
-
self.get.send(method, *args)
|
56
|
+
def method_missing(method, *args, &block)
|
57
|
+
self.get.send(method, *args, &block)
|
58
58
|
end
|
59
59
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'httpclient'
|
2
2
|
require 'uri'
|
3
3
|
require 'json'
|
4
4
|
require 'digest/md5'
|
@@ -11,11 +11,42 @@ class HyperResource
|
|
11
11
|
# things over and over again.
|
12
12
|
MAX_COORDINATOR_RETRIES = 16
|
13
13
|
|
14
|
+
CONTENT_TYPE_HEADERS = {
|
15
|
+
'Content-Type' => 'application/json; charset=utf-8'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_reader :http_client
|
20
|
+
|
21
|
+
def initialize_http_client!
|
22
|
+
@http_client = HTTPClient.new.tap do |c|
|
23
|
+
c.cookie_manager = nil
|
24
|
+
c.connect_timeout = 30
|
25
|
+
c.send_timeout = 45
|
26
|
+
c.receive_timeout = 30
|
27
|
+
c.keep_alive_timeout = 15
|
28
|
+
c.ssl_config.set_default_paths
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# We use this accessor / initialize as opposed to a simple constant
|
34
|
+
# because during specs, Webmock stubs the HTTPClient class, but that's
|
35
|
+
# happens after we initialized the constant (we could work around that
|
36
|
+
# by loading Webmock first, but this is just as simple.
|
37
|
+
initialize_http_client!
|
38
|
+
|
14
39
|
## Loads and returns the resource pointed to by +href+. The returned
|
15
40
|
## resource will be blessed into its "proper" class, if
|
16
41
|
## +self.class.namespace != nil+.
|
17
42
|
def get
|
18
|
-
execute_request
|
43
|
+
execute_request('GET') do |uri, headers|
|
44
|
+
HTTP.http_client.get(
|
45
|
+
uri,
|
46
|
+
follow_redirect: true,
|
47
|
+
header: headers
|
48
|
+
)
|
49
|
+
end
|
19
50
|
end
|
20
51
|
|
21
52
|
## By default, calls +post+ with the given arguments. Override to
|
@@ -28,8 +59,13 @@ class HyperResource
|
|
28
59
|
## the response resource.
|
29
60
|
def post(attrs = nil)
|
30
61
|
attrs ||= attributes
|
31
|
-
|
32
|
-
|
62
|
+
|
63
|
+
execute_request('POST') do |uri, headers|
|
64
|
+
HTTP.http_client.post(
|
65
|
+
uri,
|
66
|
+
body: adapter.serialize(attrs),
|
67
|
+
header: headers.merge(CONTENT_TYPE_HEADERS)
|
68
|
+
)
|
33
69
|
end
|
34
70
|
end
|
35
71
|
|
@@ -44,8 +80,13 @@ class HyperResource
|
|
44
80
|
## instead.
|
45
81
|
def put(attrs = nil)
|
46
82
|
attrs ||= attributes
|
47
|
-
|
48
|
-
|
83
|
+
|
84
|
+
execute_request('PUT') do |uri, headers|
|
85
|
+
HTTP.http_client.put(
|
86
|
+
uri,
|
87
|
+
body: adapter.serialize(attrs),
|
88
|
+
header: headers.merge(CONTENT_TYPE_HEADERS)
|
89
|
+
)
|
49
90
|
end
|
50
91
|
end
|
51
92
|
|
@@ -54,49 +95,62 @@ class HyperResource
|
|
54
95
|
## uses those instead.
|
55
96
|
def patch(attrs = nil)
|
56
97
|
attrs ||= attributes.changed_attributes
|
57
|
-
|
58
|
-
|
98
|
+
|
99
|
+
execute_request('PATCH') do |uri, headers|
|
100
|
+
HTTP.http_client.patch(
|
101
|
+
uri,
|
102
|
+
body: adapter.serialize(attrs),
|
103
|
+
header: headers.merge(CONTENT_TYPE_HEADERS)
|
104
|
+
)
|
59
105
|
end
|
60
106
|
end
|
61
107
|
|
62
108
|
## DELETEs this resource's href, and returns the response resource.
|
63
109
|
def delete
|
64
|
-
execute_request
|
65
|
-
|
66
|
-
|
67
|
-
## Returns a raw Faraday connection to this resource's URL, with proper
|
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])
|
78
|
-
builder.basic_auth(*ba)
|
79
|
-
end
|
80
|
-
|
81
|
-
builder.use WrapErrors # This has to be first!
|
82
|
-
builder.request :url_encoded
|
83
|
-
builder.adapter Faraday.default_adapter
|
110
|
+
execute_request('DELETE') do |uri, headers|
|
111
|
+
HTTP.http_client.delete(uri, header: headers)
|
84
112
|
end
|
85
113
|
end
|
86
114
|
|
87
115
|
private
|
88
116
|
|
89
|
-
def execute_request
|
117
|
+
def execute_request(method)
|
90
118
|
raise 'execute_request needs a block!' unless block_given?
|
91
119
|
retry_coordinator = Aptible::Resource.retry_coordinator_class.new(self)
|
92
120
|
|
121
|
+
uri = URI.join(root, href)
|
122
|
+
|
123
|
+
h = headers || {}
|
124
|
+
h['User-Agent'] = Aptible::Resource.configuration.user_agent
|
125
|
+
|
93
126
|
n_retry = 0
|
94
127
|
|
95
128
|
begin
|
129
|
+
t0 = Time.now
|
130
|
+
|
96
131
|
begin
|
97
|
-
|
98
|
-
|
99
|
-
|
132
|
+
res = yield(uri, h)
|
133
|
+
entity = finish_up(res)
|
134
|
+
rescue StandardError => e
|
135
|
+
Aptible::Resource.configuration.logger.info([
|
136
|
+
method,
|
137
|
+
uri,
|
138
|
+
"(#{n_retry})",
|
139
|
+
"#{(Time.now - t0).round(2)}s",
|
140
|
+
"ERR[#{e.class}: #{e}]"
|
141
|
+
].join(' '))
|
142
|
+
|
143
|
+
raise WrapErrors::WrappedError.new(method, e)
|
144
|
+
else
|
145
|
+
Aptible::Resource.configuration.logger.info([
|
146
|
+
method,
|
147
|
+
uri,
|
148
|
+
"(#{n_retry})",
|
149
|
+
"#{(Time.now - t0).round(2)}s",
|
150
|
+
res.status
|
151
|
+
].join(' '))
|
152
|
+
|
153
|
+
entity
|
100
154
|
end
|
101
155
|
rescue WrapErrors::WrappedError => e
|
102
156
|
n_retry += 1
|
@@ -121,18 +175,23 @@ class HyperResource
|
|
121
175
|
elsif status / 100 == 3
|
122
176
|
raise 'HyperResource does not handle redirects'
|
123
177
|
elsif status / 100 == 4
|
124
|
-
raise HyperResource::ClientError.new(
|
125
|
-
|
126
|
-
|
178
|
+
raise HyperResource::ClientError.new(
|
179
|
+
status.to_s,
|
180
|
+
response: response,
|
181
|
+
body: body
|
182
|
+
)
|
127
183
|
elsif status / 100 == 5
|
128
|
-
raise HyperResource::ServerError.new(
|
129
|
-
|
130
|
-
|
131
|
-
|
184
|
+
raise HyperResource::ServerError.new(
|
185
|
+
status.to_s,
|
186
|
+
response: response,
|
187
|
+
body: body
|
188
|
+
)
|
132
189
|
else ## 1xx? really?
|
133
|
-
raise HyperResource::ResponseError.new(
|
134
|
-
|
135
|
-
|
190
|
+
raise HyperResource::ResponseError.new(
|
191
|
+
"Got status #{status}, wtf?",
|
192
|
+
response: response,
|
193
|
+
body: body
|
194
|
+
)
|
136
195
|
|
137
196
|
end
|
138
197
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class HyperResource
|
2
2
|
module Modules
|
3
3
|
module HTTP
|
4
|
-
|
4
|
+
module WrapErrors
|
5
5
|
class WrappedError < StandardError
|
6
6
|
attr_reader :method, :err
|
7
7
|
|
@@ -10,12 +10,6 @@ class HyperResource
|
|
10
10
|
@err = err
|
11
11
|
end
|
12
12
|
end
|
13
|
-
|
14
|
-
def call(env)
|
15
|
-
@app.call(env)
|
16
|
-
rescue StandardError => e
|
17
|
-
raise WrappedError.new(env.method, e)
|
18
|
-
end
|
19
13
|
end
|
20
14
|
end
|
21
15
|
end
|
@@ -26,7 +26,6 @@ module HyperResource::Modules
|
|
26
26
|
:headers, ## e.g. {'Accept' => 'application/vnd.example+json'}
|
27
27
|
:namespace, ## e.g. 'ExampleAPI', or the class ExampleAPI itself
|
28
28
|
:adapter, ## subclass of HR::Adapter
|
29
|
-
:faraday_options ## e.g. {:request => {:timeout => 30}}
|
30
29
|
]
|
31
30
|
end
|
32
31
|
|
@@ -37,7 +36,6 @@ module HyperResource::Modules
|
|
37
36
|
:headers,
|
38
37
|
:namespace,
|
39
38
|
:adapter,
|
40
|
-
:faraday_options,
|
41
39
|
:token,
|
42
40
|
|
43
41
|
:request,
|
@@ -52,16 +52,31 @@ describe Aptible::Resource::Base do
|
|
52
52
|
end
|
53
53
|
|
54
54
|
describe '.find' do
|
55
|
-
it 'should
|
56
|
-
|
57
|
-
|
58
|
-
|
55
|
+
it 'should find' do
|
56
|
+
stub_request(
|
57
|
+
:get, 'https://resource.example.com/mainframes/42'
|
58
|
+
).to_return(body: { id: 42 }.to_json, status: 200)
|
59
|
+
|
60
|
+
m = Api::Mainframe.find(42)
|
61
|
+
expect(m.id).to eq(42)
|
59
62
|
end
|
60
63
|
|
61
|
-
it 'should
|
62
|
-
|
63
|
-
|
64
|
-
|
64
|
+
it 'should find with query params' do
|
65
|
+
stub_request(
|
66
|
+
:get, 'https://resource.example.com/mainframes/42?test=123'
|
67
|
+
).to_return(body: { id: 42 }.to_json, status: 200)
|
68
|
+
|
69
|
+
m = Api::Mainframe.find(42, test: 123)
|
70
|
+
expect(m.id).to eq(42)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should return an instance of the correct class' do
|
74
|
+
stub_request(
|
75
|
+
:get, 'https://resource.example.com/mainframes/42'
|
76
|
+
).to_return(body: { id: 42 }.to_json, status: 200)
|
77
|
+
|
78
|
+
m = Api::Mainframe.find(42)
|
79
|
+
expect(m).to be_a(Api::Mainframe)
|
65
80
|
end
|
66
81
|
end
|
67
82
|
|
@@ -453,7 +468,7 @@ describe Aptible::Resource::Base do
|
|
453
468
|
end
|
454
469
|
|
455
470
|
context 'configuration' do
|
456
|
-
subject { Api.new(root: 'http://
|
471
|
+
subject { Api.new(root: 'http://example.com') }
|
457
472
|
|
458
473
|
def configure_new_coordinator(&block)
|
459
474
|
Aptible::Resource.configure do |config|
|
@@ -468,7 +483,7 @@ describe Aptible::Resource::Base do
|
|
468
483
|
it 'should not retry if the proc returns false' do
|
469
484
|
configure_new_coordinator { define_method(:retry?) { |_, _e| false } }
|
470
485
|
|
471
|
-
stub_request(:get, '
|
486
|
+
stub_request(:get, 'example.com')
|
472
487
|
.to_return(body: { error: 'foo' }.to_json, status: 401).then
|
473
488
|
.to_return(body: { status: 'ok' }.to_json, status: 200)
|
474
489
|
|
@@ -479,7 +494,7 @@ describe Aptible::Resource::Base do
|
|
479
494
|
it 'should retry if the proc returns true' do
|
480
495
|
configure_new_coordinator { define_method(:retry?) { |_, _e| true } }
|
481
496
|
|
482
|
-
stub_request(:get, '
|
497
|
+
stub_request(:get, 'example.com')
|
483
498
|
.to_return(body: { error: 'foo' }.to_json, status: 401).then
|
484
499
|
.to_return(body: { error: 'foo' }.to_json, status: 401).then
|
485
500
|
.to_return(body: { status: 'ok' }.to_json, status: 200)
|
@@ -494,7 +509,7 @@ describe Aptible::Resource::Base do
|
|
494
509
|
define_method(:retry?) { |_, _e| failures += 1 || true }
|
495
510
|
end
|
496
511
|
|
497
|
-
stub_request(:get, '
|
512
|
+
stub_request(:get, 'example.com')
|
498
513
|
.to_return(body: { error: 'foo' }.to_json, status: 401).then
|
499
514
|
.to_return(body: { status: 'ok' }.to_json, status: 200).then
|
500
515
|
.to_return(body: { error: 'foo' }.to_json, status: 401)
|
@@ -505,7 +520,7 @@ describe Aptible::Resource::Base do
|
|
505
520
|
end
|
506
521
|
|
507
522
|
it 'should not retry with the default proc' do
|
508
|
-
stub_request(:get, '
|
523
|
+
stub_request(:get, 'example.com')
|
509
524
|
.to_return(body: { error: 'foo' }.to_json, status: 401).then
|
510
525
|
.to_return(body: { status: 'ok' }.to_json, status: 200)
|
511
526
|
|
@@ -522,7 +537,7 @@ describe Aptible::Resource::Base do
|
|
522
537
|
define_method(:retry?) { |_, e| (exception = e) && false }
|
523
538
|
end
|
524
539
|
|
525
|
-
stub_request(:get, '
|
540
|
+
stub_request(:get, 'example.com')
|
526
541
|
.to_return(body: { error: 'foo' }.to_json, status: 401)
|
527
542
|
|
528
543
|
expect { subject.get.body }
|
@@ -544,11 +559,11 @@ describe Aptible::Resource::Base do
|
|
544
559
|
end
|
545
560
|
end
|
546
561
|
|
547
|
-
stub_request(:get, '
|
562
|
+
stub_request(:get, 'example.com')
|
548
563
|
.with(headers: { 'Authorization' => /foo/ })
|
549
564
|
.to_return(body: { error: 'foo' }.to_json, status: 401)
|
550
565
|
|
551
|
-
stub_request(:get, '
|
566
|
+
stub_request(:get, 'example.com')
|
552
567
|
.with(headers: { 'Authorization' => /bar/ })
|
553
568
|
.to_return(body: { status: 'ok' }.to_json, status: 200)
|
554
569
|
|
@@ -563,7 +578,7 @@ describe Aptible::Resource::Base do
|
|
563
578
|
define_method(:retry?) { |_, _e| n += 1 || true }
|
564
579
|
end
|
565
580
|
|
566
|
-
stub_request(:get, '
|
581
|
+
stub_request(:get, 'example.com')
|
567
582
|
.to_return(body: { error: 'foo' }.to_json, status: 401)
|
568
583
|
|
569
584
|
expect { subject.get.body }
|
@@ -579,7 +594,7 @@ describe Aptible::Resource::Base do
|
|
579
594
|
config.user_agent = 'foo ua'
|
580
595
|
end
|
581
596
|
|
582
|
-
stub_request(:get, '
|
597
|
+
stub_request(:get, 'example.com')
|
583
598
|
.with(headers: { 'User-Agent' => 'foo ua' })
|
584
599
|
.to_return(body: { status: 'ok' }.to_json, status: 200)
|
585
600
|
|
@@ -589,22 +604,22 @@ describe Aptible::Resource::Base do
|
|
589
604
|
end
|
590
605
|
|
591
606
|
context 'token' do
|
592
|
-
subject { Api.new(root: 'http://
|
607
|
+
subject { Api.new(root: 'http://example.com', token: 'bar') }
|
593
608
|
|
594
609
|
before do
|
595
|
-
stub_request(:get, '
|
610
|
+
stub_request(:get, 'example.com/')
|
596
611
|
.with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
|
597
612
|
.to_return(body: {
|
598
|
-
_links: { some: { href: 'http://
|
599
|
-
mainframes: { href: 'http://
|
613
|
+
_links: { some: { href: 'http://example.com/some' },
|
614
|
+
mainframes: { href: 'http://example.com/mainframes' } },
|
600
615
|
_embedded: { best_mainframe: { _type: 'mainframe', status: 'ok' } }
|
601
616
|
}.to_json, status: 200)
|
602
617
|
|
603
|
-
stub_request(:get, '
|
618
|
+
stub_request(:get, 'example.com/some')
|
604
619
|
.with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
|
605
620
|
.to_return(body: { status: 'ok' }.to_json, status: 200)
|
606
621
|
|
607
|
-
stub_request(:get, '
|
622
|
+
stub_request(:get, 'example.com/mainframes')
|
608
623
|
.with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
|
609
624
|
.to_return(body: { _embedded: {
|
610
625
|
mainframes: [{ status: 'ok' }]
|
@@ -646,4 +661,85 @@ describe Aptible::Resource::Base do
|
|
646
661
|
expect(m.token).to eq('bar')
|
647
662
|
end
|
648
663
|
end
|
664
|
+
|
665
|
+
context 'lazy fetching' do
|
666
|
+
subject { Api.new(root: 'http://foo.com') }
|
667
|
+
|
668
|
+
it 'should support enumerable methods' do
|
669
|
+
index = {
|
670
|
+
_links: {
|
671
|
+
some_items: { href: 'http://foo.com/some_items' }
|
672
|
+
}
|
673
|
+
}
|
674
|
+
|
675
|
+
some_items = {
|
676
|
+
_embedded: {
|
677
|
+
some_items: [
|
678
|
+
{ id: 1, handle: 'foo' },
|
679
|
+
{ id: 2, handle: 'bar' },
|
680
|
+
{ id: 3, handle: 'qux' }
|
681
|
+
]
|
682
|
+
}
|
683
|
+
}
|
684
|
+
|
685
|
+
stub_request(:get, 'foo.com')
|
686
|
+
.to_return(body: index.to_json, status: 200)
|
687
|
+
|
688
|
+
stub_request(:get, 'foo.com/some_items')
|
689
|
+
.to_return(body: some_items.to_json, status: 200)
|
690
|
+
|
691
|
+
bar = subject.some_items.find { |m| m.id == 2 }
|
692
|
+
expect(bar.handle).to eq('bar')
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
describe '_type' do
|
697
|
+
subject { Api.new(root: 'http://example.com', token: 'bar') }
|
698
|
+
|
699
|
+
it 'uses the correct class for an expected linked instance' do
|
700
|
+
stub_request(:get, 'example.com/')
|
701
|
+
.to_return(body: {
|
702
|
+
_links: {
|
703
|
+
worst_mainframe: { href: 'http://example.com/mainframes/123' }
|
704
|
+
}
|
705
|
+
}.to_json, status: 200)
|
706
|
+
|
707
|
+
stub_request(:get, 'example.com/mainframes/123')
|
708
|
+
.to_return(body: { _type: 'mainframe', id: 123 }.to_json, status: 200)
|
709
|
+
|
710
|
+
expect(subject.worst_mainframe).to be_a(Api::Mainframe)
|
711
|
+
end
|
712
|
+
|
713
|
+
it 'uses the correct class for an unexpected linked instance' do
|
714
|
+
stub_request(:get, 'example.com/')
|
715
|
+
.to_return(body: {
|
716
|
+
_links: {
|
717
|
+
some: { href: 'http://example.com/mainframes/123' }
|
718
|
+
}
|
719
|
+
}.to_json, status: 200)
|
720
|
+
|
721
|
+
stub_request(:get, 'example.com/mainframes/123')
|
722
|
+
.to_return(body: { _type: 'mainframe', id: 123 }.to_json, status: 200)
|
723
|
+
|
724
|
+
expect(subject.some.get).to be_a(Api::Mainframe)
|
725
|
+
end
|
726
|
+
|
727
|
+
it 'uses the correct class for an expected embedded instance' do
|
728
|
+
stub_request(:get, 'example.com/')
|
729
|
+
.to_return(body: {
|
730
|
+
_embedded: { best_mainframe: { _type: 'mainframe', id: 123 } }
|
731
|
+
}.to_json, status: 200)
|
732
|
+
|
733
|
+
expect(subject.best_mainframe).to be_a(Api::Mainframe)
|
734
|
+
end
|
735
|
+
|
736
|
+
it 'uses the correct class for an unexpected embedded instance' do
|
737
|
+
stub_request(:get, 'example.com/')
|
738
|
+
.to_return(body: {
|
739
|
+
_embedded: { some: { _type: 'mainframe', id: 123 } }
|
740
|
+
}.to_json, status: 200)
|
741
|
+
|
742
|
+
expect(subject.some).to be_a(Api::Mainframe)
|
743
|
+
end
|
744
|
+
end
|
649
745
|
end
|
@@ -136,15 +136,6 @@ describe Aptible::Resource::Base do
|
|
136
136
|
expect(subject.get.body).to eq(body)
|
137
137
|
end
|
138
138
|
|
139
|
-
it 'should retry timeout errors (Net::OpenTimeout)' do
|
140
|
-
stub_request(:get, href)
|
141
|
-
.to_raise(Net::OpenTimeout).then
|
142
|
-
.to_raise(Net::OpenTimeout).then
|
143
|
-
.to_return(body: json_body)
|
144
|
-
|
145
|
-
expect(subject.get.body).to eq(body)
|
146
|
-
end
|
147
|
-
|
148
139
|
it 'should retry connection errors' do
|
149
140
|
stub_request(:get, href)
|
150
141
|
.to_raise(Errno::ECONNREFUSED).then
|
@@ -159,29 +150,8 @@ describe Aptible::Resource::Base do
|
|
159
150
|
.to_timeout.then
|
160
151
|
.to_return(body: json_body)
|
161
152
|
|
162
|
-
expect { subject.post }
|
163
|
-
|
164
|
-
end
|
165
|
-
|
166
|
-
context 'without connections' do
|
167
|
-
around do |example|
|
168
|
-
WebMock.allow_net_connect!
|
169
|
-
example.run
|
170
|
-
WebMock.disable_net_connect!
|
171
|
-
end
|
172
|
-
|
173
|
-
it 'default to 10 seconds of timeout and retries 4 times' do
|
174
|
-
# This really relies on how exactly MRI implements Net::HTTP open
|
175
|
-
# timeouts
|
176
|
-
skip 'MRI implementation-specific' if RUBY_PLATFORM == 'java'
|
177
|
-
|
178
|
-
expect(Timeout).to receive(:timeout)
|
179
|
-
.with(10, Net::OpenTimeout)
|
180
|
-
.exactly(4).times
|
181
|
-
.and_raise(Net::OpenTimeout)
|
182
|
-
|
183
|
-
expect { subject.get }.to raise_error(Faraday::ConnectionFailed)
|
184
|
-
expect(sleeps.size).to eq(3)
|
153
|
+
expect { subject.post }
|
154
|
+
.to raise_error(HTTPClient::TimeoutError)
|
185
155
|
end
|
186
156
|
end
|
187
157
|
end
|
data/spec/fixtures/api.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -18,6 +18,8 @@ require 'webmock/rspec'
|
|
18
18
|
WebMock.disable_net_connect!
|
19
19
|
|
20
20
|
RSpec.configure do |config|
|
21
|
+
config.before(:suite) { HyperResource::Modules::HTTP.initialize_http_client! }
|
22
|
+
|
21
23
|
config.before { Aptible::Resource.configuration.reset }
|
22
24
|
config.before { WebMock.reset! }
|
23
25
|
|
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: 1.0.
|
4
|
+
version: 1.1.0.pre.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frank Macreery
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uri_template
|
@@ -25,39 +25,33 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.5.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: json
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0
|
34
|
-
- - "<"
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: '0.14'
|
33
|
+
version: '0'
|
37
34
|
type: :runtime
|
38
35
|
prerelease: false
|
39
36
|
version_requirements: !ruby/object:Gem::Requirement
|
40
37
|
requirements:
|
41
38
|
- - ">="
|
42
39
|
- !ruby/object:Gem::Version
|
43
|
-
version: 0
|
44
|
-
- - "<"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '0.14'
|
40
|
+
version: '0'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
42
|
+
name: httpclient
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
50
44
|
requirements:
|
51
|
-
- - "
|
45
|
+
- - "~>"
|
52
46
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
47
|
+
version: '2.8'
|
54
48
|
type: :runtime
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
51
|
requirements:
|
58
|
-
- - "
|
52
|
+
- - "~>"
|
59
53
|
- !ruby/object:Gem::Version
|
60
|
-
version: '
|
54
|
+
version: '2.8'
|
61
55
|
- !ruby/object:Gem::Dependency
|
62
56
|
name: fridge
|
63
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -249,12 +243,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
249
243
|
version: '0'
|
250
244
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
251
245
|
requirements:
|
252
|
-
- - "
|
246
|
+
- - ">"
|
253
247
|
- !ruby/object:Gem::Version
|
254
|
-
version:
|
248
|
+
version: 1.3.1
|
255
249
|
requirements: []
|
256
250
|
rubyforge_project:
|
257
|
-
rubygems_version: 2.
|
251
|
+
rubygems_version: 2.4.5.3
|
258
252
|
signing_key:
|
259
253
|
specification_version: 4
|
260
254
|
summary: Foundation classes for Aptible resource server gems
|