restify 1.14.0 → 1.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a52b89b2f13d578bc3ee09444ed5a01e6e80597b13f8fed2a10135f482fde561
4
- data.tar.gz: 6e98b2d97468fe186a8500e7b88e72e8e174ef13840a76fcf152d2a8dcc57b4d
3
+ metadata.gz: 241fd43c8e40d2a0c2c839443df03106edf3c8e491cac9338277ff4327a3927f
4
+ data.tar.gz: d8eea64a1de42ee6eb4666feb510669b69482f24c72a0f5c883f3482143751ca
5
5
  SHA512:
6
- metadata.gz: 2c61229244ba3cfeb64d37f4328059e2f9c91f394dea86abe522cb2a4b2ce422ce107ca81a5cd5c55ec2d99b9c1409bd1f4fa6ca7ab13d86697e00dda02abb42
7
- data.tar.gz: 1dc873a2867f9cb91e55419c7bf831486b3ad86ec28186b7a2d8bac14653518a496f1cd8369a432fa1c74600b0ad91cd25c3094ffd392a9982dac2128a224484
6
+ metadata.gz: 603395d24a3ecad6d35b217ef2d154cd0982f9cd9181769b2a5aa949e134f0f9af1a15cc731f54a2d700e3bd1162b667240420e5669af20b48c66a1ba84fc2c5
7
+ data.tar.gz: 89e64dcd00dde5a7711922619380d2003db5f8c42a7cbc473b19678b6ed251134ffe4b4c94b86afaa7efa7dea030c6d43876e57c00c85ea76523f9f7029f6804
data/CHANGELOG.md CHANGED
@@ -18,6 +18,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
18
18
  ### Breaks
19
19
 
20
20
 
21
+ ## 1.15.0 - (2021-07-09)
22
+ ---
23
+
24
+ ### New
25
+ * Improve memory usage when running lots of requests with typhoeus adapter
26
+ * Use hydra for synchronous requests
27
+ * Increased thread stability of typhoeus adapter (new internal queuing mechanism)
28
+
29
+ ### Changes
30
+ * Use Ruby 2.5 as baseline for testing and linting
31
+ * Add Ruby 3.0 to automated testing
32
+ * Changed timing behavior for multiple requests due to new internal queuing mechanism for the typhoeus adapter
33
+
34
+
21
35
  ## 1.14.0 - (2020-12-15)
22
36
 
23
37
  ### New
@@ -128,12 +128,10 @@ module Restify
128
128
  return if EventMachine.reactor_running?
129
129
 
130
130
  Thread.new do
131
- begin
132
- EventMachine.run {}
133
- rescue StandardError => e
134
- puts "#{self.class} -> #{e}\n#{e.backtrace.join("\n")}"
135
- raise e
136
- end
131
+ EventMachine.run {}
132
+ rescue StandardError => e
133
+ puts "#{self.class} -> #{e}\n#{e.backtrace.join("\n")}"
134
+ raise e
137
135
  end
138
136
  end
139
137
  end
@@ -63,7 +63,7 @@ module Restify
63
63
  #
64
64
  def release(conn)
65
65
  @available.unshift(conn) if @available.size < @size
66
- @used -= 1 if @used > 0
66
+ @used -= 1 if @used.positive?
67
67
 
68
68
  logger.debug do
69
69
  "[#{conn.uri}] Released to pool (#{@available.size}/#{@used}/#{size})"
@@ -97,7 +97,7 @@ module Restify
97
97
  private
98
98
 
99
99
  def close(conn)
100
- @used -= 1 if @used > 0
100
+ @used -= 1 if @used.positive?
101
101
  @host[conn.uri.to_s] -= 1
102
102
 
103
103
  conn.close
@@ -212,39 +212,37 @@ module Restify
212
212
  end
213
213
 
214
214
  defer.callback do |conn|
215
- begin
216
- req = conn.send request.method.downcase,
217
- keepalive: true,
218
- redirects: 3,
219
- path: request.uri.normalized_path,
220
- query: request.uri.normalized_query,
221
- body: request.body,
222
- head: request.headers
223
-
224
- req.callback do
225
- writer.fulfill Response.new(
226
- request,
227
- req.last_effective_url,
228
- req.response_header.status,
229
- req.response_header,
230
- req.response
231
- )
232
-
233
- if req.response_header['CONNECTION'] == 'close'
234
- @pool.remove(conn)
235
- else
236
- @pool << conn
237
- end
238
- end
239
-
240
- req.errback do
215
+ req = conn.send request.method.downcase,
216
+ keepalive: true,
217
+ redirects: 3,
218
+ path: request.uri.normalized_path,
219
+ query: request.uri.normalized_query,
220
+ body: request.body,
221
+ head: request.headers
222
+
223
+ req.callback do
224
+ writer.fulfill Response.new(
225
+ request,
226
+ req.last_effective_url,
227
+ req.response_header.status,
228
+ req.response_header,
229
+ req.response
230
+ )
231
+
232
+ if req.response_header['CONNECTION'] == 'close'
241
233
  @pool.remove(conn)
