restify 1.10.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,7 +36,7 @@ module Restify
36
36
 
37
37
  def inherit(uri, **kwargs)
38
38
  uri ||= self.uri
39
- Context.new uri, kwargs.merge(options)
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 !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
@@ -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
@@ -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 uri, opts
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
@@ -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
 
@@ -79,6 +79,7 @@ module Restify
79
79
 
80
80
  def convert_param(value)
81
81
  return value.to_param.to_s if value.respond_to?(:to_param)
82
+
82
83
  value
83
84
  end
84
85
 
@@ -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, {}).merge \
38
- 'Content-Type' => 'application/json'
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 ||= begin
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
@@ -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
@@ -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
@@ -3,7 +3,7 @@
3
3
  module Restify
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 10
6
+ MINOR = 15
7
7
  PATCH = 0
8
8
  STAGE = nil
9
9
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.').freeze
@@ -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://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
@@ -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