aptible-resource 1.0.2 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8763d2caea0e00ee89256be017ffc5276aef0e80
4
- data.tar.gz: 5bd0eef4af8fe6bf6e20cbbae9023f5fbfdb461a
2
+ SHA256:
3
+ metadata.gz: 4eeb9d01868528de4be4cb015abbb22c9710086bf2d880c075997a60475f06ba
4
+ data.tar.gz: 6b1f04a38239649c91bc77f930c4d862ea7d068c7c4f77c36684699fed726925
5
5
  SHA512:
6
- metadata.gz: 79805e68facedfce6282d7f0f945b07df459478929a01ada4e6b76f5c88e20ac8ddacfa25bb51cbfc9a11953591aed06cd7c6976cfb709d0cfb40957607c4113
7
- data.tar.gz: 2deab0f8a6ca743f987e9beffe1e7ddfccb0c9287cfa853a82be74b6d822da3bee1eae08e7bdcbe8aa0e84d7c5a79a6bc1c9cf9e885d45b28ca89c8cf50ef9da
6
+ metadata.gz: 87b33183877986d2b1fc2ae9975a256bd361efb0c95f94a7e8d1cd145c36bebd8e3c185fcff32ed394f46076484c65743861004128a52419a5e7fe7541dd35b0
7
+ data.tar.gz: 90aaa5a7269f47379ed9e8bf7b23e510a3da41072ad7ad7a16a6f8e8ad9332cc473b367a354f88b5743d671230a317425858f42d63b9da95bb120ac1675d51de
@@ -0,0 +1 @@
1
+ * @dawenster
data/.travis.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  sudo: false
2
2
 
3
3
  rvm:
4
- - "2.0"
5
- - "2.1"
6
4
  - "2.2"
7
5
  - "2.3"
8
- - jruby-9.1.13.0
6
+ - "2.6"
7
+ - "2.7"
data/Gemfile CHANGED
@@ -1,6 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'activesupport', '~> 4.0'
4
-
5
3
  # Specify your gem's dependencies in aptible-resource.gemspec
6
4
  gemspec