242
- writer.reject(req.error)
234
+ else
235
+ @pool << conn
243
236
  end
244
- rescue Exception => e # rubocop:disable Lint/RescueException
237
+ end
238
+
239
+ req.errback do
245
240
  @pool.remove(conn)
246
- writer.reject(e)
241
+ writer.reject(req.error)
247
242
  end
243
+ rescue Exception => e # rubocop:disable Lint/RescueException
244
+ @pool.remove(conn)
245
+ writer.reject(e)
248
246
  end
249
247
  end
250
248
  end
@@ -261,12 +259,10 @@ module Restify
261
259
  return if EventMachine.reactor_running?
262
260
 
263
261
  Thread.new do
264
- begin
265
- EventMachine.run {}
266
- rescue StandardError => e
267
- logger.error(e)
268
- raise e
269
- end
262
+ EventMachine.run {}
263
+ rescue StandardError => e
264
+ logger.error(e)
265
+ raise e
270
266
  end
271
267
  end
272
268
  end
@@ -24,44 +24,39 @@ module Restify
24
24
  }.freeze
25
25
 
26
26
  def initialize(sync: false, options: {}, **kwargs)
27
- @sync = sync
28
27
  @hydra = ::Typhoeus::Hydra.new(**kwargs)
29
28
  @mutex = Mutex.new
30
- @signal = ConditionVariable.new
31
- @thread = nil
32
29
  @options = DEFAULT_OPTIONS.merge(options)
30
+ @queue = Queue.new
31
+ @sync = sync
32
+ @thread = nil
33
+
34
+ super()
33
35
  end
34
36
 
35
37
  def sync?
36
38
  @sync
37
39
  end
38
40
 
39
- # rubocop:disable Metrics/AbcSize
40
41
  # rubocop:disable Metrics/MethodLength
41
42
  def call_native(request, writer)
42
43
  req = convert(request, writer)
43
44
 
44
45
  if sync?
45
- req.run
46
+ @hydra.queue(req)
47
+ @hydra.run
46
48
  else
47
- @mutex.synchronize do
48
- debug 'request:add',
49
- tag: request.object_id,
50
- method: request.method.upcase,
51
- url: request.uri
52
-
53
- @hydra.queue(req)
54
- @hydra.dequeue_many
55
-
56
- thread.run unless thread.status
57
- end
49
+ debug 'request:add',
50
+ tag: request.object_id,
51
+ method: request.method.upcase,
52
+ url: request.uri
58
53
 
59
- debug 'request:signal', tag: request.object_id
54
+ @queue << convert(request, writer)
60
55
 
61
- @signal.signal
56
+ thread.run unless thread.status
62
57
  end
63
58
  end
64
- # rubocop:enable all
59
+ # rubocop:enable Metrics/MethodLength
65
60
 
66
61
  private
67
62
 
@@ -90,10 +85,15 @@ module Restify
90
85
  else
91
86
  writer.fulfill convert_back(response, request)
92
87
  end
88
+
89
+ # Add all newly queued requests to active hydra, e.g. requests
90
+ # queued in a completion callback.
91
+ dequeue_all
93
92
  end
94
93
  end
95
94
  end
96
- # rubocop:enable all
95
+ # rubocop:enable Metrics/MethodLength
96
+ # rubocop:enable Metrics/AbcSize
97
97
 
98
98
  def convert_back(response, request)
99
99
  uri = request.uri
@@ -117,38 +117,51 @@ module Restify
117
117
  # Recreate thread if nil or dead
118
118
  debug 'hydra:spawn'
119
119
 
120
- @thread = Thread.new { _loop }
120
+ @thread = Thread.new do
121
+ Thread.current.name = 'Restify/Typhoeus Background'
122
+ run
123
+ end
121
124
  end
122
125
 
123
126
  @thread
124
127
  end
125
128
 
