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