Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create faraday deprecation class #1054

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should accept a version argument so we can proxy deprecated classes beyond 1.0. Probably not necessary for this PR on the 0.1x branch, but definitely once this gets ported to master.

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