From 6e746fceefe0d1f75535b40a62d846a6a95bf85f Mon Sep 17 00:00:00 2001 From: Bobby McDonald Date: Thu, 17 Oct 2019 09:46:28 -0400 Subject: [PATCH] Create Faraday Deprecate class. This implementation warns in stderr similar to Gem::Deprecate, but gives a semver instead of a date like the default Gem::Deprecate does. --- lib/faraday/deprecate.rb | 97 ++++++++++++++++++++++++++++++++ lib/faraday/error.rb | 111 ++++++++++++++++++++++++++----------- spec/faraday/error_spec.rb | 84 ++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 lib/faraday/deprecate.rb create mode 100644 spec/faraday/error_spec.rb diff --git a/lib/faraday/deprecate.rb b/lib/faraday/deprecate.rb new file mode 100644 index 00000000..53a19956 --- /dev/null +++ b/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 # + klass_name = superclass.to_s[/^#$/, 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 diff --git a/lib/faraday/error.rb b/lib/faraday/error.rb index 23fa6dc7..551c15af 100644 --- a/lib/faraday/error.rb +++ b/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 @@ -28,39 +32,80 @@ module Faraday 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 - class TimeoutError < ClientError - def initialize(ex = nil) - super(ex || "timeout") + # Raised by Faraday::Response::RaiseError in case of a 400 response. + class BadRequestError < ClientError + end + + # 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 - - [:ClientError, :ConnectionFailed, :ResourceNotFound, - :ParsingError, :TimeoutError, :SSLError, :RetriableResponse].each do |const| - Error.const_set(const, Faraday.const_get(const)) + # A unified client error for SSL errors. + class SSLError < Error end + # 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 diff --git a/spec/faraday/error_spec.rb b/spec/faraday/error_spec.rb new file mode 100644 index 00000000..9b3292ad --- /dev/null +++ b/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('#>') } + 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('#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('#>') } + 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('#>') } + 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