Skip to content

Commit

Permalink
Merge pull request #1054 from BobbyMcWho/create-faraday-deprecation-c…
Browse files Browse the repository at this point in the history
…lass

Create faraday deprecation class
  • Loading branch information
technoweenie committed Oct 17, 2019
2 parents bee4af0 + 6af3809 commit a4e9bc2
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 33 deletions.
97 changes: 97 additions & 0 deletions lib/faraday/deprecate.rb
@@ -0,0 +1,97 @@
# frozen_string_literal: true

module Faraday
# @param new_klass [Class] new Klass to use
#
# @return [Class] A modified version of new_klass that warns on
# usage about deprecation.
# @see Faraday::Deprecate
module DeprecatedClass
def self.proxy_class(new_klass)
Class.new(new_klass).tap do |k|
class << k
extend Faraday::Deprecate
# Make this more human readable than #<Class:Faraday::ClientError>
klass_name = superclass.to_s[/^#<Class:(\w*::\w*)>$/, 1]
deprecate :new, "#{klass_name}.new", '1.0'
deprecate :inherited, klass_name, '1.0'
end
end
end
end

# Deprecation using semver instead of date, based on Gem::Deprecate
# Provides a single method +deprecate+ to be used to declare when
# something is going away.
#
# class Legacy
# def self.klass_method
# # ...
# end
#
# def instance_method
# # ...
# end
#
# extend Faraday::Deprecate
# deprecate :instance_method, "X.z", '1.0'
#
# class << self
# extend Faraday::Deprecate
# deprecate :klass_method, :none, '1.0'
# end
# end
module Deprecate
def self.skip # :nodoc:
@skip ||= false
end

def self.skip=(value) # :nodoc:
@skip = value
end

# Temporarily turn off warnings. Intended for tests only.
def skip_during
original = Faraday::Deprecate.skip
Faraday::Deprecate.skip, = true
yield
ensure
Faraday::Deprecate.skip = original
end

# Simple deprecation method that deprecates +name+ by wrapping it up
# in a dummy method. It warns on each call to the dummy method
# telling the user of +repl+ (unless +repl+ is :none) and the
# semver that it is planned to go away.
# @param name [Symbol] the method symbol to deprecate
# @param repl [#to_s, :none] the replacement to use, when `:none` it will
# alert the user that no replacemtent is present.
# @param ver [String] the semver the method will be removed.
def deprecate(name, repl, ver)
class_eval do
old = "_deprecated_#{name}"
alias_method old, name
define_method name do |*args, &block|
mod = is_a? Module
target = mod ? "#{self}." : "#{self.class}#"
target_message = if name == :inherited
"Inheriting #{self}"
else
"#{target}#{name}"
end

msg = [
"NOTE: #{target_message} is deprecated",
repl == :none ? ' with no replacement' : "; use #{repl} instead. ",
"It will be removed in or after version #{Gem::Version.new(ver)}",
"\n#{target}#{name} called from #{Gem.location_of_caller.join(':')}"
]
warn "#{msg.join}." unless Faraday::Deprecate.skip
send old, *args, &block
end
end
end

module_function :deprecate, :skip_during
end
end
109 changes: 77 additions & 32 deletions lib/faraday/error.rb
@@ -1,21 +1,25 @@
module Faraday
class Error < StandardError; end
# frozen_string_literal: true

class ClientError < Error
require 'faraday/deprecate'

# Faraday namespace.
module Faraday
# Faraday error base class.
class Error < StandardError
attr_reader :response, :wrapped_exception

def initialize(ex, response = nil)
def initialize(exc, response = nil)
@wrapped_exception = nil
@response = response

if ex.respond_to?(:backtrace)
super(ex.message)
@wrapped_exception = ex
elsif ex.respond_to?(:each_key)
super("the server responded with status #{ex[:status]}")
@response = ex
if exc.respond_to?(:backtrace)
super(exc.message)
@wrapped_exception = exc
elsif exc.respond_to?(:each_key)
super("the server responded with status #{exc[:status]}")
@response = exc
else
super(ex.to_s)
super(exc.to_s)
end
end

Expand All @@ -28,39 +32,80 @@ def backtrace
end

def inspect
inner = ''
if @wrapped_exception
inner << " wrapped=#{@wrapped_exception.inspect}"
end
if @response
inner << " response=#{@response.inspect}"
end
if inner.empty?
inner << " #{super}"
end
inner = +''
inner << " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception
inner << " response=#{@response.inspect}" if @response
inner << " #{super}" if inner.empty?
%(#<#{self.class}#{inner}>)
end
end

class ConnectionFailed < ClientError; end
class ResourceNotFound < ClientError; end
class ParsingError < ClientError; end
# Faraday client error class. Represents 4xx status responses.
class ClientError < Error
end

# Raised by Faraday::Response::RaiseError in case of a 400 response.
class BadRequestError < ClientError
end

class TimeoutError < ClientError
def initialize(ex = nil)
super(ex || "timeout")
# Raised by Faraday::Response::RaiseError in case of a 401 response.
class UnauthorizedError < ClientError
end

# Raised by Faraday::Response::RaiseError in case of a 403 response.
class ForbiddenError < ClientError
end

# Raised by Faraday::Response::RaiseError in case of a 404 response.
class ResourceNotFound < ClientError
end

# Raised by Faraday::Response::RaiseError in case of a 407 response.
class ProxyAuthError < ClientError
end

# Raised by Faraday::Response::RaiseError in case of a 409 response.
class ConflictError < ClientError
end

# Raised by Faraday::Response::RaiseError in case of a 422 response.
class UnprocessableEntityError < ClientError
end

# Faraday server error class. Represents 5xx status responses.
class ServerError < Error
end

# A unified client error for timeouts.
class TimeoutError < ServerError
def initialize(exc = 'timeout', response = nil)
super(exc, response)
end
end

class SSLError < ClientError
# A unified error for failed connections.
class ConnectionFailed < Error
end

class RetriableResponse < ClientError; end
# A unified client error for SSL errors.
class SSLError < Error
end

[:ClientError, :ConnectionFailed, :ResourceNotFound,
:ParsingError, :TimeoutError, :SSLError, :RetriableResponse].each do |const|
Error.const_set(const, Faraday.const_get(const))
# Raised by FaradayMiddleware::ResponseMiddleware
class ParsingError < Error
end

# Exception used to control the Retry middleware.
#
# @see Faraday::Request::Retry
class RetriableResponse < Error
end

%i[ClientError ConnectionFailed ResourceNotFound
ParsingError TimeoutError SSLError RetriableResponse].each do |const|
Error.const_set(
const,
DeprecatedClass.proxy_class(Faraday.const_get(const))
)
end
end
84 changes: 84 additions & 0 deletions spec/faraday/error_spec.rb
@@ -0,0 +1,84 @@
# frozen_string_literal: true

RSpec.describe Faraday::ClientError do
describe '.initialize' do
subject { described_class.new(exception, response) }
let(:response) { nil }

context 'with exception only' do
let(:exception) { RuntimeError.new('test') }

it { expect(subject.wrapped_exception).to eq(exception) }
it { expect(subject.response).to be_nil }
it { expect(subject.message).to eq(exception.message) }
it { expect(subject.backtrace).to eq(exception.backtrace) }
it { expect(subject.inspect).to eq('#<Faraday::ClientError wrapped=#<RuntimeError: test>>') }
end

context 'with response hash' do
let(:exception) { { status: 400 } }

it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 400') }
it { expect(subject.inspect).to eq('#<Faraday::ClientError response={:status=>400}>') }
end

context 'with string' do
let(:exception) { 'custom message' }

it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to be_nil }
it { expect(subject.message).to eq('custom message') }
it { expect(subject.inspect).to eq('#<Faraday::ClientError #<Faraday::ClientError: custom message>>') }
end

context 'with anything else #to_s' do
let(:exception) { %w[error1 error2] }

it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to be_nil }
it { expect(subject.message).to eq('["error1", "error2"]') }
it { expect(subject.inspect).to eq('#<Faraday::ClientError #<Faraday::ClientError: ["error1", "error2"]>>') }
end

