restify 1.10.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 +4 -4
- data/CHANGELOG.md +135 -53
- data/README.md +10 -12
- data/lib/restify/adapter/em.rb +6 -8
- data/lib/restify/adapter/pooled_em.rb +36 -40
- data/lib/restify/adapter/typhoeus.rb +69 -47
- data/lib/restify/context.rb +4 -4
- data/lib/restify/error.rb +48 -0
- data/lib/restify/global.rb +2 -2
- data/lib/restify/link.rb +4 -4
- data/lib/restify/processors/base.rb +2 -6
- data/lib/restify/processors/base/parsing.rb +2 -6
- data/lib/restify/promise.rb +2 -3
- data/lib/restify/relation.rb +1 -0
- data/lib/restify/request.rb +13 -5
- data/lib/restify/resource.rb +1 -1
- data/lib/restify/timeout.rb +2 -3
- data/lib/restify/version.rb +1 -1
- data/spec/restify/context_spec.rb +2 -2
- data/spec/restify/error_spec.rb +70 -0
- data/spec/restify/features/head_requests_spec.rb +5 -6
- data/spec/restify/features/request_bodies_spec.rb +83 -0
- data/spec/restify/features/request_headers_spec.rb +6 -7
- data/spec/restify/features/response_errors_spec.rb +127 -0
- data/spec/restify/global_spec.rb +2 -2
- data/spec/restify_spec.rb +50 -57
- data/spec/spec_helper.rb +12 -12
- data/spec/support/stub_server.rb +102 -0
- metadata +24 -13
- data/spec/restify/features/response_errors.rb +0 -79
data/lib/restify/context.rb
CHANGED
@@ -36,7 +36,7 @@ module Restify
|
|
36
36
|
|
37
37
|
def inherit(uri, **kwargs)
|
38
38
|
uri ||= self.uri
|
39
|
-
Context.new
|
39
|
+
Context.new(uri, **kwargs, **options)
|
40
40
|
end
|
41
41
|
|
42
42
|
def process(response)
|
@@ -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
|
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
@@ -30,10 +30,22 @@ module Restify
|
|
30
30
|
NotFound.new(response)
|
31
31
|
when 406
|
32
32
|
NotAcceptable.new(response)
|
33
|
+
when 410
|
34
|
+
Gone.new(response)
|
33
35
|
when 422
|
34
36
|
UnprocessableEntity.new(response)
|
37
|
+
when 429
|
38
|
+
TooManyRequests.new(response)
|
35
39
|
when 400...500
|
36
40
|
ClientError.new(response)
|
41
|
+
when 500
|
42
|
+
InternalServerError.new(response)
|
43
|
+
when 502
|
44
|
+
BadGateway.new(response)
|
45
|
+
when 503
|
46
|
+
ServiceUnavailable.new(response)
|
47
|
+
when 504
|
48
|
+
GatewayTimeout.new(response)
|
37
49
|
when 500...600
|
38
50
|
ServerError.new(response)
|
39
51
|
else
|
@@ -87,14 +99,50 @@ module Restify
|
|
87
99
|
# 5XX status code.
|
88
100
|
class ServerError < ResponseError; end
|
89
101
|
|
102
|
+
# A {GatewayError} is the common base class for 502, 503 and 504
|
103
|
+
# response codes often used by load balancers when upstream servers are
|
104
|
+
# failing or not available.
|
105
|
+
#
|
106
|
+
# This can be used to catch "common" gateway responses.
|
107
|
+
class GatewayError < ServerError; end
|
108
|
+
|
90
109
|
###
|
91
110
|
# CONCRETE SUBCLASSES FOR TYPICAL STATUS CODES
|
92
111
|
#
|
93
112
|
# This makes it easy to rescue specific expected error types.
|
94
113
|
|
95
114
|
class BadRequest < ClientError; end
|
115
|
+
|
96
116
|
class Unauthorized < ClientError; end
|
117
|
+
|
97
118
|
class NotFound < ClientError; end
|
119
|
+
|
98
120
|
class NotAcceptable < ClientError; end
|
121
|
+
|
122
|
+
class Gone < ClientError; end
|
123
|
+
|
99
124
|
class UnprocessableEntity < ClientError; end
|
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
|
+
|
141
|
+
class InternalServerError < ServerError; end
|
142
|
+
|
143
|
+
class BadGateway < GatewayError; end
|
144
|
+
|
145
|
+
class ServiceUnavailable < GatewayError; end
|
146
|
+
|
147
|
+
class GatewayTimeout < GatewayError; end
|
100
148
|
end
|
data/lib/restify/global.rb
CHANGED
@@ -39,9 +39,9 @@ module Restify
|
|
39
39
|
|
40
40
|
def resolve_context(uri, **opts)
|
41
41
|
if uri.is_a? Symbol
|
42
|
-
Restify::Registry.fetch(uri).inherit(nil, opts)
|
42
|
+
Restify::Registry.fetch(uri).inherit(nil, **opts)
|
43
43
|
else
|
44
|
-
Context.new
|
44
|
+
Context.new(uri, **opts)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
data/lib/restify/link.rb
CHANGED
@@ -25,10 +25,10 @@ module Restify
|
|
25
25
|
end
|
26
26
|
|
27
27
|
class << self
|
28
|
-
REGEXP_URI = /<[^>]*>\s
|
29
|
-
REGEXP_PAR = /;\s*\w+\s*=\s*/i
|
30
|
-
REGEXP_QUT = /"[^"]*"\s
|
31
|
-
REGEXP_ARG = /\w+\s*/i
|
28
|
+
REGEXP_URI = /<[^>]*>\s*/.freeze
|
29
|
+
REGEXP_PAR = /;\s*\w+\s*=\s*/i.freeze
|
30
|
+
REGEXP_QUT = /"[^"]*"\s*/.freeze
|
31
|
+
REGEXP_ARG = /\w+\s*/i.freeze
|
32
32
|
|
33
33
|
def parse(string)
|
34
34
|
scanner = StringScanner.new(string.strip)
|
@@ -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
|
data/lib/restify/promise.rb
CHANGED
@@ -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)
|
@@ -23,6 +21,7 @@ module Restify
|
|
23
21
|
|
24
22
|
super
|
25
23
|
raise t if incomplete?
|
24
|
+
|
26
25
|
self
|
27
26
|
end
|
28
27
|
|
data/lib/restify/relation.rb
CHANGED
data/lib/restify/request.rb
CHANGED
@@ -34,18 +34,26 @@ module Restify
|
|
34
34
|
@uri = opts.fetch(:uri) { raise ArgumentError.new ':uri required.' }
|
35
35
|
@data = opts.fetch(:data, nil)
|
36
36
|
@timeout = opts.fetch(:timeout, 300)
|
37
|
-
@headers = opts.fetch(:headers, {})
|
38
|
-
|
37
|
+
@headers = opts.fetch(:headers, {})
|
38
|
+
|
39
|
+
@headers['Content-Type'] ||= 'application/json' if json?
|
39
40
|
end
|
40
41
|
|
41
42
|
def body
|
42
|
-
@body ||=
|
43
|
-
JSON.dump(data) unless data.nil?
|
44
|
-
end
|
43
|
+
@body ||= json? ? JSON.dump(@data) : @data
|
45
44
|
end
|
46
45
|
|
47
46
|
def to_s
|
48
47
|
"#<#{self.class} #{method.upcase} #{uri}>"
|
49
48
|
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def json?
|
53
|
+
return false if @data.nil?
|
54
|
+
return false if @data.is_a? String
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
50
58
|
end
|
51
59
|
end
|
data/lib/restify/resource.rb
CHANGED
data/lib/restify/timeout.rb
CHANGED
@@ -51,9 +51,7 @@ module Restify
|
|
51
51
|
"Timeout must be an number but is #{value}"
|
52
52
|
end
|
53
53
|
|
54
|
-
|
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
|
@@ -61,6 +59,7 @@ module Restify
|
|
61
59
|
class << self
|
62
60
|
def new(timeout, *args)
|
63
61
|
return timeout if timeout.is_a?(self)
|
62
|
+
|
64
63
|
super
|
65
64
|
end
|
66
65
|
end
|
data/lib/restify/version.rb
CHANGED
@@ -47,7 +47,7 @@ describe Restify::Context do
|
|
47
47
|
|
48
48
|
context 'YAML' do
|
49
49
|
let(:dump) { YAML.dump(context) }
|
50
|
-
let(:load) { YAML.load(dump) } # rubocop:disable YAMLLoad
|
50
|
+
let(:load) { YAML.load(dump) } # rubocop:disable Security/YAMLLoad
|
51
51
|
|
52
52
|
subject { load }
|
53
53
|
|
@@ -56,7 +56,7 @@ describe Restify::Context do
|
|
56
56
|
|
57
57
|
context 'Marshall' do
|
58
58
|
let(:dump) { Marshal.dump(context) }
|
59
|
-
let(:load) { Marshal.load(dump) } # rubocop:disable MarshalLoad
|
59
|
+
let(:load) { Marshal.load(dump) } # rubocop:disable Security/MarshalLoad
|
60
60
|
|
61
61
|
subject { load }
|
62
62
|
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Restify::ResponseError do
|
6
|
+
let(:response) { double 'response' }
|
7
|
+
let(:message) { 'Error' }
|
8
|
+
let(:uri) { 'http://localhost' }
|
9
|
+
|
10
|
+
before do
|
11
|
+
allow(response).to receive(:uri).and_return(uri)
|
12
|
+
allow(response).to receive(:code).and_return(code)
|
13
|
+
allow(response).to receive(:message).and_return(message)
|
14
|
+
allow(response).to receive(:decoded_body).and_return({})
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.from_code' do
|
18
|
+
subject(:err) { described_class.from_code(response) }
|
19
|
+
|
20
|
+
context 'with 400 Bad Request' do
|
21
|
+
let(:code) { 400 }
|
22
|
+
it { is_expected.to be_a ::Restify::BadRequest }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with 401 Unauthorized' do
|
26
|
+
let(:code) { 401 }
|
27
|
+
it { is_expected.to be_a ::Restify::Unauthorized }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with 404 Unauthorized' do
|
31
|
+
let(:code) { 404 }
|
32
|
+
it { is_expected.to be_a ::Restify::NotFound }
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with 406 Not Acceptable' do
|
36
|
+
let(:code) { 406 }
|
37
|
+
it { is_expected.to be_a ::Restify::NotAcceptable }
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with 410 Gone' do
|
41
|
+
let(:code) { 410 }
|
42
|
+
it { is_expected.to be_a ::Restify::Gone }
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'with 422 Unprocessable Entity' do
|
46
|
+
let(:code) { 422 }
|
47
|
+
it { is_expected.to be_a ::Restify::UnprocessableEntity }
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with 500 Internal Server Error' do
|
51
|
+
let(:code) { 500 }
|
52
|
+
it { is_expected.to be_a ::Restify::InternalServerError }
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with 502 Bad Gateway' do
|
56
|
+
let(:code) { 502 }
|
57
|
+
it { is_expected.to be_a ::Restify::BadGateway }
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with 503 Service Unavailable' do
|
61
|
+
let(:code) { 503 }
|
62
|
+
it { is_expected.to be_a ::Restify::ServiceUnavailable }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'with 504 Gateway Timeout' do
|
66
|
+
let(:code) { 504 }
|
67
|
+
it { is_expected.to be_a ::Restify::GatewayTimeout }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -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://
|
7
|
+
stub_request(:head, 'http://stubserver/base')
|
8
8
|
.with(query: hash_including({}))
|
9
9
|
.to_return do
|
10
|
-
|
10
|
+
<<~HTTP
|
11
11
|
HTTP/1.1 200 OK
|
12
12
|
Content-Length: 333
|
13
|
-
|
14
|
-
|
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
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Restify do
|
6
|
+
let!(:request_stub) do
|
7
|
+
stub_request(:post, 'http://stubserver/base').to_return do
|
8
|
+
<<~HTTP
|
9
|
+
HTTP/1.1 200 OK
|
10
|
+
Link: <http://localhost:9292/other>; rel="neat"
|
11
|
+
HTTP
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'Request body' do
|
16
|
+
subject { Restify.new('http://localhost:9292/base').post(body, {}, {headers: headers}).value! }
|
17
|
+
let(:headers) { {} }
|
18
|
+
|
19
|
+
context 'with JSON-like data structures' do
|
20
|
+
let(:body) { {a: 'b', c: 'd'} }
|
21
|
+
|
22
|
+
it 'is serialized as JSON' do
|
23
|
+
subject
|
24
|
+
|
25
|
+
expect(
|
26
|
+
request_stub.with(body: '{"a":"b","c":"d"}')
|
27
|
+
).to have_been_requested
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'gets a JSON media type for free' do
|
31
|
+
subject
|
32
|
+
|
33
|
+
expect(
|
34
|
+
request_stub.with(headers: {'Content-Type' => 'application/json'})
|
35
|
+
).to have_been_requested
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with overridden media type' do
|
39
|
+
let(:headers) { {'Content-Type' => 'application/vnd.api+json'} }
|
40
|
+
|
41
|
+
it 'respects the override' do
|
42
|
+
subject
|
43
|
+
|
44
|
+
expect(
|
45
|
+
request_stub.with(headers: {'Content-Type' => 'application/vnd.api+json'})
|
46
|
+
).to have_been_requested
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with strings' do
|
52
|
+
let(:body) { 'a=b&c=d' }
|
53
|
+
|
54
|
+
it 'is sent as provided' do
|
55
|
+
subject
|
56
|
+
|
57
|
+
expect(
|
58
|
+
request_stub.with(body: 'a=b&c=d')
|
59
|
+
).to have_been_requested
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'does not get a JSON media type' do
|
63
|
+
subject
|
64
|
+
|
65
|
+
expect(
|
66
|
+
request_stub.with {|req| req.headers['Content-Type'] !~ /json/ }
|
67
|
+
).to have_been_requested
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'with overridden media type' do
|
71
|
+
let(:headers) { {'Content-Type' => 'application/text'} }
|
72
|
+
|
73
|
+
it 'respects the override' do
|
74
|
+
subject
|
75
|
+
|
76
|
+
expect(
|
77
|
+
request_stub.with(headers: {'Content-Type' => 'application/text'})
|
78
|
+
).to have_been_requested
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|