126
- def _loop
127
- Thread.current.name = 'Restify/Typhoeus Background'
128
- loop { _run }
129
- end
129
+ # rubocop:disable Metrics/MethodLength
130
+ def run
131
+ runs = 0
132
+
133
+ loop do
134
+ if @queue.empty? && runs > 100
135
+ debug 'hydra:gc'
136
+ GC.start(full_mark: false, immediate_sweep: false)
137
+ runs = 0
138
+ end
130
139
 
131
- def _ongoing?
132
- @hydra.queued_requests.any? || @hydra.multi.easy_handles.any?
133
- end
140
+ debug 'hydra:pop'
134
141
 
135
- # rubocop:disable Metrics/MethodLength
136
- def _run
137
- debug 'hydra:run'
138
- @hydra.run while _ongoing?
139
- debug 'hydra:completed'
142
+ # Wait for next item and add all available requests to hydra
143
+ @hydra.queue @queue.pop
144
+ dequeue_all
140
145
 
141
- @mutex.synchronize do
142
- return if _ongoing?
146
+ debug 'hydra:run'
147
+ @hydra.run
148
+ runs += 1
149
+ debug 'hydra:completed'
150
+ rescue StandardError => e
151
+ logger.error(e)
152
+ end
153
+ ensure
154
+ debug 'hydra:exit'
155
+ end
156
+ # rubocop:enable Metrics/MethodLength
143
157
 
144
- debug 'hydra:pause'
145
- @signal.wait(@mutex, 60)
146
- debug 'hydra:resumed'
158
+ def dequeue_all
159
+ loop do
160
+ @hydra.queue @queue.pop(true)
161
+ rescue ThreadError
162
+ break
147
163
  end
148
- rescue StandardError => e
149
- logger.error(e)
150
164
  end
151
- # rubocop:enable all
152
165
 
153
166
  def _log_prefix
154
167
  "[#{object_id}/#{Thread.current.object_id}]"
@@ -59,10 +59,10 @@ module Restify
59
59
 
60
60
  ret = cache.call(request) {|req| adapter.call(req) }
61
61
  ret.then do |response|
62
- if !response.errored?
63
- process response
64
- else
62
+ if response.errored?
65
63
  raise ResponseError.from_code(response)
64
+ else
65
+ process response
66
66
  end
67
67
  end
68
68
  end
data/lib/restify/error.rb CHANGED
@@ -34,6 +34,8 @@ module Restify
34
34
  Gone.new(response)
35
35
  when 422
36
36
  UnprocessableEntity.new(response)
37
+ when 429
38
+ TooManyRequests.new(response)
37
39
  when 400...500
38
40
  ClientError.new(response)
39
41
  when 500
@@ -110,15 +112,37 @@ module Restify
110
112
  # This makes it easy to rescue specific expected error types.
111
113
 
112
114
  class BadRequest < ClientError; end
115
+
113
116
  class Unauthorized < ClientError; end
117
+
114
118
  class NotFound < ClientError; end
119
+
115
120
  class NotAcceptable < ClientError; end
121
+
116
122
  class Gone < ClientError; end
123
+
117
124
  class UnprocessableEntity < ClientError; end
118
125
 
126
+ class TooManyRequests < ClientError
127
+ def retry_after
128
+ case response.headers['RETRY_AFTER']
129
+ when /^\d+$/
130
+ DateTime.now + Rational(response.headers['RETRY_AFTER'].to_i, 86_400)
131
+ when String
132
+ begin
133
+ DateTime.httpdate response.headers['RETRY_AFTER']
134
+ rescue ArgumentError
135
+ nil
136
+ end
137
+ end
138
+ end
139
+ end
140
+
119
141
  class InternalServerError < ServerError; end
120
142
 
121
143
  class BadGateway < GatewayError; end
144
+
122
145
  class ServiceUnavailable < GatewayError; end
146
+
123
147
  class GatewayTimeout < GatewayError; end
124
148
  end
@@ -5,9 +5,7 @@ module Restify
5
5
  class Base
6
6
  extend Forwardable
7
7
 
8
- attr_reader :context
9
-
10
- attr_reader :response
8
+ attr_reader :context, :response
11
9
 
12
10
  def initialize(context, response)
13
11
  @context = context
@@ -18,9 +16,7 @@ module Restify
18
16
  @resource ||= begin
19
17
  resource = load
20
18
 
21
- unless resource.is_a? Restify::Resource
22
- resource = Resource.new context, response: response, data: resource
23
- end
19
+ resource = Resource.new context, response: response, data: resource unless resource.is_a? Restify::Resource
24
20
 
25
21
  resource._restify_response = response
26
22
  merge_relations! resource._restify_relations
@@ -24,9 +24,7 @@ module Restify
24
24
  data = object.each_with_object({}, &method(:parse_data))
25
25
  relations = object.each_with_object({}, &method(:parse_rels))
26
26
 
27
- if self.class.indifferent_access?
28
- data = with_indifferent_access(data)
29
- end
27
+ data = with_indifferent_access(data) if self.class.indifferent_access?
30
28
 
31
29
  Resource.new context,
32
30
  data: data,
@@ -56,9 +54,7 @@ module Restify
56
54
  return
57
55
  end
58
56
 
59
- if relations.key?(name) || pair[1].nil? || pair[1].to_s =~ /\A\w*\z/
60
- return
61
- end
57
+ return if relations.key?(name) || pair[1].nil? || pair[1].to_s =~ /\A\w*\z/
62
58
 
63
59
  relations[name] = pair[1].to_s
64
60
  end
@@ -11,9 +11,7 @@ module Restify
11
11
  # When dependencies were passed in, but none are left after flattening,
12
12
  # then we don't have to wait for explicit dependencies or resolution
13
13
  # through a writer.
14
- if !@task && @dependencies.empty? && dependencies.any?
15
- complete true, [], nil
16
- end
14
+ complete true, [], nil if !@task && @dependencies.empty? && dependencies.any?
17
15
  end
18
16
 
19
17
  def wait(timeout = nil)
@@ -97,7 +97,7 @@ module Restify
97
97
  text = {
98
98
  '@data' => data,
99
99
  '@relations' => @relations
100
- }.map {|k, v| k + '=' + v.inspect }.join(' ')
100
+ }.map {|k, v| "#{k}=#{v.inspect}" }.join(' ')
101
101
 
102
102
  "#<#{self.class} #{text}>"
103
103
  end
@@ -51,9 +51,7 @@ module Restify
51
51
  "Timeout must be an number but is #{value}"
52
52
  end
53
53
 
54
- unless value > 0
55
- raise ArgumentError.new "Timeout must be > 0 but is #{value.inspect}."
56
- end
54
+ raise ArgumentError.new "Timeout must be > 0 but is #{value.inspect}." unless value.positive?
57
55
 
58
56
  value
59
57
  end
@@ -3,7 +3,7 @@
3
3
  module Restify
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 14
6
+ MINOR = 15
7
7
  PATCH = 0
8
8
  STAGE = nil
9
9
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.').freeze
@@ -4,20 +4,19 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify do
6
6
  let!(:request_stub) do
7
- stub_request(:head, 'http://localhost/base')
7
+ stub_request(:head, 'http://stubserver/base')
8
8
  .with(query: hash_including({}))
9
9
  .to_return do
10
- <<-RESPONSE.gsub(/^ {8}/, '')
10
+ <<~HTTP
11
11
  HTTP/1.1 200 OK
12
12
  Content-Length: 333
13
- Transfer-Encoding: chunked
14
- Link: <http://localhost/other>; rel="neat"
15
- RESPONSE
13
+ Link: <http://localhost:9292/other>; rel="neat"
14
+ HTTP
16
15
  end
17
16
  end
18
17
 
19
18
  describe 'HEAD requests' do
20
- subject { Restify.new('http://localhost/base').head(params).value! }
19
+ subject { Restify.new('http://localhost:9292/base').head(params).value! }
21
20
  let(:params) { {} }
22
21
 
23
22
  it 'returns a resource with access to headers' do
@@ -4,19 +4,16 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify do
6
6
  let!(:request_stub) do
7
- stub_request(:post, 'http://localhost/base')
8
- .to_return do
9
- <<-RESPONSE.gsub(/^ {8}/, '')
7
+ stub_request(:post, 'http://stubserver/base').to_return do
8
+ <<~HTTP
10
9
  HTTP/1.1 200 OK
11
- Content-Length: 333
12
- Transfer-Encoding: chunked
13
- Link: <http://localhost/other>; rel="neat"
14
- RESPONSE
10
+ Link: <http://localhost:9292/other>; rel="neat"
11
+ HTTP
15
12
  end
16
13
  end
17
14
 
18
15
  describe 'Request body' do
19
- subject { Restify.new('http://localhost/base').post(body, {}, {headers: headers}).value! }
16
+ subject { Restify.new('http://localhost:9292/base').post(body, {}, {headers: headers}).value! }
20
17
  let(:headers) { {} }
21
18
 
22
19
  context 'with JSON-like data structures' do
@@ -66,18 +63,18 @@ describe Restify do
66
63
  subject
67
64
 