data/README.md CHANGED
@@ -49,7 +49,5 @@ This gem depends on a vendored version of [HyperResource](https://github.com/gam
49
49
 
50
50
  MIT License, see [LICENSE](LICENSE.md) for details.
51
51
 
52
- Copyright (c) 2014 [Aptible](https://www.aptible.com) and contributors.
53
-
54
- [<img src="https://s.gravatar.com/avatar/9b58236204e844e3181e43e05ddb0809?s=60" style="border-radius: 50%;" alt="@sandersonet" />](https://github.com/sandersonet)
52
+ Copyright (c) 2019 [Aptible](https://www.aptible.com) and contributors.
55
53
 
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
 
5
6
  require 'English'
@@ -20,18 +21,17 @@ Gem::Specification.new do |spec|
20
21
  spec.require_paths = ['lib']
21
22
 
22
23
  # HyperResource dependencies
23
- spec.add_dependency 'uri_template', '>= 0.5.2'
24
- spec.add_dependency 'faraday', '>= 0.9.2', '< 0.14'
25
- spec.add_dependency 'json'
26
-
24
+ spec.add_dependency 'activesupport'
27
25
  spec.add_dependency 'fridge'
28
- spec.add_dependency 'activesupport', '>= 4.0', '< 6.0'
29
26
  spec.add_dependency 'gem_config', '~> 0.3.1'
27
+ spec.add_dependency 'httpclient', '~> 2.8'
28
+ spec.add_dependency 'json'
29
+ spec.add_dependency 'uri_template', '>= 0.5.2'
30
30
 
31
- spec.add_development_dependency 'bundler', '~> 1.3'
32
31
  spec.add_development_dependency 'aptible-tasks'
33
- spec.add_development_dependency 'rake'
34
- spec.add_development_dependency 'rspec', '~> 2.0'
32
+ spec.add_development_dependency 'bundler'
35
33
  spec.add_development_dependency 'pry'
34
+ spec.add_development_dependency 'rake', '< 11.0'
35
+ spec.add_development_dependency 'rspec', '~> 2.0'
36
36
  spec.add_development_dependency 'webmock', '~> 2.3.2'
37
37
  end
@@ -6,6 +6,7 @@ module Aptible
6
6
  return nil unless object
7
7
 
8
8
  return nil unless (type = object['_type'])
9
+
9
10
  if type.respond_to?(:camelize)
10
11
  type.camelize
11
12
  else
@@ -5,7 +5,7 @@ require 'active_support/core_ext'
5
5
  require 'date'
6
6
 
7
7
  # Require vendored HyperResource
8
- $LOAD_PATH.unshift File.expand_path('../..', __FILE__)
8
+ $LOAD_PATH.unshift File.expand_path('..', __dir__)
9
9
  require 'hyper_resource'
10
10
 
11
11
  require 'aptible/resource/adapter'
@@ -15,12 +15,14 @@ require 'aptible/resource/boolean'
15
15
  module Aptible
16
16
  module Resource
17
17
  # rubocop:disable ClassLength
18
+ # rubocop:disable DuplicateMethods
18
19
  class Base < HyperResource
19
20
  attr_accessor :errors
20
21
  attr_reader :token
21
22
 
22
23
  def self.get_data_type_from_response(response)
23
24
  return nil unless response && response.body
25
+
24
26
  adapter.get_data_type_from_object(adapter.deserialize(response.body))
25
27
  end
26
28
 
@@ -79,6 +81,7 @@ module Aptible
79
81
  new(options).find_by_url(url)
80
82
  rescue HyperResource::ClientError => e
81
83
  return nil if e.response.status == 404
84
+
82
85
  raise e
83
86
  end
84
87
 
@@ -182,6 +185,7 @@ module Aptible
182
185
 
183
186
  define_method iterator_method do |&block|
184
187
  next enum_for(iterator_method) if block.nil?
188
+
185
189
  send(relation).each(&block)
186
190
  end
187
191
  end
@@ -224,16 +228,7 @@ module Aptible
224
228
  end
225
229
  end
226
230
 
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
-
231
+ # rubocop:disable ReturnInVoidContext
237
232
  def initialize(options = {})
238
233
  return super(options) unless options.is_a?(Hash)
239
234
 
@@ -241,6 +236,7 @@ module Aptible
241
236
  super(options)
242
237
  self.token = options[:token] if options[:token]
243
238
  end
239
+ # rubocop:enable ReturnInVoidContext
244
240
 
245
241
  def populate_default_options!(options)
246
242
  options[:root] ||= root_url
@@ -305,7 +301,7 @@ module Aptible
305
301
  # Already deleted
306
302
  raise unless e.response.status == 404
307
303
  rescue HyperResource::ResponseError
308
- # HyperResource/Faraday choke on empty response bodies
304
+ # HyperResource chokes on empty response bodies
309
305
  nil
310
306
  end
311
307
 
@@ -327,5 +323,6 @@ module Aptible
327
323
  end
328
324
  end
329
325
  # rubocop:enable ClassLength
326
+ # rubocop:enable DuplicateMethods
330
327
  end
331
328
  end
@@ -5,11 +5,27 @@ module Aptible
5
5
 
6
6
  IDEMPOTENT_METHODS = [
7
7
  # Idempotent as per RFC
8
- :delete, :get, :head, :options, :put,
8
+ 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT',
9
+
9
10
  # Idempotent on our APIs
10
- :patch
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
@@ -19,9 +35,12 @@ module Aptible
19
35
  def retry?(method, err)
20
36
  # rubocop:disable Style/CaseEquality
21
37
  return false unless RETRY_ERRORS.any? { |c| c === err }
38
+
22
39
  return false unless IDEMPOTENT_METHODS.include?(method)
40
+
23
41
  retry_in = retry_schedule.shift
24
42
  return false if retry_in.nil?
43
+
25
44
  sleep retry_in
26
45
  true
27
46
  # rubocop:enable Style/CaseEquality
@@ -1,6 +1,7 @@
1
1
  module Aptible
2
2
  module Resource
3
3
  class Errors
4
+ # rubocop:disable DuplicateMethods
4
5
  attr_accessor :status_code, :messages, :full_messages
5
6
 
6
7
  def self.from_exception(exception)
@@ -23,6 +24,7 @@ module Aptible
23
24
  def any?
24
25
  full_messages.any?
25
26
  end
27
+ # rubocop:enable DuplicateMethods
26
28
  end
27
29
  end
28
30
  end
@@ -1,10 +1,9 @@
1
1
  module Aptible
2
2
  module Resource
3
3
  class NullRetryCoordinator
4
- def initialize(_)
5
- end
4
+ def initialize(___); end
6
5
 
7
- def retry?(_, _)
6
+ def retry?(___, ____)
8
7
  false
9
8
  end
10
9
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module Resource
3
- VERSION = '1.0.2'.freeze
3
+ VERSION = '1.1.1'.freeze
4
4
  end
5
5
  end
@@ -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
@@ -37,6 +42,7 @@ module Aptible
37
42
  def retry_coordinator_class
38
43
  override = Thread.current[RETRY_COORDINATOR_OVERRIDE]
39
44
  return override if override
45
+
40
46
  configuration.retry_coordinator_class
41
47
  end
42
48
  end
@@ -10,7 +10,7 @@ class HyperResource
10
10
  end
11
11
 
12
12
  class ResponseError < Exception
13
- ## The +Faraday::Response+ object which led to this exception.
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.
@@ -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,7 +1,7 @@
1
1
  class HyperResource
2
2
  module Modules
3
3
  module HTTP
4
- class WrapErrors < Faraday::Middleware
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
@@ -1,4 +1,4 @@
1
- require 'faraday'
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 { faraday_connection.get(href || '') }
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
- execute_request do
32
- faraday_connection.post { |req| req.body = adapter.serialize(attrs) }
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
- execute_request do
48
- faraday_connection.put { |req| req.body = adapter.serialize(attrs) }
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
- execute_request do
58
- faraday_connection.patch { |req| req.body = adapter.serialize(attrs) }
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 { faraday_connection.delete }
65
- end
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
- finish_up(yield)
98
- rescue HyperResource::ResponseError => e
99
- raise WrapErrors::WrappedError.new(e.response.env.method, e)
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(status.to_s,
125
- response: response,
126
- body: body)
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(status.to_s,
129
- response: response,
130
- body: body)
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("Got status #{status}, wtf?",
134
- response: response,
135
- body: body)
190
+ raise HyperResource::ResponseError.new(
191
+ "Got status #{status}, wtf?",
192
+ response: response,
193
+ body: body
194
+ )
136
195
 
137
196
  end
138
197
 
@@ -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,
@@ -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] rescue nil
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) rescue nil
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
  ##
@@ -1,5 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
+ # rubocop:disable BlockLength
4
+ # rubocop:disable InterpolationCheck
3
5
  describe Aptible::Resource::Base do
4
6
  let(:hyperresource_exception) { HyperResource::ResponseError.new('403') }
5
7
  let(:error_response) { double 'Faraday::Response' }
@@ -36,6 +38,7 @@ describe Aptible::Resource::Base do
36
38
  calls << u
37
39
  page = pages[u]
38
40
  raise "Accessed unexpected URL #{u}" if page.nil?
41
+
39
42
  page
40
43
  end
41
44
  end
@@ -52,16 +55,31 @@ describe Aptible::Resource::Base do
52
55
  end
53
56
 
54
57
  describe '.find' do
55
- it 'should call find_by_url' do
56
- url = '/mainframes/42'
57
- expect(Api::Mainframe).to receive(:find_by_url).with url, {}
58
- Api::Mainframe.find(42)
58
+ it 'should find' do
59
+ stub_request(
60
+ :get, 'https://resource.example.com/mainframes/42'
61
+ ).to_return(body: { id: 42 }.to_json, status: 200)
62
+
63
+ m = Api::Mainframe.find(42)
64
+ expect(m.id).to eq(42)
65
+ end
66
+
67
+ it 'should find with query params' do
68
+ stub_request(
69
+ :get, 'https://resource.example.com/mainframes/42?test=123'
70
+ ).to_return(body: { id: 42 }.to_json, status: 200)
71
+
72
+ m = Api::Mainframe.find(42, test: 123)
73
+ expect(m.id).to eq(42)
59
74
  end
60
75
 
61
- it 'should call find_by_url with query params' do
62
- url = '/mainframes/42?test=123'
63
- expect(Api::Mainframe).to receive(:find_by_url).with url, test: 123
64
- Api::Mainframe.find(42, test: 123)
76
+ it 'should return an instance of the correct class' do
77
+ stub_request(
78
+ :get, 'https://resource.example.com/mainframes/42'
79
+ ).to_return(body: { id: 42 }.to_json, status: 200)
80
+
81
+ m = Api::Mainframe.find(42)
82
+ expect(m).to be_a(Api::Mainframe)
65
83
  end
66
84
  end
67
85
 
@@ -72,7 +90,7 @@ describe Aptible::Resource::Base do
72
90
 
73
91
  before do
74
92
  collection.stub(:entries) { [mainframe] }
75
- collection.stub(:links) { Hash.new }
93
+ collection.stub(:links) { {} }
76
94
  Api::Mainframe.any_instance.stub(:find_by_url) { collection }
77
95
  end
78
96
 
@@ -254,7 +272,7 @@ describe Aptible::Resource::Base do
254
272
  HyperResource.any_instance.stub(:put) { raise hyperresource_exception }
255
273
  begin
256
274
  subject.update!({})
257
- rescue
275
+ rescue StandardError
258
276
  # Allow errors to be populated and tested
259
277
  nil
260
278
  end
@@ -453,7 +471,7 @@ describe Aptible::Resource::Base do
453
471
  end
454
472
 
455
473
  context 'configuration' do
456
- subject { Api.new(root: 'http://foo.com') }
474
+ subject { Api.new(root: 'http://example.com') }
457
475
 
458
476
  def configure_new_coordinator(&block)
459
477
  Aptible::Resource.configure do |config|
@@ -468,7 +486,7 @@ describe Aptible::Resource::Base do
468
486
  it 'should not retry if the proc returns false' do
469
487
  configure_new_coordinator { define_method(:retry?) { |_, _e| false } }
470
488
 
471
- stub_request(:get, 'foo.com')
489
+ stub_request(:get, 'example.com')
472
490
  .to_return(body: { error: 'foo' }.to_json, status: 401).then
473
491
  .to_return(body: { status: 'ok' }.to_json, status: 200)
474
492
 
@@ -479,7 +497,7 @@ describe Aptible::Resource::Base do
479
497
  it 'should retry if the proc returns true' do
480
498
  configure_new_coordinator { define_method(:retry?) { |_, _e| true } }
481
499
 
482
- stub_request(:get, 'foo.com')
500
+ stub_request(:get, 'example.com')
483
501
  .to_return(body: { error: 'foo' }.to_json, status: 401).then
484
502
  .to_return(body: { error: 'foo' }.to_json, status: 401).then
485
503
  .to_return(body: { status: 'ok' }.to_json, status: 200)
@@ -494,7 +512,7 @@ describe Aptible::Resource::Base do
494
512
  define_method(:retry?) { |_, _e| failures += 1 || true }
495
513
  end
496
514
 
497
- stub_request(:get, 'foo.com')
515
+ stub_request(:get, 'example.com')
498
516
  .to_return(body: { error: 'foo' }.to_json, status: 401).then
499
517
  .to_return(body: { status: 'ok' }.to_json, status: 200).then
500
518
  .to_return(body: { error: 'foo' }.to_json, status: 401)
@@ -505,7 +523,7 @@ describe Aptible::Resource::Base do
505
523
  end
506
524
 
507
525
  it 'should not retry with the default proc' do
508
- stub_request(:get, 'foo.com')
526
+ stub_request(:get, 'example.com')
509
527
  .to_return(body: { error: 'foo' }.to_json, status: 401).then
510
528
  .to_return(body: { status: 'ok' }.to_json, status: 200)
511
529
 
@@ -522,7 +540,7 @@ describe Aptible::Resource::Base do
522
540
  define_method(:retry?) { |_, e| (exception = e) && false }
523
541
  end
524
542
 
525
- stub_request(:get, 'foo.com')
543
+ stub_request(:get, 'example.com')
526
544
  .to_return(body: { error: 'foo' }.to_json, status: 401)
527
545
 
528
546
  expect { subject.get.body }
@@ -544,11 +562,11 @@ describe Aptible::Resource::Base do
544
562
  end
545
563
  end
546
564
 
547
- stub_request(:get, 'foo.com')
565
+ stub_request(:get, 'example.com')
548
566
  .with(headers: { 'Authorization' => /foo/ })
549
567
  .to_return(body: { error: 'foo' }.to_json, status: 401)
550
568
 
551
- stub_request(:get, 'foo.com')
569
+ stub_request(:get, 'example.com')
552
570
  .with(headers: { 'Authorization' => /bar/ })
553
571
  .to_return(body: { status: 'ok' }.to_json, status: 200)
554
572
 
@@ -563,7 +581,7 @@ describe Aptible::Resource::Base do
563
581
  define_method(:retry?) { |_, _e| n += 1 || true }
564
582
  end
565
583
 
566
- stub_request(:get, 'foo.com')
584
+ stub_request(:get, 'example.com')
567
585
  .to_return(body: { error: 'foo' }.to_json, status: 401)
568
586
 
569
587
  expect { subject.get.body }
@@ -579,7 +597,7 @@ describe Aptible::Resource::Base do
579
597
  config.user_agent = 'foo ua'
580
598
  end
581
599
 
582
- stub_request(:get, 'foo.com')
600
+ stub_request(:get, 'example.com')
583
601
  .with(headers: { 'User-Agent' => 'foo ua' })
584
602
  .to_return(body: { status: 'ok' }.to_json, status: 200)
585
603
 
@@ -589,22 +607,22 @@ describe Aptible::Resource::Base do
589
607
  end
590
608
 
591
609
  context 'token' do
592
- subject { Api.new(root: 'http://foo.com', token: 'bar') }
610
+ subject { Api.new(root: 'http://example.com', token: 'bar') }
593
611
 
594
612
  before do
595
- stub_request(:get, 'foo.com/')
613
+ stub_request(:get, 'example.com/')
596
614
  .with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
597
615
  .to_return(body: {
598
- _links: { some: { href: 'http://foo.com/some' },
599
- mainframes: { href: 'http://foo.com/mainframes' } },
616
+ _links: { some: { href: 'http://example.com/some' },
617
+ mainframes: { href: 'http://example.com/mainframes' } },
600
618
  _embedded: { best_mainframe: { _type: 'mainframe', status: 'ok' } }
601
619
  }.to_json, status: 200)
602
620
 
603
- stub_request(:get, 'foo.com/some')
621
+ stub_request(:get, 'example.com/some')
604
622
  .with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
605
623
  .to_return(body: { status: 'ok' }.to_json, status: 200)
606
624
 
607
- stub_request(:get, 'foo.com/mainframes')
625
+ stub_request(:get, 'example.com/mainframes')
608
626
  .with(headers: { 'Authorization' => /Bearer (bar|foo)/ })
609
627
  .to_return(body: { _embedded: {
610
628
  mainframes: [{ status: 'ok' }]
@@ -646,4 +664,87 @@ describe Aptible::Resource::Base do
646
664
  expect(m.token).to eq('bar')
647
665
  end
648
666
  end
667
+
668
+ context 'lazy fetching' do
669
+ subject { Api.new(root: 'http://foo.com') }
670
+
671
+ it 'should support enumerable methods' do
672
+ index = {
673
+ _links: {
674
+ some_items: { href: 'http://foo.com/some_items' }
675
+ }
676
+ }
677
+
678
+ some_items = {
679
+ _embedded: {
680
+ some_items: [
681
+ { id: 1, handle: 'foo' },
682
+ { id: 2, handle: 'bar' },
683
+ { id: 3, handle: 'qux' }
684
+ ]
685
+ }
686
+ }
687
+
688
+ stub_request(:get, 'foo.com')
689
+ .to_return(body: index.to_json, status: 200)
690
+
691
+ stub_request(:get, 'foo.com/some_items')
692
+ .to_return(body: some_items.to_json, status: 200)
693
+
694
+ bar = subject.some_items.find { |m| m.id == 2 }
695
+ expect(bar.handle).to eq('bar')
696
+ end
697
+ end
698
+
699
+ describe '_type' do
700
+ subject { Api.new(root: 'http://example.com', token: 'bar') }
701
+
702
+ it 'uses the correct class for an expected linked instance' do
703
+ stub_request(:get, 'example.com/')
704
+ .to_return(body: {
705
+ _links: {
706
+ worst_mainframe: { href: 'http://example.com/mainframes/123' }
707
+ }
708
+ }.to_json, status: 200)
709
+
710
+ stub_request(:get, 'example.com/mainframes/123')
711
+ .to_return(body: { _type: 'mainframe', id: 123 }.to_json, status: 200)
712
+
713
+ expect(subject.worst_mainframe).to be_a(Api::Mainframe)
714
+ end
715
+
716
+ it 'uses the correct class for an unexpected linked instance' do
717
+ stub_request(:get, 'example.com/')
718
+ .to_return(body: {
719
+ _links: {
720
+ some: { href: 'http://example.com/mainframes/123' }
721
+ }
722
+ }.to_json, status: 200)
723
+
724
+ stub_request(:get, 'example.com/mainframes/123')
725
+ .to_return(body: { _type: 'mainframe', id: 123 }.to_json, status: 200)
726
+
727
+ expect(subject.some.get).to be_a(Api::Mainframe)
728
+ end
729
+
730
+ it 'uses the correct class for an expected embedded instance' do
731
+ stub_request(:get, 'example.com/')
732
+ .to_return(body: {
733
+ _embedded: { best_mainframe: { _type: 'mainframe', id: 123 } }
734
+ }.to_json, status: 200)
735
+
736
+ expect(subject.best_mainframe).to be_a(Api::Mainframe)
737
+ end
738
+
739
+ it 'uses the correct class for an unexpected embedded instance' do
740
+ stub_request(:get, 'example.com/')
741
+ .to_return(body: {
742
+ _embedded: { some: { _type: 'mainframe', id: 123 } }
743
+ }.to_json, status: 200)
744
+
745
+ expect(subject.some).to be_a(Api::Mainframe)
746
+ end
747
+ end
649
748
  end
749
+ # rubocop:enable InterpolationCheck
750
+ # rubocop:enable BlockLength
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  # With webmock (fake connections), to check how we handle timeouts.
4
+ # rubocop:disable BlockLength
4
5
  describe Aptible::Resource::Base do
5
6
  let(:body) do
6
7
  { 'hello' => '1', '_links' => { 'self' => { 'href' => href } } }
@@ -136,15 +137,6 @@ describe Aptible::Resource::Base do
136
137
  expect(subject.get.body).to eq(body)
137
138
  end
138
139
 
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
140
  it 'should retry connection errors' do
149
141
  stub_request(:get, href)
150
142
  .to_raise(Errno::ECONNREFUSED).then
@@ -159,30 +151,10 @@ describe Aptible::Resource::Base do
159
151
  .to_timeout.then
160
152
  .to_return(body: json_body)
161
153
 
162
- expect { subject.post }.to raise_error(Faraday::ConnectionFailed)
163
- end
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)
154
+ expect { subject.post }
155
+ .to raise_error(HTTPClient::TimeoutError)
185
156
  end
186
157
  end
187
158
  end
188
159
  end
160
+ # rubocop:enable BlockLength
data/spec/fixtures/api.rb CHANGED
@@ -4,6 +4,7 @@ class Api < Aptible::Resource::Base
4
4
  has_many :mainframes
5
5
  embeds_many :embedded_mainframes
6
6
  embeds_one :best_mainframe
7
+ has_one :worst_mainframe
7
8
 
8
9
  def namespace
9
10
  'Api'
@@ -3,3 +3,4 @@ require_relative 'api'
3
3
  # rubocop:disable ClassAndModuleChildren
4
4
  class Api::Mainframe < Api
5
5
  end
6
+ # rubocop:enable ClassAndModuleChildren
@@ -3,3 +3,4 @@ require_relative 'api'
3
3
  # rubocop:disable ClassAndModuleChildren
4
4
  class Api::Token < Api
5
5
  end
6
+ # rubocop:enable ClassAndModuleChildren
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,127 +1,115 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-16 00:00:00.000000000 Z
11
+ date: 2022-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: uri_template
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.5.2
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.5.2
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: faraday
28
+ name: fridge
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.9.2
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.9.2
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '0.14'
40
+ version: '0'
47
41
  - !ruby/object:Gem::Dependency
48
- name: json
42
+ name: gem_config
49
43
  requirement: !ruby/object:Gem::Requirement
50
44
  requirements:
51
- - - ">="
45
+ - - "~>"
52
46
  - !ruby/object:Gem::Version
53
- version: '0'
47
+ version: 0.3.1
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: '0'
54
+ version: 0.3.1
61
55
  - !ruby/object:Gem::Dependency
62
- name: fridge
56
+ name: httpclient
63
57
  requirement: !ruby/object:Gem::Requirement
64
58
  requirements:
65
- - - ">="
59
+ - - "~>"
66
60
  - !ruby/object:Gem::Version
67
- version: '0'
61
+ version: '2.8'
68
62
  type: :runtime
69
63
  prerelease: false
70
64
  version_requirements: !ruby/object:Gem::Requirement
71
65
  requirements:
72
- - - ">="
66
+ - - "~>"
73
67
  - !ruby/object:Gem::Version
74
- version: '0'
68
+ version: '2.8'
75
69
  - !ruby/object:Gem::Dependency
76
- name: activesupport
70
+ name: json
77
71
  requirement: !ruby/object:Gem::Requirement
78
72
  requirements:
79
73
  - - ">="
80
74
  - !ruby/object:Gem::Version
81
- version: '4.0'
82
- - - "<"
83
- - !ruby/object:Gem::Version
84
- version: '6.0'
75
+ version: '0'
85
76
  type: :runtime
86
77
  prerelease: false
87
78
  version_requirements: !ruby/object:Gem::Requirement
88
79
  requirements:
89
80
  - - ">="
90
81
  - !ruby/object:Gem::Version
91
- version: '4.0'
92
- - - "<"
93
- - !ruby/object:Gem::Version
94
- version: '6.0'
82
+ version: '0'
95
83
  - !ruby/object:Gem::Dependency
96
- name: gem_config
84
+ name: uri_template
97
85
  requirement: !ruby/object:Gem::Requirement
98
86
  requirements:
99
- - - "~>"
87
+ - - ">="
100
88
  - !ruby/object:Gem::Version
101
- version: 0.3.1
89
+ version: 0.5.2
102
90
  type: :runtime
103
91
  prerelease: false
104
92
  version_requirements: !ruby/object:Gem::Requirement
105
93
  requirements:
106
- - - "~>"
94
+ - - ">="
107
95
  - !ruby/object:Gem::Version
108
- version: 0.3.1
96
+ version: 0.5.2
109
97
  - !ruby/object:Gem::Dependency
110
- name: bundler
98
+ name: aptible-tasks
111
99
  requirement: !ruby/object:Gem::Requirement
112
100
  requirements:
113
- - - "~>"
101
+ - - ">="
114
102
  - !ruby/object:Gem::Version
115
- version: '1.3'
103
+ version: '0'
116
104
  type: :development
117
105
  prerelease: false
118
106
  version_requirements: !ruby/object:Gem::Requirement
119
107
  requirements:
120
- - - "~>"
108
+ - - ">="
121
109
  - !ruby/object:Gem::Version
122
- version: '1.3'
110
+ version: '0'
123
111
  - !ruby/object:Gem::Dependency
124
- name: aptible-tasks
112
+ name: bundler
125
113
  requirement: !ruby/object:Gem::Requirement
126
114
  requirements:
127
115
  - - ">="
@@ -135,7 +123,7 @@ dependencies:
135
123
  - !ruby/object:Gem::Version
136
124
  version: '0'
137
125
  - !ruby/object:Gem::Dependency
138
- name: rake
126
+ name: pry
139
127
  requirement: !ruby/object:Gem::Requirement
140
128
  requirements:
141
129
  - - ">="
@@ -149,33 +137,33 @@ dependencies:
149
137
  - !ruby/object:Gem::Version
150
138
  version: '0'
151
139
  - !ruby/object:Gem::Dependency
152
- name: rspec
140
+ name: rake
153
141
  requirement: !ruby/object:Gem::Requirement
154
142
  requirements:
155
- - - "~>"
143
+ - - "<"
156
144
  - !ruby/object:Gem::Version
157
- version: '2.0'
145
+ version: '11.0'
158
146
  type: :development
159
147
  prerelease: false
160
148
  version_requirements: !ruby/object:Gem::Requirement
161
149
  requirements:
162
- - - "~>"
150
+ - - "<"
163
151
  - !ruby/object:Gem::Version
164
- version: '2.0'
152
+ version: '11.0'
165
153
  - !ruby/object:Gem::Dependency
166
- name: pry
154
+ name: rspec
167
155
  requirement: !ruby/object:Gem::Requirement
168
156
  requirements:
169
- - - ">="
157
+ - - "~>"
170
158
  - !ruby/object:Gem::Version
171
- version: '0'
159
+ version: '2.0'
172
160
  type: :development
173
161
  prerelease: false
174
162
  version_requirements: !ruby/object:Gem::Requirement
175
163
  requirements:
176
- - - ">="
164
+ - - "~>"
177
165
  - !ruby/object:Gem::Version
178
- version: '0'
166
+ version: '2.0'
179
167
  - !ruby/object:Gem::Dependency
180
168
  name: webmock
181
169
  requirement: !ruby/object:Gem::Requirement
@@ -197,6 +185,7 @@ executables: []
197
185
  extensions: []
198
186
  extra_rdoc_files: []
199
187
  files:
188
+ - ".github/CODEOWNERS"
200
189
  - ".gitignore"
201
190
  - ".rspec"
202
191
  - ".rubocop.yml"
@@ -238,7 +227,7 @@ homepage: https://github.com/aptible/aptible-resource
238
227
  licenses:
239
228
  - MIT
240
229
  metadata: {}
241
- post_install_message:
230
+ post_install_message:
242
231
  rdoc_options: []
243
232
  require_paths:
244
233
  - lib
@@ -253,9 +242,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
242
  - !ruby/object:Gem::Version
254
243
  version: '0'
255
244
  requirements: []
256
- rubyforge_project:
257
- rubygems_version: 2.6.13
258
- signing_key:
245
+ rubygems_version: 3.0.3.1
246
+ signing_key:
259
247
  specification_version: 4
260
248
  summary: Foundation classes for Aptible resource server gems
261
249
  test_files: