restify 1.14.0 → 1.15.0

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
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