68
65
  expect(
69
- request_stub.with {|req| req.headers['Content-Type'].nil? }
66
+ request_stub.with {|req| req.headers['Content-Type'] !~ /json/ }
70
67
  ).to have_been_requested
71
68
  end
72
69
 
73
70
  context 'with overridden media type' do
74
- let(:headers) { {'Content-Type' => 'application/x-www-form-urlencoded'} }
71
+ let(:headers) { {'Content-Type' => 'application/text'} }
75
72
 
76
73
  it 'respects the override' do
77
74
  subject
78
75
 
79
76
  expect(
80
- request_stub.with(headers: {'Content-Type' => 'application/x-www-form-urlencoded'})
77
+ request_stub.with(headers: {'Content-Type' => 'application/text'})
81
78
  ).to have_been_requested
82
79
  end
83
80
  end
@@ -4,20 +4,19 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify do
6
6
  let!(:request_stub) do
7
- stub_request(:get, 'http://localhost/base').to_return do
8
- <<-RESPONSE.gsub(/^ {8}/, '')
7
+ stub_request(:get, "http://stubserver/base").to_return do
8
+ <<~HTTP
9
9
  HTTP/1.1 200 OK
10
10
  Content-Type: application/json
11
- Transfer-Encoding: chunked
12
- Link: <http://localhost/base>; rel="self"
11
+ Link: <http://localhost:9292/base>; rel="self"
13
12
 
14
13
  { "response": "success" }
15
- RESPONSE
14
+ HTTP
16
15
  end
17
16
  end
18
17
 
19
18
  context 'with request headers configured for a single request' do
20
- let(:context) { Restify.new('http://localhost/base') }
19
+ let(:context) { Restify.new('http://localhost:9292/base') }
21
20
 
22
21
  it 'sends the headers only for that request' do
23
22
  root = context.get(
@@ -37,7 +36,7 @@ describe Restify do
37
36
  context 'with request headers configured for context' do
38
37
  let(:context) do
39
38
  Restify.new(
40
- 'http://localhost/base',
39
+ 'http://localhost:9292/base',
41
40
  headers: {'Accept' => 'application/msgpack, application/json'}
42
41
  )
43
42
  end
@@ -4,21 +4,15 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify do
6
6
  let!(:request_stub) do
7
- stub_request(:get, 'http://localhost/base')
8
- .to_return do
9
- <<-RESPONSE.gsub(/^ {8}/, '')
10
- HTTP/1.1 #{http_status}
11
- Content-Length: 333
12
- Transfer-Encoding: chunked
13
- Link: <http://localhost/other>; rel="neat"
14
- RESPONSE
15
- end
7
+ stub_request(:get, 'http://stubserver/base')
8
+ .to_return(status: http_status, headers: headers)
16
9
  end
17
10
 
18
11
  let(:http_status) { '200 OK' }
12
+ let(:headers) { {} }
19
13
 
20
14
  describe 'Error handling' do
21
- subject(:request) { Restify.new('http://localhost/base').get.value! }
15
+ subject(:request) { Restify.new('http://localhost:9292/base').get.value! }
22
16
 
23
17
  context 'for 400 status codes' do
24
18
  let(:http_status) { '400 Bad Request' }
@@ -60,6 +54,60 @@ describe Restify do
60
54
  end
61
55
  end
62
56
 
57
+ context 'for 429 status codes' do
58
+ let(:http_status) { '429 Too Many Requests' }
59
+
60
+ it 'throws a TooManyRequests exception' do
61
+ expect { request }.to raise_error Restify::TooManyRequests
62
+ end
63
+
64
+ describe 'the exception' do
65
+ subject(:exception) do
66
+ exception = nil
67
+ begin
68
+ request
69
+ rescue Restify::TooManyRequests => e
70
+ exception = e
71
+ end
72
+ exception
73
+ end
74
+
75
+ context 'by default' do
76
+ it 'does not know when to retry again' do
77
+ expect(exception.retry_after).to be_nil
78
+ end
79
+ end
80
+
81
+ context 'with Retry-After header containing seconds' do
82
+ let(:headers) { {'Retry-After' => '120'} }
83
+
84
+ it 'determines the date correctly' do
85
+ now = DateTime.now
86
+ lower = now + Rational(119, 86_400)
87
+ upper = now + Rational(121, 86_400)
88
+
89
+ expect(exception.retry_after).to be_between(lower, upper)
90
+ end
91
+ end
92
+
93
+ context 'with Retry-After header containing HTTP date' do
94
+ let(:headers) { {'Retry-After' => 'Sun, 13 Mar 2033 13:03:33 GMT'} }
95
+
96
+ it 'parses the date correctly' do
97
+ expect(exception.retry_after.to_s).to eq '2033-03-13T13:03:33+00:00'
98
+ end
99
+ end
100
+
101
+ context 'with Retry-After header containing invalid date string' do
102
+ let(:headers) { {'Retry-After' => 'tomorrow 12:00:00'} }
103
+
104
+ it 'does not know when to retry again' do
105
+ expect(exception.retry_after).to be_nil
106
+ end
107
+ end
108
+ end
109
+ end
110
+
63
111
  context 'for any other 4xx status codes' do
64
112
  let(:http_status) { '415 Unsupported Media Type' }
65
113
 
@@ -29,7 +29,7 @@ describe Restify::Global do
29
29
  subject { global.new(name, **options) }
30
30
 
31
31
  it 'returns relation for stored registry item' do
32
- Restify::Registry.store name, uri, options
32
+ Restify::Registry.store(name, uri, **options)
33
33
 
34
34
  expect(subject).to be_a Restify::Relation
35
35
  expect(subject.pattern).to eq uri
data/spec/restify_spec.rb CHANGED
@@ -5,99 +5,94 @@ require 'spec_helper'
5
5
  describe Restify do
6
6
  context 'as a dynamic HATEOAS client' do
7
7
  before do
8
- stub_request(:get, 'http://localhost/base').to_return do
9
- <<-RESPONSE.gsub(/^ {10}/, '')
8
+ stub_request(:get, 'http://stubserver/base').to_return do
9
+ <<~HTTP
10
10
  HTTP/1.1 200 OK
11
11
  Content-Type: application/json
12
- Transfer-Encoding: chunked
13
- Link: <http://localhost/base/users{/id}>; rel="users"
14
- Link: <http://localhost/base/courses{/id}>; rel="courses"
12
+ Link: <http://localhost:9292/base/users{/id}>; rel="users"
13
+ Link: <http://localhost:9292/base/courses{/id}>; rel="courses"
15
14
 
16
15
  {
17
- "profile_url": "http://localhost/base/profile",
18
- "search_url": "http://localhost/base/search?q={query}",
16
+ "profile_url": "http://localhost:9292/base/profile",
17
+ "search_url": "http://localhost:9292/base/search?q={query}",
19
18
  "mirror_url": null
20
19
  }
21
- RESPONSE
20
+ HTTP
22
21
  end
23
22
 
24
- stub_request(:get, 'http://localhost/base/users').to_return do
25
- <<-RESPONSE.gsub(/^ {10}/, '')
23
+ stub_request(:get, 'http://stubserver/base/users')
24
+ .to_return do
25
+ <<~HTTP
26
26
  HTTP/1.1 200 OK
27
27
  Content-Type: application/json
28
- Transfer-Encoding: chunked
29
28
 
30
29
  [{
31
- "name": "John Smith",
32
- "url": "http://localhost/base/users/john.smith",
33
- "blurb_url": "http://localhost/base/users/john.smith/blurb",
34
- "languages": ["de", "en"]
35
- },
36
- {
37
- "name": "Jane Smith",
38
- "self_url": "http://localhost/base/user/jane.smith"
39
- }]
40
- RESPONSE
30
+ "name": "John Smith",
31
+ "url": "http://localhost:9292/base/users/john.smith",
32
+ "blurb_url": "http://localhost:9292/base/users/john.smith/blurb",
33
+ "languages": ["de", "en"]
34
+ },
35
+ {
36
+ "name": "Jane Smith",
37
+ "self_url": "http://localhost:9292/base/user/jane.smith"
38
+ }]
39
+ HTTP
41
40
  end
42
41
 
43
- stub_request(:post, 'http://localhost/base/users')
42
+ stub_request(:post, 'http://stubserver/base/users')
44
43
  .with(body: {})
45
44
  .to_return do
46
- <<-RESPONSE.gsub(/^ {12}/, '')
47
- HTTP/1.1 422 Unprocessable Entity
48
- Content-Type: application/json
49
- Transfer-Encoding: chunked
45
+ <<~HTTP
46
+ HTTP/1.1 422 Unprocessable Entity
47
+ Content-Type: application/json
50
48
 
51
- {"errors":{"name":["can't be blank"]}}
52
- RESPONSE
49
+ {"errors":{"name":["can't be blank"]}}
50
+ HTTP
53
51
  end
54
52
 
55
- stub_request(:post, 'http://localhost/base/users')
53
+ stub_request(:post, 'http://stubserver/base/users')
56
54
  .with(body: {name: 'John Smith'})
57
55
  .to_return do
