recurly 2.17.0 → 3.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.bumpversion.cfg +15 -0
- data/.github/ISSUE_TEMPLATE/bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/question-or-other.md +10 -0
- data/.github/workflows/docs.yml +28 -0
- data/.github_changelog_generator +8 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/CODE_OF_CONDUCT.md +130 -0
- data/CONTRIBUTING.md +106 -0
- data/GETTING_STARTED.md +319 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +9 -153
- data/Rakefile +6 -0
- data/benchmark.rb +16 -0
- data/lib/data/ca-certificates.crt +3466 -0
- data/lib/recurly/client/operations.rb +3665 -0
- data/lib/recurly/client.rb +353 -0
- data/lib/recurly/connection_pool.rb +40 -0
- data/lib/recurly/errors/api_errors.rb +83 -0
- data/lib/recurly/errors/network_errors.rb +10 -0
- data/lib/recurly/errors.rb +68 -0
- data/lib/recurly/http.rb +50 -0
- data/lib/recurly/pager.rb +146 -0
- data/lib/recurly/request.rb +31 -0
- data/lib/recurly/requests/account_acquisition_cost.rb +18 -0
- data/lib/recurly/requests/account_acquisition_updatable.rb +26 -0
- data/lib/recurly/requests/account_create.rb +90 -0
- data/lib/recurly/requests/account_purchase.rb +90 -0
- data/lib/recurly/requests/account_update.rb +78 -0
- data/lib/recurly/requests/add_on_create.rb +94 -0
- data/lib/recurly/requests/add_on_pricing.rb +18 -0
- data/lib/recurly/requests/add_on_update.rb +74 -0
- data/lib/recurly/requests/address.rb +46 -0
- data/lib/recurly/requests/billing_info_create.rb +110 -0
- data/lib/recurly/requests/coupon_bulk_create.rb +14 -0
- data/lib/recurly/requests/coupon_create.rb +94 -0
- data/lib/recurly/requests/coupon_pricing.rb +18 -0
- data/lib/recurly/requests/coupon_redemption_create.rb +18 -0
- data/lib/recurly/requests/coupon_update.rb +34 -0
- data/lib/recurly/requests/custom_field.rb +18 -0
- data/lib/recurly/requests/external_refund.rb +22 -0
- data/lib/recurly/requests/external_transaction.rb +26 -0
- data/lib/recurly/requests/invoice_address.rb +54 -0
- data/lib/recurly/requests/invoice_collect.rb +18 -0
- data/lib/recurly/requests/invoice_create.rb +42 -0
- data/lib/recurly/requests/invoice_refund.rb +34 -0
- data/lib/recurly/requests/invoice_updatable.rb +34 -0
- data/lib/recurly/requests/item_create.rb +58 -0
- data/lib/recurly/requests/item_update.rb +58 -0
- data/lib/recurly/requests/line_item_create.rb +82 -0
- data/lib/recurly/requests/line_item_refund.rb +22 -0
- data/lib/recurly/requests/measured_unit_create.rb +22 -0
- data/lib/recurly/requests/measured_unit_update.rb +22 -0
- data/lib/recurly/requests/plan_create.rb +98 -0
- data/lib/recurly/requests/plan_hosted_pages.rb +26 -0
- data/lib/recurly/requests/plan_pricing.rb +22 -0
- data/lib/recurly/requests/plan_update.rb +94 -0
- data/lib/recurly/requests/pricing.rb +18 -0
- data/lib/recurly/requests/purchase_create.rb +74 -0
- data/lib/recurly/requests/shipping_address_create.rb +62 -0
- data/lib/recurly/requests/shipping_address_update.rb +66 -0
- data/lib/recurly/requests/shipping_fee_create.rb +22 -0
- data/lib/recurly/requests/shipping_method_create.rb +26 -0
- data/lib/recurly/requests/shipping_method_update.rb +26 -0
- data/lib/recurly/requests/shipping_purchase.rb +22 -0
- data/lib/recurly/requests/subscription_add_on_create.rb +38 -0
- data/lib/recurly/requests/subscription_add_on_tier.rb +18 -0
- data/lib/recurly/requests/subscription_add_on_update.rb +42 -0
- data/lib/recurly/requests/subscription_cancel.rb +14 -0
- data/lib/recurly/requests/subscription_change_create.rb +66 -0
- data/lib/recurly/requests/subscription_change_shipping_create.rb +22 -0
- data/lib/recurly/requests/subscription_create.rb +106 -0
- data/lib/recurly/requests/subscription_pause.rb +14 -0
- data/lib/recurly/requests/subscription_purchase.rb +66 -0
- data/lib/recurly/requests/subscription_shipping_create.rb +30 -0
- data/lib/recurly/requests/subscription_shipping_purchase.rb +22 -0
- data/lib/recurly/requests/subscription_shipping_update.rb +22 -0
- data/lib/recurly/requests/subscription_update.rb +58 -0
- data/lib/recurly/requests/tier.rb +18 -0
- data/lib/recurly/requests/usage_create.rb +26 -0
- data/lib/recurly/requests.rb +8 -0
- data/lib/recurly/resource.rb +23 -1082
- data/lib/recurly/resources/account.rb +130 -0
- data/lib/recurly/resources/account_acquisition.rb +46 -0
- data/lib/recurly/resources/account_acquisition_cost.rb +18 -0
- data/lib/recurly/resources/account_balance.rb +26 -0
- data/lib/recurly/resources/account_balance_amount.rb +18 -0
- data/lib/recurly/resources/account_mini.rb +46 -0
- data/lib/recurly/resources/account_note.rb +34 -0
- data/lib/recurly/resources/add_on.rb +114 -0
- data/lib/recurly/resources/add_on_mini.rb +54 -0
- data/lib/recurly/resources/add_on_pricing.rb +18 -0
- data/lib/recurly/resources/address.rb +46 -0
- data/lib/recurly/resources/billing_info.rb +66 -0
- data/lib/recurly/resources/billing_info_updated_by.rb +18 -0
- data/lib/recurly/resources/binary_file.rb +14 -0
- data/lib/recurly/resources/coupon.rb +130 -0
- data/lib/recurly/resources/coupon_discount.rb +26 -0
- data/lib/recurly/resources/coupon_discount_pricing.rb +18 -0
- data/lib/recurly/resources/coupon_discount_trial.rb +18 -0
- data/lib/recurly/resources/coupon_mini.rb +42 -0
- data/lib/recurly/resources/coupon_redemption.rb +50 -0
- data/lib/recurly/resources/coupon_redemption_mini.rb +34 -0
- data/lib/recurly/resources/credit_payment.rb +66 -0
- data/lib/recurly/resources/custom_field.rb +18 -0
- data/lib/recurly/resources/custom_field_definition.rb +50 -0
- data/lib/recurly/resources/error.rb +22 -0
- data/lib/recurly/resources/error_may_have_transaction.rb +26 -0
- data/lib/recurly/resources/export_dates.rb +18 -0
- data/lib/recurly/resources/export_file.rb +22 -0
- data/lib/recurly/resources/export_files.rb +18 -0
- data/lib/recurly/resources/fraud_info.rb +22 -0
- data/lib/recurly/resources/invoice.rb +146 -0
- data/lib/recurly/resources/invoice_address.rb +54 -0
- data/lib/recurly/resources/invoice_collection.rb +22 -0
- data/lib/recurly/resources/invoice_mini.rb +30 -0
- data/lib/recurly/resources/item.rb +82 -0
- data/lib/recurly/resources/item_mini.rb +34 -0
- data/lib/recurly/resources/line_item.rb +198 -0
- data/lib/recurly/resources/line_item_list.rb +26 -0
- data/lib/recurly/resources/measured_unit.rb +46 -0
- data/lib/recurly/resources/payment_method.rb +66 -0
- data/lib/recurly/resources/plan.rb +118 -0
- data/lib/recurly/resources/plan_hosted_pages.rb +26 -0
- data/lib/recurly/resources/plan_mini.rb +26 -0
- data/lib/recurly/resources/plan_pricing.rb +22 -0
- data/lib/recurly/resources/pricing.rb +18 -0
- data/lib/recurly/resources/settings.rb +22 -0
- data/lib/recurly/resources/shipping_address.rb +82 -0
- data/lib/recurly/resources/shipping_method.rb +46 -0
- data/lib/recurly/resources/shipping_method_mini.rb +26 -0
- data/lib/recurly/resources/site.rb +54 -0
- data/lib/recurly/resources/subscription.rb +174 -0
- data/lib/recurly/resources/subscription_add_on.rb +66 -0
- data/lib/recurly/resources/subscription_add_on_tier.rb +18 -0
- data/lib/recurly/resources/subscription_change.rb +78 -0
- data/lib/recurly/resources/subscription_change_preview.rb +78 -0
- data/lib/recurly/resources/subscription_shipping.rb +26 -0
- data/lib/recurly/resources/tax_info.rb +22 -0
- data/lib/recurly/resources/tier.rb +18 -0
- data/lib/recurly/resources/transaction.rb +158 -0
- data/lib/recurly/resources/transaction_error.rb +38 -0
- data/lib/recurly/resources/transaction_payment_gateway.rb +26 -0
- data/lib/recurly/resources/unique_coupon_code.rb +50 -0
- data/lib/recurly/resources/usage.rb +70 -0
- data/lib/recurly/resources/user.rb +42 -0
- data/lib/recurly/resources.rb +18 -0
- data/lib/recurly/schema/file_parser.rb +13 -0
- data/lib/recurly/schema/json_parser.rb +72 -0
- data/lib/recurly/schema/request_caster.rb +60 -0
- data/lib/recurly/schema/resource_caster.rb +46 -0
- data/lib/recurly/schema/schema_factory.rb +48 -0
- data/lib/recurly/schema/schema_validator.rb +144 -0
- data/lib/recurly/schema.rb +156 -0
- data/lib/recurly/version.rb +1 -15
- data/lib/recurly.rb +15 -137
- data/openapi/api.yaml +21024 -0
- data/recurly.gemspec +39 -0
- data/scripts/build +5 -0
- data/scripts/bump +11 -0
- data/scripts/changelog +14 -0
- data/scripts/clean +6 -0
- data/scripts/format +12 -0
- data/scripts/prepare-release +36 -0
- data/scripts/release +32 -0
- data/scripts/test +15 -0
- metadata +206 -168
- data/lib/recurly/account.rb +0 -169
- data/lib/recurly/account_balance.rb +0 -21
- data/lib/recurly/add_on.rb +0 -30
- data/lib/recurly/address.rb +0 -25
- data/lib/recurly/adjustment.rb +0 -76
- data/lib/recurly/api/errors.rb +0 -206
- data/lib/recurly/api/net_http_adapter.rb +0 -111
- data/lib/recurly/api.rb +0 -101
- data/lib/recurly/billing_info.rb +0 -80
- data/lib/recurly/coupon.rb +0 -134
- data/lib/recurly/credit_payment.rb +0 -28
- data/lib/recurly/custom_field.rb +0 -15
- data/lib/recurly/delivery.rb +0 -19
- data/lib/recurly/error.rb +0 -13
- data/lib/recurly/gift_card.rb +0 -79
- data/lib/recurly/helper.rb +0 -51
- data/lib/recurly/invoice.rb +0 -268
- data/lib/recurly/invoice_collection.rb +0 -14
- data/lib/recurly/js.rb +0 -14
- data/lib/recurly/juris_detail.rb +0 -14
- data/lib/recurly/measured_unit.rb +0 -16
- data/lib/recurly/money.rb +0 -120
- data/lib/recurly/plan.rb +0 -40
- data/lib/recurly/purchase.rb +0 -219
- data/lib/recurly/redemption.rb +0 -46
- data/lib/recurly/resource/association.rb +0 -16
- data/lib/recurly/resource/errors.rb +0 -20
- data/lib/recurly/resource/pager.rb +0 -314
- data/lib/recurly/shipping_address.rb +0 -22
- data/lib/recurly/subscription/add_ons.rb +0 -77
- data/lib/recurly/subscription.rb +0 -325
- data/lib/recurly/subscription_add_on.rb +0 -50
- data/lib/recurly/tax_detail.rb +0 -14
- data/lib/recurly/tax_type.rb +0 -12
- data/lib/recurly/transaction/errors.rb +0 -107
- data/lib/recurly/transaction.rb +0 -116
- data/lib/recurly/usage.rb +0 -24
- data/lib/recurly/webhook/account_notification.rb +0 -10
- data/lib/recurly/webhook/billing_info_updated_notification.rb +0 -6
- data/lib/recurly/webhook/canceled_account_notification.rb +0 -6
- data/lib/recurly/webhook/canceled_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/closed_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/closed_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/credit_payment_notification.rb +0 -12
- data/lib/recurly/webhook/dunning_notification.rb +0 -14
- data/lib/recurly/webhook/expired_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/failed_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/failed_payment_notification.rb +0 -6
- data/lib/recurly/webhook/gift_card_notification.rb +0 -8
- data/lib/recurly/webhook/invoice_notification.rb +0 -12
- data/lib/recurly/webhook/new_account_notification.rb +0 -6
- data/lib/recurly/webhook/new_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/new_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/new_credit_payment_notification.rb +0 -6
- data/lib/recurly/webhook/new_dunning_event_notification.rb +0 -6
- data/lib/recurly/webhook/new_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/new_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/new_usage_notification.rb +0 -8
- data/lib/recurly/webhook/notification.rb +0 -18
- data/lib/recurly/webhook/paid_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/past_due_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/past_due_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/processing_payment_notification.rb +0 -6
- data/lib/recurly/webhook/purchased_gift_card_notification.rb +0 -7
- data/lib/recurly/webhook/reactivated_account_notification.rb +0 -6
- data/lib/recurly/webhook/redeemed_gift_card_notification.rb +0 -7
- data/lib/recurly/webhook/renewed_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/reopened_charge_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/reopened_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/scheduled_payment_notification.rb +0 -6
- data/lib/recurly/webhook/subscription_notification.rb +0 -12
- data/lib/recurly/webhook/successful_payment_notification.rb +0 -6
- data/lib/recurly/webhook/successful_refund_notification.rb +0 -6
- data/lib/recurly/webhook/transaction_authorized_notification.rb +0 -6
- data/lib/recurly/webhook/transaction_notification.rb +0 -12
- data/lib/recurly/webhook/updated_account_notification.rb +0 -6
- data/lib/recurly/webhook/updated_balance_gift_card_notification.rb +0 -7
- data/lib/recurly/webhook/updated_subscription_notification.rb +0 -6
- data/lib/recurly/webhook/void_payment_notification.rb +0 -6
- data/lib/recurly/webhook/voided_credit_invoice_notification.rb +0 -6
- data/lib/recurly/webhook/voided_credit_payment_notification.rb +0 -6
- data/lib/recurly/webhook.rb +0 -88
- data/lib/recurly/xml/nokogiri.rb +0 -58
- data/lib/recurly/xml/rexml.rb +0 -50
- data/lib/recurly/xml.rb +0 -120
@@ -0,0 +1,353 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "erb"
|
3
|
+
require "net/https"
|
4
|
+
require "base64"
|
5
|
+
require "securerandom"
|
6
|
+
require_relative "./schema/json_parser"
|
7
|
+
require_relative "./schema/file_parser"
|
8
|
+
|
9
|
+
module Recurly
|
10
|
+
class Client
|
11
|
+
require_relative "./client/operations"
|
12
|
+
|
13
|
+
BASE_HOST = "v3.recurly.com"
|
14
|
+
BASE_PORT = 443
|
15
|
+
CA_FILE = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt")
|
16
|
+
BINARY_TYPES = [
|
17
|
+
"application/pdf",
|
18
|
+
].freeze
|
19
|
+
JSON_CONTENT_TYPE = "application/json"
|
20
|
+
MAX_RETRIES = 3
|
21
|
+
LOG_LEVELS = %i(debug info warn error fatal).freeze
|
22
|
+
BASE36_ALPHABET = (("0".."9").to_a + ("a".."z").to_a).freeze
|
23
|
+
REQUEST_OPTIONS = [:headers].freeze
|
24
|
+
|
25
|
+
# Initialize a client. It requires an API key.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# API_KEY = '83749879bbde395b5fe0cc1a5abf8e5'
|
29
|
+
# client = Recurly::Client.new(api_key: API_KEY)
|
30
|
+
# sub = client.get_subscription(subscription_id: 'abcd123456')
|
31
|
+
# @example
|
32
|
+
# # You can also pass the initializer a block. This will give you
|
33
|
+
# # a client scoped for just that block
|
34
|
+
# Recurly::Client.new(api_key: API_KEY) do |client|
|
35
|
+
# sub = client.get_subscription(subscription_id: 'abcd123456')
|
36
|
+
# end
|
37
|
+
# @example
|
38
|
+
# # If you only plan on using the client for more than one site,
|
39
|
+
# # you should initialize a new client for each site.
|
40
|
+
#
|
41
|
+
# client = Recurly::Client.new(api_key: API_KEY1)
|
42
|
+
# sub = client.get_subscription(subscription_id: 'uuid-abcd123456')
|
43
|
+
#
|
44
|
+
# # you should create a new client to connect to another site
|
45
|
+
# client = Recurly::Client.new(api_key: API_KEY2)
|
46
|
+
# sub = client.get_subscription(subscription_id: 'uuid-abcd7890')
|
47
|
+
#
|
48
|
+
# @param api_key [String] The private API key
|
49
|
+
# @param logger [Logger] A logger to use. Defaults to creating a new STDOUT logger with level WARN.
|
50
|
+
def initialize(api_key:, site_id: nil, subdomain: nil, logger: nil)
|
51
|
+
set_site_id(site_id, subdomain)
|
52
|
+
set_api_key(api_key)
|
53
|
+
|
54
|
+
if logger.nil?
|
55
|
+
@logger = Logger.new(STDOUT).tap do |l|
|
56
|
+
l.level = Logger::WARN
|
57
|
+
end
|
58
|
+
else
|
59
|
+
unless LOG_LEVELS.all? { |lev| logger.respond_to?(lev) }
|
60
|
+
raise ArgumentError, "You must pass in a logger implementation that responds to the following messages: #{LOG_LEVELS}"
|
61
|
+
end
|
62
|
+
@logger = logger
|
63
|
+
end
|
64
|
+
|
65
|
+
if @logger.level < Logger::INFO
|
66
|
+
msg = <<-MSG
|
67
|
+
The Recurly logger should not be initialized
|
68
|
+
beyond the level INFO. It is currently configured to emit
|
69
|
+
headers and request / response bodies. This has the potential to leak
|
70
|
+
PII and other sensitive information and should never be used in production.
|
71
|
+
MSG
|
72
|
+
log_warn("SECURITY_WARNING", message: msg)
|
73
|
+
end
|
74
|
+
|
75
|
+
# execute block with this client if given
|
76
|
+
yield(self) if block_given?
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
# Used by the operations.rb file to interpolate paths
|
82
|
+
attr_reader :site_id
|
83
|
+
|
84
|
+
def pager(path, **options)
|
85
|
+
Pager.new(
|
86
|
+
client: self,
|
87
|
+
path: path,
|
88
|
+
options: options,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def head(path, **options)
|
93
|
+
request = Net::HTTP::Head.new build_url(path, options)
|
94
|
+
set_headers(request, options[:headers])
|
95
|
+
http_response = run_request(request, options)
|
96
|
+
handle_response! request, http_response
|
97
|
+
end
|
98
|
+
|
99
|
+
def get(path, **options)
|
100
|
+
request = Net::HTTP::Get.new build_url(path, options)
|
101
|
+
set_headers(request, options[:headers])
|
102
|
+
http_response = run_request(request, options)
|
103
|
+
handle_response! request, http_response
|
104
|
+
end
|
105
|
+
|
106
|
+
def post(path, request_data, request_class, **options)
|
107
|
+
request_class.new(request_data).validate!
|
108
|
+
request = Net::HTTP::Post.new build_url(path, options)
|
109
|
+
request.set_content_type(JSON_CONTENT_TYPE)
|
110
|
+
set_headers(request, options[:headers])
|
111
|
+
request.body = JSON.dump(request_data)
|
112
|
+
http_response = run_request(request, options)
|
113
|
+
handle_response! request, http_response
|
114
|
+
end
|
115
|
+
|
116
|
+
def put(path, request_data = nil, request_class = nil, **options)
|
117
|
+
request = Net::HTTP::Put.new build_url(path, options)
|
118
|
+
request.set_content_type(JSON_CONTENT_TYPE)
|
119
|
+
set_headers(request, options[:headers])
|
120
|
+
if request_data
|
121
|
+
request_class.new(request_data).validate!
|
122
|
+
json_body = JSON.dump(request_data)
|
123
|
+
request.body = json_body
|
124
|
+
end
|
125
|
+
http_response = run_request(request, options)
|
126
|
+
handle_response! request, http_response
|
127
|
+
end
|
128
|
+
|
129
|
+
def delete(path, **options)
|
130
|
+
request = Net::HTTP::Delete.new build_url(path, options)
|
131
|
+
set_headers(request, options[:headers])
|
132
|
+
http_response = run_request(request, options)
|
133
|
+
handle_response! request, http_response
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
@connection_pool = Recurly::ConnectionPool.new
|
139
|
+
|
140
|
+
class << self
|
141
|
+
# @return [Recurly::ConnectionPool]
|
142
|
+
attr_accessor :connection_pool
|
143
|
+
end
|
144
|
+
|
145
|
+
def run_request(request, options = {})
|
146
|
+
self.class.connection_pool.with_connection do |http|
|
147
|
+
set_http_options(http, options)
|
148
|
+
|
149
|
+
retries = 0
|
150
|
+
|
151
|
+
begin
|
152
|
+
http.start unless http.started?
|
153
|
+
log_attrs = {
|
154
|
+
method: request.method,
|
155
|
+
path: request.path,
|
156
|
+
}
|
157
|
+
if @logger.level < Logger::INFO
|
158
|
+
log_attrs[:request_body] = request.body
|
159
|
+
# No need to log the authorization header
|
160
|
+
headers = request.to_hash.reject { |k, _| k&.downcase == "authorization" }
|
161
|
+
log_attrs[:request_headers] = headers
|
162
|
+
end
|
163
|
+
|
164
|
+
log_info("Request", **log_attrs)
|
165
|
+
start = Time.now
|
166
|
+
response = http.request(request)
|
167
|
+
elapsed = Time.now - start
|
168
|
+
|
169
|
+
# GETs are safe to retry after a server error, requests with an Idempotency-Key will return the prior response
|
170
|
+
if response.kind_of?(Net::HTTPServerError) && request.is_a?(Net::HTTP::Get)
|
171
|
+
retries += 1
|
172
|
+
log_info("Retrying", retries: retries, **log_attrs)
|
173
|
+
start = Time.now
|
174
|
+
response = http.request(request) if retries < MAX_RETRIES
|
175
|
+
elapsed = Time.now - start
|
176
|
+
end
|
177
|
+
|
178
|
+
if @logger.level < Logger::INFO
|
179
|
+
log_attrs[:response_body] = response.body
|
180
|
+
log_attrs[:response_headers] = response.to_hash
|
181
|
+
end
|
182
|
+
log_info("Response", time_ms: (elapsed * 1_000).floor, status: response.code, **log_attrs)
|
183
|
+
|
184
|
+
response
|
185
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ECONNABORTED,
|
186
|
+
Errno::EPIPE, Errno::ETIMEDOUT, Net::OpenTimeout, EOFError, SocketError => ex
|
187
|
+
retries += 1
|
188
|
+
if retries < MAX_RETRIES
|
189
|
+
retry
|
190
|
+
end
|
191
|
+
|
192
|
+
if ex.kind_of?(Net::OpenTimeout) || ex.kind_of?(Errno::ETIMEDOUT)
|
193
|
+
raise Recurly::Errors::TimeoutError, "Request timed out"
|
194
|
+
end
|
195
|
+
|
196
|
+
raise Recurly::Errors::ConnectionFailedError, "Failed to connect to Recurly: #{ex.message}"
|
197
|
+
rescue Net::ReadTimeout, Timeout::Error
|
198
|
+
raise Recurly::Errors::TimeoutError, "Request timed out"
|
199
|
+
rescue OpenSSL::SSL::SSLError => ex
|
200
|
+
raise Recurly::Errors::SSLError, ex.message
|
201
|
+
rescue StandardError => ex
|
202
|
+
raise Recurly::Errors::NetworkError, ex.message
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def set_headers(request, additional_headers = {})
|
208
|
+
# TODO this is undocumented until we finalize it
|
209
|
+
additional_headers.each { |header, v| request[header] = v } if additional_headers
|
210
|
+
|
211
|
+
request["Accept"] = "application/vnd.recurly.#{api_version}".chomp # got this method from operations.rb
|
212
|
+
request["Authorization"] = "Basic #{Base64.encode64(@api_key)}".chomp
|
213
|
+
request["User-Agent"] = "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}"
|
214
|
+
|
215
|
+
unless request.is_a?(Net::HTTP::Get) || request.is_a?(Net::HTTP::Head)
|
216
|
+
request["Idempotency-Key"] ||= generate_idempotency_key
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# from https://github.com/rails/rails/blob/6-0-stable/activesupport/lib/active_support/core_ext/securerandom.rb
|
221
|
+
def generate_idempotency_key(n = 16)
|
222
|
+
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
|
223
|
+
idx = byte % 64
|
224
|
+
idx = SecureRandom.random_number(36) if idx >= 36
|
225
|
+
BASE36_ALPHABET[idx]
|
226
|
+
end.join
|
227
|
+
end
|
228
|
+
|
229
|
+
def set_http_options(http, options)
|
230
|
+
http.open_timeout = options[:open_timeout] || 20
|
231
|
+
http.read_timeout = options[:read_timeout] || 60
|
232
|
+
end
|
233
|
+
|
234
|
+
def handle_response!(request, http_response)
|
235
|
+
response = HTTP::Response.new(http_response, request)
|
236
|
+
raise_api_error!(http_response, response) unless http_response.kind_of?(Net::HTTPSuccess)
|
237
|
+
resource = if response.body
|
238
|
+
if http_response.content_type&.include?(JSON_CONTENT_TYPE)
|
239
|
+
JSONParser.parse(self, response.body)
|
240
|
+
elsif BINARY_TYPES.include?(http_response.content_type)
|
241
|
+
FileParser.parse(response.body)
|
242
|
+
else
|
243
|
+
raise Recurly::Errors::InvalidResponseError, "Unexpected content type: #{http_response.content_type}"
|
244
|
+
end
|
245
|
+
else
|
246
|
+
Resources::Empty.new
|
247
|
+
end
|
248
|
+
# Keep this interface "private"
|
249
|
+
resource.instance_variable_set(:@response, response)
|
250
|
+
resource
|
251
|
+
end
|
252
|
+
|
253
|
+
def raise_api_error!(http_response, response)
|
254
|
+
if response.content_type.include?(JSON_CONTENT_TYPE)
|
255
|
+
error = JSONParser.parse(self, response.body)
|
256
|
+
begin
|
257
|
+
error_class = Errors::APIError.error_class(error.type)
|
258
|
+
raise error_class.new(response, error)
|
259
|
+
rescue NameError
|
260
|
+
error_class = Errors::APIError.from_response(http_response)
|
261
|
+
raise error_class.new(response, error)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
error_class = Errors::APIError.from_response(http_response)
|
266
|
+
|
267
|
+
if error_class <= Recurly::Errors::APIError
|
268
|
+
error = Recurly::Resources::Error.new(message: "#{http_response.code}: #{http_response.message}")
|
269
|
+
raise error_class.new(response, error)
|
270
|
+
else
|
271
|
+
raise error_class, "#{http_response.code}: #{http_response.message}"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def read_headers(response)
|
276
|
+
if !@_ignore_deprecation_warning && response.headers["Recurly-Deprecated"]&.upcase == "TRUE"
|
277
|
+
log_warn("DEPRECTATION WARNING", message: "Your current API version \"#{api_version}\" is deprecated and will be sunset on #{response.headers["Recurly-Sunset-Date"]}")
|
278
|
+
end
|
279
|
+
response
|
280
|
+
end
|
281
|
+
|
282
|
+
def validate_path_parameters!(**options)
|
283
|
+
# Check to see that we are passing the correct data types
|
284
|
+
# This prevents a confusing error if the user passes in a non-primitive by mistake
|
285
|
+
options.each do |k, v|
|
286
|
+
unless [String, Symbol, Integer, Float].include?(v.class)
|
287
|
+
message = "We cannot build the url with the given argument #{k}=#{v.inspect}."
|
288
|
+
if k =~ /_id$/
|
289
|
+
message << " Since this appears to be an id, perhaps you meant to pass in a String?"
|
290
|
+
end
|
291
|
+
raise ArgumentError, message
|
292
|
+
end
|
293
|
+
end
|
294
|
+
# Check to make sure that parameters are not empty string values
|
295
|
+
empty_strings = options.select { |_, v| v.is_a?(String) && v.strip.empty? }
|
296
|
+
if empty_strings.any?
|
297
|
+
raise ArgumentError, "#{empty_strings.keys.join(", ")} cannot be an empty string"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def interpolate_path(path, **options)
|
302
|
+
validate_path_parameters!(options)
|
303
|
+
options.each do |k, v|
|
304
|
+
# We need to encode the values for the url
|
305
|
+
options[k] = ERB::Util.url_encode(v.to_s)
|
306
|
+
end
|
307
|
+
path = path.gsub("{", "%{")
|
308
|
+
path % options
|
309
|
+
end
|
310
|
+
|
311
|
+
def set_site_id(site_id, subdomain)
|
312
|
+
if site_id
|
313
|
+
@site_id = site_id
|
314
|
+
elsif subdomain
|
315
|
+
@site_id = "subdomain-#{subdomain}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def set_api_key(api_key)
|
320
|
+
@api_key = api_key
|
321
|
+
end
|
322
|
+
|
323
|
+
def build_url(path, options)
|
324
|
+
path = scope_by_site(path, options)
|
325
|
+
query_params = options.reject { |k, _| REQUEST_OPTIONS.include?(k.to_sym) }
|
326
|
+
if query_params.any?
|
327
|
+
"#{path}?#{URI.encode_www_form(query_params)}"
|
328
|
+
else
|
329
|
+
path
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def scope_by_site(path, **options)
|
334
|
+
if site = site_id || options[:site_id]
|
335
|
+
# Ensure that we are only including the site_id once because the Pager operations
|
336
|
+
# will use the cursor returned from the API which may already have these components
|
337
|
+
path.start_with?("/sites/#{site}") ? path : "/sites/#{site}#{path}"
|
338
|
+
else
|
339
|
+
path
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Define a private `log_<level>` method for each log level
|
344
|
+
LOG_LEVELS.each do |level|
|
345
|
+
define_method "log_#{level}" do |tag, **attrs|
|
346
|
+
@logger.send(level, "Recurly") do
|
347
|
+
msg = attrs.each_pair.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
348
|
+
"[#{tag}] #{msg}"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "net/https"
|
2
|
+
|
3
|
+
module Recurly
|
4
|
+
class ConnectionPool
|
5
|
+
def initialize
|
6
|
+
@mutex = Mutex.new
|
7
|
+
@pool = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def with_connection
|
11
|
+
http = nil
|
12
|
+
@mutex.synchronize do
|
13
|
+
http = @pool.pop
|
14
|
+
end
|
15
|
+
|
16
|
+
# create connection if the pool was empty
|
17
|
+
http ||= init_http_connection
|
18
|
+
|
19
|
+
response = yield http
|
20
|
+
|
21
|
+
if http.started?
|
22
|
+
@mutex.synchronize do
|
23
|
+
@pool.push(http)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
response
|
28
|
+
end
|
29
|
+
|
30
|
+
def init_http_connection
|
31
|
+
http = Net::HTTP.new(Client::BASE_HOST, Client::BASE_PORT)
|
32
|
+
http.use_ssl = true
|
33
|
+
http.ca_file = Client::CA_FILE
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
35
|
+
http.keep_alive_timeout = 600
|
36
|
+
|
37
|
+
http
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# This file is automatically created by Recurly's OpenAPI generation process
|
2
|
+
# and thus any edits you make by hand will be lost. If you wish to make a
|
3
|
+
# change to this file, please create a Github issue explaining the changes you
|
4
|
+
# need and we will usher them to the appropriate places.
|
5
|
+
module Recurly
|
6
|
+
module Errors
|
7
|
+
ERROR_MAP = {
|
8
|
+
"500" => "InternalServerError",
|
9
|
+
"502" => "BadGatewayError",
|
10
|
+
"503" => "ServiceUnavailableError",
|
11
|
+
"304" => "NotModifiedError",
|
12
|
+
"400" => "BadRequestError",
|
13
|
+
"401" => "UnauthorizedError",
|
14
|
+
"402" => "PaymentRequiredError",
|
15
|
+
"403" => "ForbiddenError",
|
16
|
+
"404" => "NotFoundError",
|
17
|
+
"406" => "NotAcceptableError",
|
18
|
+
"412" => "PreconditionFailedError",
|
19
|
+
"422" => "UnprocessableEntityError",
|
20
|
+
"429" => "TooManyRequestsError",
|
21
|
+
}
|
22
|
+
|
23
|
+
class ResponseError < Errors::APIError; end
|
24
|
+
|
25
|
+
class ServerError < ResponseError; end
|
26
|
+
|
27
|
+
class InternalServerError < ServerError; end
|
28
|
+
|
29
|
+
class BadGatewayError < ServerError; end
|
30
|
+
|
31
|
+
class ServiceUnavailableError < ServerError; end
|
32
|
+
|
33
|
+
class RedirectionError < ResponseError; end
|
34
|
+
|
35
|
+
class NotModifiedError < ResponseError; end
|
36
|
+
|
37
|
+
class ClientError < Errors::APIError; end
|
38
|
+
|
39
|
+
class BadRequestError < ClientError; end
|
40
|
+
|
41
|
+
class InvalidContentTypeError < BadRequestError; end
|
42
|
+
|
43
|
+
class UnauthorizedError < ClientError; end
|
44
|
+
|
45
|
+
class PaymentRequiredError < ClientError; end
|
46
|
+
|
47
|
+
class ForbiddenError < ClientError; end
|
48
|
+
|
49
|
+
class InvalidApiKeyError < ForbiddenError; end
|
50
|
+
|
51
|
+
class InvalidPermissionsError < ForbiddenError; end
|
52
|
+
|
53
|
+
class NotFoundError < ClientError; end
|
54
|
+
|
55
|
+
class NotAcceptableError < ClientError; end
|
56
|
+
|
57
|
+
class UnknownApiVersionError < NotAcceptableError; end
|
58
|
+
|
59
|
+
class UnavailableInApiVersionError < NotAcceptableError; end
|
60
|
+
|
61
|
+
class InvalidApiVersionError < NotAcceptableError; end
|
62
|
+
|
63
|
+
class PreconditionFailedError < ClientError; end
|
64
|
+
|
65
|
+
class UnprocessableEntityError < ClientError; end
|
66
|
+
|
67
|
+
class ValidationError < UnprocessableEntityError; end
|
68
|
+
|
69
|
+
class MissingFeatureError < UnprocessableEntityError; end
|
70
|
+
|
71
|
+
class TransactionError < UnprocessableEntityError; end
|
72
|
+
|
73
|
+
class SimultaneousRequestError < UnprocessableEntityError; end
|
74
|
+
|
75
|
+
class ImmutableSubscriptionError < UnprocessableEntityError; end
|
76
|
+
|
77
|
+
class InvalidTokenError < UnprocessableEntityError; end
|
78
|
+
|
79
|
+
class TooManyRequestsError < ClientError; end
|
80
|
+
|
81
|
+
class RateLimitedError < TooManyRequestsError; end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Recurly
|
2
|
+
module Errors
|
3
|
+
class NetworkError < StandardError; end
|
4
|
+
class InvalidResponseError < NetworkError; end
|
5
|
+
class TimeoutError < NetworkError; end
|
6
|
+
class ConnectionFailedError < NetworkError; end
|
7
|
+
class SSLError < NetworkError; end
|
8
|
+
class UnavailableError < NetworkError; end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Recurly
|
2
|
+
module Errors
|
3
|
+
class APIError < StandardError
|
4
|
+
# @!attribute recurly_error
|
5
|
+
# @return [Recurly::Resources::Error] The {Recurly::Resources::Error} object
|
6
|
+
attr_reader :recurly_error
|
7
|
+
|
8
|
+
# Looks up an Error class by name
|
9
|
+
# @example
|
10
|
+
# Errors.error_class('BadRequestError')
|
11
|
+
# #=> Errors::BadRequestError
|
12
|
+
# @param error_key [String]
|
13
|
+
# @return [Errors::APIError,Errors::NetworkError]
|
14
|
+
def self.error_class(error_key)
|
15
|
+
class_name = error_key.split("_").map(&:capitalize).join
|
16
|
+
class_name += "Error" unless class_name.end_with?("Error")
|
17
|
+
Errors.const_get(class_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
# When the response does not have a JSON body, this determines the appropriate
|
21
|
+
# Error class based on the response code. This may occur when a load balancer
|
22
|
+
# returns an error before it reaches Recurly's API.
|
23
|
+
# @param response [Net::Response]
|
24
|
+
# @return [Errors::APIError,Errors::NetworkError]
|
25
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
26
|
+
def self.from_response(response)
|
27
|
+
case response
|
28
|
+
when Net::HTTPBadRequest # 400
|
29
|
+
Recurly::Errors::BadRequestError
|
30
|
+
when Net::HTTPUnauthorized, Net::HTTPForbidden # 401, 403
|
31
|
+
Recurly::Errors::UnauthorizedError
|
32
|
+
when Net::HTTPRequestTimeOut # 408
|
33
|
+
Recurly::Errors::TimeoutError
|
34
|
+
when Net::HTTPTooManyRequests # 429
|
35
|
+
Recurly::Errors::RateLimitedError
|
36
|
+
when Net::HTTPInternalServerError # 500
|
37
|
+
Recurly::Errors::InternalServerError
|
38
|
+
when Net::HTTPServiceUnavailable # 503
|
39
|
+
Recurly::Errors::UnavailableError
|
40
|
+
when Net::HTTPGatewayTimeOut # 504
|
41
|
+
Recurly::Errors::TimeoutError
|
42
|
+
when Net::HTTPServerError # 5xx
|
43
|
+
Recurly::Errors::UnavailableError
|
44
|
+
else
|
45
|
+
Recurly::Errors::APIError
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
49
|
+
|
50
|
+
def initialize(response, error)
|
51
|
+
super(error.message)
|
52
|
+
@response = response
|
53
|
+
@recurly_error = error
|
54
|
+
end
|
55
|
+
|
56
|
+
def status_code
|
57
|
+
@response.status
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_response
|
61
|
+
@response
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
require_relative "./errors/api_errors"
|
67
|
+
require_relative "./errors/network_errors"
|
68
|
+
end
|
data/lib/recurly/http.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module Recurly
|
2
|
+
module HTTP
|
3
|
+
class Response
|
4
|
+
attr_accessor :status, :body, :request,
|
5
|
+
:request_id, :rate_limit, :rate_limit_remaining,
|
6
|
+
:rate_limit_reset, :date, :proxy_metadata,
|
7
|
+
:content_type, :total_records
|
8
|
+
|
9
|
+
def initialize(resp, request)
|
10
|
+
@request = Request.new(request.method, request.path, request.body)
|
11
|
+
@status = resp.code.to_i
|
12
|
+
@request_id = resp["x-request-id"]
|
13
|
+
@rate_limit = resp["x-ratelimit-limit"].to_i
|
14
|
+
@rate_limit_remaining = resp["x-ratelimit-remaining"].to_i
|
15
|
+
@rate_limit_reset = Time.at(resp["x-ratelimit-reset"].to_i).to_datetime
|
16
|
+
@total_records = resp["recurly-total-records"]&.to_i
|
17
|
+
if resp["content-type"]
|
18
|
+
@content_type = resp["content-type"].split(";").first
|
19
|
+
else
|
20
|
+
@content_type = resp.content_type
|
21
|
+
end
|
22
|
+
if resp.body && !resp.body.empty?
|
23
|
+
@body = resp.body
|
24
|
+
else
|
25
|
+
@body = nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Request
|
31
|
+
attr_accessor :method, :path, :body
|
32
|
+
|
33
|
+
def initialize(method, path, body = nil)
|
34
|
+
@method = method
|
35
|
+
@path = path
|
36
|
+
if body && !body.empty?
|
37
|
+
@body = body
|
38
|
+
else
|
39
|
+
@body = nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
method == other.method \
|
45
|
+
&& path == other.path \
|
46
|
+
&& body == other.body
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|