context 'maintains backward-compatibility until 1.0' do
it 'does not raise an error for error-namespaced classes but prints an error message' do
error_message, error = with_warn_squelching { Faraday::Error::ClientError.new('foo') }

expect(error).to be_a Faraday::ClientError
expect(error_message).to match(
Regexp.new(
'NOTE: Faraday::Error::ClientError.new is deprecated; '\
'use Faraday::ClientError.new instead. It will be removed in or after version 1.0'
)
)
end

it 'does not raise an error for inherited error-namespaced classes but prints an error message' do
error_message, = with_warn_squelching { class E < Faraday::Error::ClientError; end }

expect(error_message).to match(
Regexp.new(
'NOTE: Inheriting Faraday::Error::ClientError is deprecated; '\
'use Faraday::ClientError instead. It will be removed in or after version 1.0'
)
)
end

it 'allows backward-compatible class to be subclassed' do
expect { class CustomError < Faraday::Error::ClientError; end }.not_to raise_error
end
end

def with_warn_squelching
stderr_catcher = StringIO.new
original_stderr = $stderr
$stderr = stderr_catcher
result = yield if block_given?
[stderr_catcher.tap(&:rewind).string, result]
ensure
$stderr = original_stderr
end
end
end
2 changes: 1 addition & 1 deletion test/adapters/rack_test.rb
Expand Up @@ -26,7 +26,7 @@ def test_timeout
conn = create_connection(:request => {:timeout => 1, :open_timeout => 1})
begin
conn.get '/slow'
rescue Faraday::Error::ClientError
rescue Faraday::Error::TimeoutError
end
end

Expand Down

0 comments on commit a4e9bc2

Please sign in to comment.