58
- <<-RESPONSE.gsub(/^ {12}/, '')
59
- HTTP/1.1 201 Created
60
- Content-Type: application/json
61
- Location: http://localhost/base/users/john.smith
62
- Transfer-Encoding: chunked
63
-
64
- {
65
- "name": "John Smith",
66
- "url": "http://localhost/base/users/john.smith",
67
- "blurb_url": "http://localhost/base/users/john.smith/blurb",
68
- "languages": ["de", "en"]
69
- }
70
- RESPONSE
56
+ <<~HTTP
57
+ HTTP/1.1 201 Created
58
+ Content-Type: application/json
59
+ Location: http://localhost:9292/base/users/john.smith
60
+
61
+ {
62
+ "name": "John Smith",
63
+ "url": "http://localhost:9292/base/users/john.smith",
64
+ "blurb_url": "http://localhost:9292/base/users/john.smith/blurb",
65
+ "languages": ["de", "en"]
66
+ }
67
+ HTTP
71
68
  end
72
69
 
73
- stub_request(:get, 'http://localhost/base/users/john.smith')
70
+ stub_request(:get, 'http://stubserver/base/users/john.smith')
74
71
  .to_return do
75
- <<-RESPONSE.gsub(/^ {10}/, '')
72
+ <<~HTTP
76
73
  HTTP/1.1 200 OK
77
74
  Content-Type: application/json
78
- Link: <http://localhost/base/users/john.smith>; rel="self"
79
- Transfer-Encoding: chunked
75
+ Link: <http://localhost:9292/base/users/john.smith>; rel="self"
80
76
 
81
77
  {
82
78
  "name": "John Smith",
83
- "url": "http://localhost/base/users/john.smith"
79
+ "url": "http://localhost:9292/base/users/john.smith"
84
80
  }
85
- RESPONSE
81
+ HTTP
86
82
  end
87
83
 
88
- stub_request(:get, 'http://localhost/base/users/john.smith/blurb')
84
+ stub_request(:get, 'http://stubserver/base/users/john.smith/blurb')
89
85
  .to_return do
90
- <<-RESPONSE.gsub(/^ {10}/, '')
86
+ <<~HTTP
91
87
  HTTP/1.1 200 OK
92
88
  Content-Type: application/json
93
- Link: <http://localhost/base/users/john.smith>; rel="user"
94
- Transfer-Encoding: chunked
89
+ Link: <http://localhost:9292/base/users/john.smith>; rel="user"
95
90
 
96
91
  {
97
92
  "title": "Prof. Dr. John Smith",
98
93
  "image": "http://example.org/avatar.png"
99
94
  }
100
- RESPONSE
95
+ HTTP
101
96
  end
102
97
  end
103
98
 
@@ -107,7 +102,7 @@ describe Restify do
107
102
 
108
103
  # First request the entry resource usually the
109
104
  # root using GET and wait for it.
110
- root = Restify.new('http://localhost/base').get.value!
105
+ root = Restify.new('http://localhost:9292/base').get.value!
111
106
 
112
107
  # Therefore we need the `users` relations of our root
113
108
  # resource.
@@ -194,7 +189,7 @@ describe Restify do
194
189
  skip 'Seems to be impossible to detect EM scheduled fibers from within'
195
190
 
196
191
  EM.synchrony do
197
- root = Restify.new('http://localhost/base').get.value!
192
+ root = Restify.new('http://localhost:9292/base').get.value!
198
193
 
199
194
  users_relation = root.rel(:users)
200
195
 
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rspec'
4
- require 'webmock/rspec'
4
+ require 'rspec/collection_matchers'
5
5
 
6
6
  require 'simplecov'
7
7
  SimpleCov.start do
@@ -34,17 +34,13 @@ if ENV['ADAPTER']
34
34
  end
35
35
  end
36
36
 
37
- require 'webmock/rspec'
38
- require 'rspec/collection_matchers'
39
- require 'em-synchrony'
40
-
41
- Dir[File.expand_path('spec/support/**/*.rb')].sort.each {|f| require f }
37
+ require_relative 'support/stub_server.rb'
42
38
 
43
39
  RSpec.configure do |config|
44
40
  config.order = 'random'
45
41
 
46
42
  config.before(:suite) do
47
- ::Restify::Timeout.default_timeout = 2
43
+ ::Restify::Timeout.default_timeout = 0.1
48
44
  end
49
45
 
