aptible-resource 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- 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: