From 843ea882199c3fb0e05508873c1fc527acbe9238 Mon Sep 17 00:00:00 2001 From: Brandur Date: Fri, 13 Jan 2017 14:10:05 -0800 Subject: [PATCH] Add a StripeResponse to objects created by API calls --- lib/stripe.rb | 94 +++++++++++----------------- lib/stripe/account.rb | 8 +-- lib/stripe/api_operations/create.rb | 4 +- lib/stripe/api_operations/delete.rb | 4 +- lib/stripe/api_operations/list.rb | 5 +- lib/stripe/api_operations/request.rb | 4 +- lib/stripe/api_operations/save.rb | 10 ++- lib/stripe/api_resource.rb | 11 +++- lib/stripe/bank_account.rb | 4 +- lib/stripe/charge.rb | 24 +++---- lib/stripe/customer.rb | 14 ++--- lib/stripe/dispute.rb | 4 +- lib/stripe/errors.rb | 16 ++++- lib/stripe/invoice.rb | 8 +-- lib/stripe/list_object.rb | 8 ++- lib/stripe/order.rb | 8 +-- lib/stripe/source.rb | 4 +- lib/stripe/stripe_response.rb | 34 ++++++++++ lib/stripe/transfer.rb | 4 +- lib/stripe/util.rb | 21 +++++-- 20 files changed, 167 insertions(+), 122 deletions(-) create mode 100644 lib/stripe/stripe_response.rb diff --git a/lib/stripe.rb b/lib/stripe.rb index 87554ebd..beac6b6c 100644 --- a/lib/stripe.rb +++ b/lib/stripe.rb @@ -23,6 +23,7 @@ require 'stripe/api_operations/request' require 'stripe/errors' require 'stripe/util' require 'stripe/stripe_object' +require 'stripe/stripe_response' require 'stripe/list_object' require 'stripe/api_resource' require 'stripe/singleton_api_resource' @@ -203,9 +204,15 @@ module Stripe :method => method, :open_timeout => open_timeout, :payload => payload, :url => url, :timeout => read_timeout) - response = execute_request_with_rescues(request_opts, api_base_url) + http_resp = execute_request_with_rescues(request_opts, api_base_url) - [parse(response), api_key] + begin + resp = StripeResponse.from_rest_client_response(http_resp) + rescue JSON::ParserError + raise general_api_error(http_resp.code, http_resp.body) + end + + [resp, api_key] end def self.max_network_retries @@ -218,20 +225,6 @@ module Stripe private - def self.api_error(error, resp, error_obj) - APIError.new(error[:message], resp.code, resp.body, error_obj, resp.headers) - end - - def self.authentication_error(error, resp, error_obj) - AuthenticationError.new(error[:message], resp.code, resp.body, error_obj, - resp.headers) - end - - def self.card_error(error, resp, error_obj) - CardError.new(error[:message], error[:param], error[:code], - resp.code, resp.body, error_obj, resp.headers) - end - def self.execute_request(opts) RestClient::Request.execute(opts) end @@ -274,9 +267,9 @@ module Stripe response end - def self.general_api_error(rcode, rbody) - APIError.new("Invalid response object from API: #{rbody.inspect} " + - "(HTTP response code was #{rcode})", rcode, rbody) + def self.general_api_error(status, body) + APIError.new("Invalid response object from API: #{body.inspect} " + + "(HTTP response code was #{status})", status, body) end def self.get_uname @@ -310,32 +303,45 @@ module Stripe "uname lookup failed" end - def self.handle_api_error(resp) + def self.handle_api_error(http_resp) begin - error_obj = JSON.parse(resp.body) - error_obj = Util.symbolize_names(error_obj) - error = error_obj[:error] + resp = StripeResponse.from_rest_client_response(http_resp) + error = resp.data[:error] raise StripeError.new unless error && error.is_a?(Hash) rescue JSON::ParserError, StripeError - raise general_api_error(resp.code, resp.body) + raise general_api_error(http_resp.code, http_resp.body) end - case resp.code + case resp.http_status when 400, 404 - raise invalid_request_error(error, resp, error_obj) + error = InvalidRequestError.new( + error[:message], error[:param], + resp.http_status, resp.http_body, resp.data, resp.http_headers) when 401 - raise authentication_error(error, resp, error_obj) + error = AuthenticationError.new( + error[:message], + resp.http_status, resp.http_body, resp.data, resp.http_headers) when 402 - raise card_error(error, resp, error_obj) + error = CardError.new( + error[:message], error[:param], error[:code], + resp.http_status, resp.http_body, resp.data, resp.http_headers) when 403 - raise permission_error(error, resp, error_obj) + error = PermissionError.new( + error[:message], + resp.http_status, resp.http_body, resp.data, resp.http_headers) when 429 - raise rate_limit_error(error, resp, error_obj) + error = RateLimitError.new( + error[:message], + resp.http_status, resp.http_body, resp.data, resp.http_headers) else - raise api_error(error, resp, error_obj) + error = APIError.new( + error[:message], + resp.http_status, resp.http_body, resp.data, resp.http_headers) end + error.response = resp + raise(error) end def self.handle_restclient_error(e, request_opts, retry_count, api_base_url=nil) @@ -383,32 +389,6 @@ module Stripe raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})") end - def self.invalid_request_error(error, resp, error_obj) - InvalidRequestError.new(error[:message], error[:param], resp.code, - resp.body, error_obj, resp.headers) - end - - def self.parse(response) - begin - # Would use :symbolize_names => true, but apparently there is - # some library out there that makes symbolize_names not work. - response = JSON.parse(response.body) - rescue JSON::ParserError - raise general_api_error(response.code, response.body) - end - - Util.symbolize_names(response) - end - - def self.permission_error(error, resp, error_obj) - PermissionError.new(error[:message], resp.code, resp.body, error_obj, resp.headers) - end - - def self.rate_limit_error(error, resp, error_obj) - RateLimitError.new(error[:message], resp.code, resp.body, error_obj, - resp.headers) - end - def self.request_headers(api_key, method) headers = { 'User-Agent' => "Stripe/v1 RubyBindings/#{Stripe::VERSION}", diff --git a/lib/stripe/account.rb b/lib/stripe/account.rb index e759c390..ac3ab0d5 100644 --- a/lib/stripe/account.rb +++ b/lib/stripe/account.rb @@ -37,8 +37,8 @@ module Stripe def reject(params={}, opts={}) opts = Util.normalize_opts(opts) - response, opts = request(:post, resource_url + '/reject', params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, resource_url + '/reject', params, opts) + initialize_from(response.data, opts) end # Somewhat unfortunately, we attempt to do a special encoding trick when @@ -93,9 +93,9 @@ module Stripe def deauthorize(client_id, opts={}) opts = {:api_base => Stripe.connect_base}.merge(Util.normalize_opts(opts)) - response, opts = request(:post, '/oauth/deauthorize', { 'client_id' => client_id, 'stripe_user_id' => self.id }, opts) + resp, opts = request(:post, '/oauth/deauthorize', { 'client_id' => client_id, 'stripe_user_id' => self.id }, opts) opts.delete(:api_base) # the api_base here is a one-off, don't persist it - Util.convert_to_stripe_object(response, opts) + Util.convert_to_stripe_object(resp.data, opts, response: resp) end ARGUMENT_NOT_PROVIDED = Object.new diff --git a/lib/stripe/api_operations/create.rb b/lib/stripe/api_operations/create.rb index a63f9b17..f0061941 100644 --- a/lib/stripe/api_operations/create.rb +++ b/lib/stripe/api_operations/create.rb @@ -2,8 +2,8 @@ module Stripe module APIOperations module Create def create(params={}, opts={}) - response, opts = request(:post, resource_url, params, opts) - Util.convert_to_stripe_object(response, opts) + resp, opts = request(:post, resource_url, params, opts) + Util.convert_to_stripe_object(resp.data, opts, response: resp) end end end diff --git a/lib/stripe/api_operations/delete.rb b/lib/stripe/api_operations/delete.rb index 73157ee1..aa121d69 100644 --- a/lib/stripe/api_operations/delete.rb +++ b/lib/stripe/api_operations/delete.rb @@ -3,8 +3,8 @@ module Stripe module Delete def delete(params={}, opts={}) opts = Util.normalize_opts(opts) - response, opts = request(:delete, resource_url, params, opts) - initialize_from(response, opts) + self.response, opts = request(:delete, resource_url, params, opts) + initialize_from(response.data, opts) end end end diff --git a/lib/stripe/api_operations/list.rb b/lib/stripe/api_operations/list.rb index 81a3c4b1..6f93520a 100644 --- a/lib/stripe/api_operations/list.rb +++ b/lib/stripe/api_operations/list.rb @@ -4,8 +4,9 @@ module Stripe def list(filters={}, opts={}) opts = Util.normalize_opts(opts) - response, opts = request(:get, resource_url, filters, opts) - obj = ListObject.construct_from(response, opts) + resp, opts = request(:get, resource_url, filters, opts) + obj = ListObject.construct_from(resp.data, opts) + obj.response = resp # set filters so that we can fetch the same limit, expansions, and # predicates when accessing the next and previous pages diff --git a/lib/stripe/api_operations/request.rb b/lib/stripe/api_operations/request.rb index 4531256c..60f5fa48 100644 --- a/lib/stripe/api_operations/request.rb +++ b/lib/stripe/api_operations/request.rb @@ -12,7 +12,7 @@ module Stripe api_base = headers.delete(:api_base) # Assume all remaining opts must be headers - response, opts[:api_key] = Stripe.request(method, url, api_key, params, headers, api_base) + resp, opts[:api_key] = Stripe.request(method, url, api_key, params, headers, api_base) # Hash#select returns an array before 1.9 opts_to_persist = {} @@ -22,7 +22,7 @@ module Stripe end end - [response, opts_to_persist] + [resp, opts_to_persist] end end diff --git a/lib/stripe/api_operations/save.rb b/lib/stripe/api_operations/save.rb index cd93c57d..eb3b3861 100644 --- a/lib/stripe/api_operations/save.rb +++ b/lib/stripe/api_operations/save.rb @@ -21,8 +21,8 @@ module Stripe end end - response, opts = request(:post, "#{resource_url}/#{id}", params, opts) - Util.convert_to_stripe_object(response, opts) + resp, opts = request(:post, "#{resource_url}/#{id}", params, opts) + Util.convert_to_stripe_object(resp.data, opts, response: resp) end end @@ -57,10 +57,8 @@ module Stripe # generated a uri for this object with an identifier baked in values.delete(:id) - response, opts = request(:post, save_url, values, opts) - initialize_from(response, opts) - - self + self.response, opts = request(:post, save_url, values, opts) + initialize_from(response.data, opts) end def self.included(base) diff --git a/lib/stripe/api_resource.rb b/lib/stripe/api_resource.rb index d2443b1f..0e35f66d 100644 --- a/lib/stripe/api_resource.rb +++ b/lib/stripe/api_resource.rb @@ -2,6 +2,13 @@ module Stripe class APIResource < StripeObject include Stripe::APIOperations::Request + # Response contains a structure that has some basic information about the + # response that conveyed the API resource. + # + # Note that this is only set on the top-level API resource returned by an + # API operation. It will hold a nil value on all others. + attr_accessor :response + # A flag that can be set a behavior that will cause this resource to be # encoded and sent up along with an update of its parent resource. This is # usually not desirable because resources are updated individually on their @@ -54,8 +61,8 @@ module Stripe end def refresh - response, opts = request(:get, resource_url, @retrieve_params) - initialize_from(response, opts) + self.response, opts = request(:get, resource_url, @retrieve_params) + initialize_from(response.data, opts) end def self.retrieve(id, opts={}) diff --git a/lib/stripe/bank_account.rb b/lib/stripe/bank_account.rb index 2ae4c67f..386a43ad 100644 --- a/lib/stripe/bank_account.rb +++ b/lib/stripe/bank_account.rb @@ -5,8 +5,8 @@ module Stripe extend Stripe::APIOperations::List def verify(params={}, opts={}) - response, opts = request(:post, resource_url + '/verify', params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, resource_url + '/verify', params, opts) + initialize_from(response.data, opts) end def resource_url diff --git a/lib/stripe/charge.rb b/lib/stripe/charge.rb index f3c1b4a4..00493348 100644 --- a/lib/stripe/charge.rb +++ b/lib/stripe/charge.rb @@ -20,41 +20,41 @@ module Stripe # from the server self.refresh else - response, opts = request(:post, refund_url, params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, refund_url, params, opts) + initialize_from(response.data, opts) end end def capture(params={}, opts={}) - response, opts = request(:post, capture_url, params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, capture_url, params, opts) + initialize_from(response.data, opts) end def update_dispute(params={}, opts={}) - response, opts = request(:post, dispute_url, params, opts) - initialize_from({ :dispute => response }, opts, true) + self.response, opts = request(:post, dispute_url, params, opts) + initialize_from({ :dispute => response.data }, opts, true) dispute end def close_dispute(params={}, opts={}) - response, opts = request(:post, close_dispute_url, params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, close_dispute_url, params, opts) + initialize_from(response.data, opts) end def mark_as_fraudulent params = { :fraud_details => { :user_report => 'fraudulent' } } - response, opts = request(:post, resource_url, params) - initialize_from(response, opts) + self.response, opts = request(:post, resource_url, params) + initialize_from(response.data, opts) end def mark_as_safe params = { :fraud_details => { :user_report => 'safe' } } - response, opts = request(:post, resource_url, params) - initialize_from(response, opts) + self.response, opts = request(:post, resource_url, params) + initialize_from(response.data, opts) end private diff --git a/lib/stripe/customer.rb b/lib/stripe/customer.rb index 488457f3..ba7c12e9 100644 --- a/lib/stripe/customer.rb +++ b/lib/stripe/customer.rb @@ -38,25 +38,25 @@ module Stripe end def cancel_subscription(params={}, opts={}) - response, opts = request(:delete, subscription_url, params, opts) - initialize_from({ :subscription => response }, opts, true) + self.response, opts = request(:delete, subscription_url, params, opts) + initialize_from({ :subscription => response.data }, opts, true) subscription end def update_subscription(params={}, opts={}) - response, opts = request(:post, subscription_url, params, opts) - initialize_from({ :subscription => response }, opts, true) + self.response, opts = request(:post, subscription_url, params, opts) + initialize_from({ :subscription => response.data }, opts, true) subscription end def create_subscription(params={}, opts={}) - response, opts = request(:post, subscriptions_url, params, opts) - initialize_from({ :subscription => response }, opts, true) + self.response, opts = request(:post, subscriptions_url, params, opts) + initialize_from({ :subscription => response.data }, opts, true) subscription end def delete_discount - _, opts = request(:delete, discount_url) + self.response, opts = request(:delete, discount_url) initialize_from({ :discount => nil }, opts, true) end diff --git a/lib/stripe/dispute.rb b/lib/stripe/dispute.rb index 73b6439c..7fc994cb 100644 --- a/lib/stripe/dispute.rb +++ b/lib/stripe/dispute.rb @@ -5,8 +5,8 @@ module Stripe include Stripe::APIOperations::Save def close(params={}, opts={}) - response, opts = request(:post, close_url, params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, close_url, params, opts) + initialize_from(response.data, opts) end def close_url diff --git a/lib/stripe/errors.rb b/lib/stripe/errors.rb index dcf99780..9fad60cb 100644 --- a/lib/stripe/errors.rb +++ b/lib/stripe/errors.rb @@ -3,12 +3,24 @@ module Stripe # errors derive. class StripeError < StandardError attr_reader :message - attr_reader :http_status + + # Response contains a structure that has some basic information about the + # response that conveyed the error. + attr_accessor :response + + # These fields are now available as part of #response and that usage should + # be preferred. attr_reader :http_body attr_reader :http_headers + attr_reader :http_status + attr_reader :json_body # equivalent to #data attr_reader :request_id - attr_reader :json_body + # Initializes a StripeError. + # + # Note: We should try to move away from the very heavy constructors ordered + # parameters to each just setting accessor values directly or optional + # arguments. def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil, http_headers=nil) @message = message diff --git a/lib/stripe/invoice.rb b/lib/stripe/invoice.rb index 7ecd0bac..d8210350 100644 --- a/lib/stripe/invoice.rb +++ b/lib/stripe/invoice.rb @@ -5,13 +5,13 @@ module Stripe extend Stripe::APIOperations::Create def self.upcoming(params, opts={}) - response, opts = request(:get, upcoming_url, params, opts) - Util.convert_to_stripe_object(response, opts) + resp, opts = request(:get, upcoming_url, params, opts) + Util.convert_to_stripe_object(resp.data, opts, response: resp) end def pay(opts={}) - response, opts = request(:post, pay_url, {}, opts) - initialize_from(response, opts) + self.response, opts = request(:post, pay_url, {}, opts) + initialize_from(response.data, opts) end private diff --git a/lib/stripe/list_object.rb b/lib/stripe/list_object.rb index 318d1ccf..4a936e7c 100644 --- a/lib/stripe/list_object.rb +++ b/lib/stripe/list_object.rb @@ -10,6 +10,10 @@ module Stripe # expansions, and predicates as a user pages through resources. attr_accessor :filters + # Response contains a structure that has some basic information about the + # response that conveyed the list resource. + attr_accessor :response + # An empty list object. This is returned from +next+ when we know that # there isn't a next page in order to replicate the behavior of the API # when it attempts to return a page beyond the last. @@ -64,8 +68,8 @@ module Stripe def retrieve(id, opts={}) id, retrieve_params = Util.normalize_id(id) - response, opts = request(:get,"#{resource_url}/#{CGI.escape(id)}", retrieve_params, opts) - Util.convert_to_stripe_object(response, opts) + resp, opts = request(:get,"#{resource_url}/#{CGI.escape(id)}", retrieve_params, opts) + Util.convert_to_stripe_object(resp.data, opts, response: resp) end # Fetches the next page in the resource list (if there is one). diff --git a/lib/stripe/order.rb b/lib/stripe/order.rb index 2f9b9eab..8c001588 100644 --- a/lib/stripe/order.rb +++ b/lib/stripe/order.rb @@ -5,13 +5,13 @@ module Stripe include Stripe::APIOperations::Save def pay(params, opts={}) - response, opts = request(:post, pay_url, params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, pay_url, params, opts) + initialize_from(response.data, opts) end def return_order(params, opts={}) - response, opts = request(:post, returns_url, params, opts) - Util.convert_to_stripe_object(response, opts) + resp, opts = request(:post, returns_url, params, opts) + Util.convert_to_stripe_object(resp.data, opts, response: resp) end private diff --git a/lib/stripe/source.rb b/lib/stripe/source.rb index 6f194137..ff2fb9d2 100644 --- a/lib/stripe/source.rb +++ b/lib/stripe/source.rb @@ -4,8 +4,8 @@ module Stripe include Stripe::APIOperations::Save def verify(params={}, opts={}) - response, opts = request(:post, resource_url + '/verify', params, opts) - initialize_from(response, opts) + self.response, opts = request(:post, resource_url + '/verify', params, opts) + initialize_from(response.data, opts) end end end diff --git a/lib/stripe/stripe_response.rb b/lib/stripe/stripe_response.rb new file mode 100644 index 00000000..7d216726 --- /dev/null +++ b/lib/stripe/stripe_response.rb @@ -0,0 +1,34 @@ +module Stripe + # StripeResponse encapsulates some vitals of a response that came back from + # the Stripe API. + class StripeResponse + # The data contained by the HTTP body of the response deserialized from + # JSON. + attr_accessor :data + + # The raw HTTP body of the response. + attr_accessor :http_body + + # A Hash of the HTTP headers of the response. + attr_accessor :http_headers + + # The integer HTTP status code of the response. + attr_accessor :http_status + + # The Stripe request ID of the response. + attr_accessor :request_id + + # Initializes a StripeResponse object from a RestClient HTTP response + # object. + # + # This may throw JSON::ParserError if the response body is not valid JSON. + def self.from_rest_client_response(http_resp) + resp = StripeResponse.new + resp.data = JSON.parse(http_resp.body, symbolize_names: true) + resp.http_body = http_resp.body + resp.http_headers = http_resp.headers + resp.http_status = http_resp.code + resp + end + end +end diff --git a/lib/stripe/transfer.rb b/lib/stripe/transfer.rb index e5840bb6..8a8c3e5f 100644 --- a/lib/stripe/transfer.rb +++ b/lib/stripe/transfer.rb @@ -5,8 +5,8 @@ module Stripe include Stripe::APIOperations::Save def cancel - response, api_key = self.request(:post, cancel_url) - initialize_from(response, api_key) + self.response, api_key = self.request(:post, cancel_url) + initialize_from(response.data, api_key) end def cancel_url diff --git a/lib/stripe/util.rb b/lib/stripe/util.rb index 6d8b0a08..4c834871 100644 --- a/lib/stripe/util.rb +++ b/lib/stripe/util.rb @@ -68,19 +68,28 @@ module Stripe # # ==== Attributes # - # * +resp+ - Hash of fields and values to be converted into a StripeObject. + # * +data+ - Hash of fields and values to be converted into a StripeObject. # * +opts+ - Options for +StripeObject+ like an API key that will be reused # on subsequent API calls. - def self.convert_to_stripe_object(resp, opts) - case resp + # * +response+ - An object containing information about the API response + # that produced the data which is hydrating the StripeObject. + def self.convert_to_stripe_object(data, opts, response: nil) + obj = case data when Array - resp.map { |i| convert_to_stripe_object(i, opts) } + data.map { |i| convert_to_stripe_object(i, opts) } when Hash # Try converting to a known object class. If none available, fall back to generic StripeObject - object_classes.fetch(resp[:object], StripeObject).construct_from(resp, opts) + object_classes.fetch(data[:object], StripeObject).construct_from(data, opts) else - resp + data end + + case obj + when APIResource, ListObject + obj.response = response + end + + obj end def self.file_readable(file)