50
46
  config.before(:each) do
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma'
4
+ require 'rack'
5
+ require 'webmock'
6
+ require 'webmock/rspec/matchers'
7
+
8
+ module Stub
9
+ # This Rack application matches the request received from rack against the
10
+ # webmock stub database and returns the response.
11
+ #
12
+ # A custom server name is used to
13
+ # 1) has a stable name without a dynamic port for easier `#stub_request`
14
+ # calls, and
15
+ # 2) to ensure no actual request is intercepted (they are send to
16
+ # `localhost:<port>`).
17
+ #
18
+ # If no stub is found a special HTTP 599 error code will be returned.
19
+ class Handler
20
+ def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
21
+ signature = WebMock::RequestSignature.new(
22
+ env['REQUEST_METHOD'].downcase,
23
+ "http://stubserver#{env['REQUEST_URI']}"
24
+ )
25
+
26
+ # Extract request headers from rack env. Most header should start with
27
+ # `HTTP_` but at least content type is present as `CONTENT_TYPE`.
28
+ headers = {}
29
+ env.each_pair do |key, value|
30
+ case key
31
+ when /^HTTP_(.*)$/, /^(CONTENT_.*)$/
32
+ headers[Regexp.last_match(1)] = value
33
+ end
34
+ end
35
+
36
+ # Read request body from socket into string
37
+ signature.body = env['rack.input'].read
38
+ signature.headers = headers
39
+
40
+ WebMock::RequestRegistry.instance.requested_signatures.put(signature)
41
+ response = ::WebMock::StubRegistry.instance.response_for_request(signature)
42
+
43
+ # If no stub matched `nil` is returned.
44
+ if response
45
+ status = response.status
46
+ status = status.to_s.split(' ', 2) unless status.is_a?(Array)
47
+ status = Integer(status[0])
48
+
49
+ [status, response.headers || {}, [response.body.to_s]]
50
+ else
51
+ # Return special HTTP 599 with the error message that would normally
52
+ # appear on missing stubs.
53
+ [599, {}, [WebMock::NetConnectNotAllowedError.new(signature).message]]
54
+ end
55
+ end
56
+ end
57
+
58
+ class Exception < ::StandardError; end
59
+
60
+ # Inject into base adapter to have HTTP 599 (missing stub) error raised as an
61
+ # extra exception, not just a server error.
62
+ module Patch
63
+ def call(request)
64
+ super.then do |response|
65
+ next response unless response.code == 599
66
+
67
+ raise ::Stub::Exception.new(response.body)
68
+ end
69
+ end
70
+
71
+ ::Restify::Adapter::Base.prepend(self)
72
+ end
73
+
74
+ class << self
75
+ def start_server!
76
+ @server = ::Puma::Server.new(Handler.new)
77
+ @server.add_tcp_listener('localhost', 9292)
78
+
79
+ Thread.new do
80
+ @server.run
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ RSpec.configure do |config|
87
+ config.include WebMock::API
88
+ config.include WebMock::Matchers
89
+
90
+ config.before(:suite) do
91
+ Stub.start_server!
92
+
93
+ # Net::HTTP adapter must be enabled, otherwise webmock fails to create mock
94
+ # responses from raw strings.
95
+ WebMock.disable!(except: %i[net_http])
96
+ end
97
+
98
+ config.around(:each) do |example|
99
+ example.run
100
+ WebMock.reset!
101
+ end
102
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restify
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.14.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Graichen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-15 00:00:00.000000000 Z
11
+ date: 2021-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -209,6 +209,7 @@ files:
209
209
  - spec/restify/timeout_spec.rb
210
210
  - spec/restify_spec.rb
211
211
  - spec/spec_helper.rb
212
+ - spec/support/stub_server.rb
212
213
  homepage: https://github.com/jgraichen/restify
213
214
  licenses:
214
215
  - LGPL-3.0+
@@ -221,14 +222,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
222
  requirements:
222
223
  - - ">="
223
224
  - !ruby/object:Gem::Version
224
- version: '0'
225
+ version: 2.5.0
225
226
  required_rubygems_version: !ruby/object:Gem::Requirement
226
227
  requirements:
227
228
  - - ">="
228
229
  - !ruby/object:Gem::Version
229
230
  version: '0'
230
231
  requirements: []
231
- rubygems_version: 3.0.8
232
+ rubygems_version: 3.2.22
232
233
  signing_key:
233
234
  specification_version: 4
234
235
  summary: An experimental hypermedia REST client.
@@ -252,3 +253,4 @@ test_files:
252
253
  - spec/restify/timeout_spec.rb
253
254
  - spec/restify_spec.rb
254
255
  - spec/spec_helper.rb
256
+ - spec/support/stub_server.rb