recurly 2.17.5 → 4.18.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/.changelog_config.yaml +11 -0
- data/.github/ISSUE_TEMPLATE/bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/question-or-other.md +10 -0
- data/.github/workflows/ci.yml +29 -0
- data/.github/workflows/docs.yml +28 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +295 -0
- data/CODE_OF_CONDUCT.md +130 -0
- data/CONTRIBUTING.md +106 -0
- data/GETTING_STARTED.md +330 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +9 -148
- data/Rakefile +6 -0
- data/benchmark.rb +16 -0
- data/lib/data/ca-certificates.crt +3466 -0
- data/lib/recurly/client/operations.rb +4079 -0
- data/lib/recurly/client.rb +400 -0
- data/lib/recurly/connection_pool.rb +42 -0
- data/lib/recurly/errors/api_errors.rb +90 -0
- data/lib/recurly/errors/network_errors.rb +7 -0
- data/lib/recurly/errors.rb +51 -0
- data/lib/recurly/http.rb +50 -0
- data/lib/recurly/pager.rb +136 -0
- data/lib/recurly/request.rb +31 -0
- data/lib/recurly/requests/account_acquisition_cost.rb +18 -0
- data/lib/recurly/requests/account_acquisition_update.rb +26 -0
- data/lib/recurly/requests/account_create.rb +98 -0
- data/lib/recurly/requests/account_purchase.rb +98 -0
- data/lib/recurly/requests/account_update.rb +86 -0
- data/lib/recurly/requests/add_on_create.rb +102 -0
- data/lib/recurly/requests/add_on_pricing.rb +26 -0
- data/lib/recurly/requests/add_on_update.rb +78 -0
- data/lib/recurly/requests/address.rb +38 -0
- data/lib/recurly/requests/billing_info_create.rb +134 -0
- data/lib/recurly/requests/billing_info_verify.rb +14 -0
- data/lib/recurly/requests/coupon_bulk_create.rb +14 -0
- data/lib/recurly/requests/coupon_create.rb +102 -0
- data/lib/recurly/requests/coupon_pricing.rb +18 -0
- data/lib/recurly/requests/coupon_redemption_create.rb +22 -0
- data/lib/recurly/requests/coupon_update.rb +34 -0
- data/lib/recurly/requests/custom_field.rb +18 -0
- data/lib/recurly/requests/dunning_campaigns_bulk_update.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 +22 -0
- data/lib/recurly/requests/invoice_create.rb +42 -0
- data/lib/recurly/requests/invoice_refund.rb +34 -0
- data/lib/recurly/requests/invoice_update.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 +86 -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/percentage_tier.rb +18 -0
- data/lib/recurly/requests/percentage_tiers_by_currency.rb +18 -0
- data/lib/recurly/requests/plan_create.rb +102 -0
- data/lib/recurly/requests/plan_hosted_pages.rb +26 -0
- data/lib/recurly/requests/plan_pricing.rb +26 -0
- data/lib/recurly/requests/plan_update.rb +94 -0
- data/lib/recurly/requests/pricing.rb +22 -0
- data/lib/recurly/requests/purchase_create.rb +78 -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 +46 -0
- data/lib/recurly/requests/subscription_add_on_percentage_tier.rb +18 -0
- data/lib/recurly/requests/subscription_add_on_tier.rb +26 -0
- data/lib/recurly/requests/subscription_add_on_update.rb +50 -0
- data/lib/recurly/requests/subscription_cancel.rb +14 -0
- data/lib/recurly/requests/subscription_change_billing_info_create.rb +14 -0
- data/lib/recurly/requests/subscription_change_create.rb +74 -0
- data/lib/recurly/requests/subscription_change_shipping_create.rb +30 -0
- data/lib/recurly/requests/subscription_create.rb +114 -0
- data/lib/recurly/requests/subscription_pause.rb +14 -0
- data/lib/recurly/requests/subscription_purchase.rb +70 -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 +70 -0
- data/lib/recurly/requests/tier.rb +22 -0
- data/lib/recurly/requests/tier_pricing.rb +22 -0
- data/lib/recurly/requests/usage_create.rb +26 -0
- data/lib/recurly/requests.rb +8 -0
- data/lib/recurly/resource.rb +23 -1092
- data/lib/recurly/resources/account.rb +138 -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 +22 -0
- data/lib/recurly/resources/account_mini.rb +50 -0
- data/lib/recurly/resources/account_note.rb +34 -0
- data/lib/recurly/resources/add_on.rb +122 -0
- data/lib/recurly/resources/add_on_mini.rb +54 -0
- data/lib/recurly/resources/add_on_pricing.rb +26 -0
- data/lib/recurly/resources/address.rb +38 -0
- data/lib/recurly/resources/address_with_name.rb +46 -0
- data/lib/recurly/resources/billing_info.rb +74 -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 +126 -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 +54 -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/dunning_campaign.rb +50 -0
- data/lib/recurly/resources/dunning_campaigns_bulk_update_response.rb +18 -0
- data/lib/recurly/resources/dunning_cycle.rb +58 -0
- data/lib/recurly/resources/dunning_interval.rb +18 -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 +162 -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/invoice_template.rb +34 -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 +206 -0
- data/lib/recurly/resources/measured_unit.rb +46 -0
- data/lib/recurly/resources/payment_method.rb +70 -0
- data/lib/recurly/resources/percentage_tier.rb +18 -0
- data/lib/recurly/resources/percentage_tiers_by_currency.rb +18 -0
- data/lib/recurly/resources/plan.rb +122 -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 +26 -0
- data/lib/recurly/resources/pricing.rb +22 -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 +198 -0
- data/lib/recurly/resources/subscription_add_on.rb +78 -0
- data/lib/recurly/resources/subscription_add_on_percentage_tier.rb +18 -0
- data/lib/recurly/resources/subscription_add_on_tier.rb +26 -0
- data/lib/recurly/resources/subscription_change.rb +82 -0
- data/lib/recurly/resources/subscription_change_billing_info.rb +14 -0
- data/lib/recurly/resources/subscription_shipping.rb +26 -0
- data/lib/recurly/resources/tax_detail.rb +26 -0
- data/lib/recurly/resources/tax_info.rb +26 -0
- data/lib/recurly/resources/tier.rb +22 -0
- data/lib/recurly/resources/tier_pricing.rb +22 -0
- data/lib/recurly/resources/transaction.rb +162 -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/unique_coupon_code_params.rb +26 -0
- data/lib/recurly/resources/usage.rb +78 -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 -138
- data/openapi/api.yaml +22879 -0
- data/recurly.gemspec +39 -0
- data/scripts/build +5 -0
- data/scripts/clean +6 -0
- data/scripts/format +12 -0
- data/scripts/prepare-release +50 -0
- data/scripts/release +17 -0
- data/scripts/test +15 -0
- metadata +217 -217
- data/lib/recurly/account.rb +0 -179
- 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 -208
- 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 -32
- 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 -82
- data/lib/recurly/helper.rb +0 -51
- data/lib/recurly/invoice.rb +0 -273
- 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/note.rb +0 -14
- 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 -313
- data/lib/recurly/shipping_address.rb +0 -26
- data/lib/recurly/subscription/add_ons.rb +0 -77
- data/lib/recurly/subscription.rb +0 -328
- 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 -129
- data/lib/recurly/usage.rb +0 -28
- 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/low_balance_gift_card_notification.rb +0 -6
- 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/transaction_status_updated_notification.rb +0 -6
- 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_invoice_notification.rb +0 -6
- 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 -91
- data/lib/recurly/xml/nokogiri.rb +0 -60
- data/lib/recurly/xml/rexml.rb +0 -52
- data/lib/recurly/xml.rb +0 -122
@@ -0,0 +1,400 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "erb"
|
3
|
+
require "net/https"
|
4
|
+
require "base64"
|
5
|
+
require "securerandom"
|
6
|
+
require "uri"
|
7
|
+
require_relative "./schema/json_parser"
|
8
|
+
require_relative "./schema/file_parser"
|
9
|
+
|
10
|
+
module Recurly
|
11
|
+
class Client
|
12
|
+
require_relative "./client/operations"
|
13
|
+
|
14
|
+
API_HOSTS = {
|
15
|
+
us: "https://v3.recurly.com",
|
16
|
+
eu: "https://v3.eu.recurly.com",
|
17
|
+
}
|
18
|
+
REGION = :us
|
19
|
+
CA_FILE = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt")
|
20
|
+
BINARY_TYPES = [
|
21
|
+
"application/pdf",
|
22
|
+
].freeze
|
23
|
+
JSON_CONTENT_TYPE = "application/json"
|
24
|
+
MAX_RETRIES = 3
|
25
|
+
LOG_LEVELS = %i(debug info warn error fatal).freeze
|
26
|
+
BASE36_ALPHABET = (("0".."9").to_a + ("a".."z").to_a).freeze
|
27
|
+
ALLOWED_OPTIONS = [
|
28
|
+
:site_id,
|
29
|
+
:open_timeout,
|
30
|
+
:read_timeout,
|
31
|
+
:body,
|
32
|
+
:params,
|
33
|
+
:headers,
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
# Initialize a client. It requires an API key.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# API_KEY = '83749879bbde395b5fe0cc1a5abf8e5'
|
40
|
+
# client = Recurly::Client.new(api_key: API_KEY)
|
41
|
+
# sub = client.get_subscription(subscription_id: 'abcd123456')
|
42
|
+
# @example
|
43
|
+
# # You can also pass the initializer a block. This will give you
|
44
|
+
# # a client scoped for just that block
|
45
|
+
# Recurly::Client.new(api_key: API_KEY) do |client|
|
46
|
+
# sub = client.get_subscription(subscription_id: 'abcd123456')
|
47
|
+
# end
|
48
|
+
# @example
|
49
|
+
# # If you only plan on using the client for more than one site,
|
50
|
+
# # you should initialize a new client for each site.
|
51
|
+
#
|
52
|
+
# client = Recurly::Client.new(api_key: API_KEY1)
|
53
|
+
# sub = client.get_subscription(subscription_id: 'uuid-abcd123456')
|
54
|
+
#
|
55
|
+
# # you should create a new client to connect to another site
|
56
|
+
# client = Recurly::Client.new(api_key: API_KEY2)
|
57
|
+
# sub = client.get_subscription(subscription_id: 'uuid-abcd7890')
|
58
|
+
#
|
59
|
+
# @param region [String] The DataCenter that is called by the API. Default to "us"
|
60
|
+
# @param base_url [String] The base URL for the API. Defaults to "https://v3.recurly.com"
|
61
|
+
# @param ca_file [String] The CA bundle to use when connecting to the API. Defaults to "data/ca-certificates.crt"
|
62
|
+
# @param api_key [String] The private API key
|
63
|
+
# @param logger [Logger] A logger to use. Defaults to creating a new STDOUT logger with level WARN.
|
64
|
+
def initialize(region: REGION, base_url: API_HOSTS[:us], ca_file: CA_FILE, api_key:, logger: nil)
|
65
|
+
raise ArgumentError, "'api_key' must be set to a non-nil value" if api_key.nil?
|
66
|
+
|
67
|
+
raise ArgumentError, "Invalid region type. Expected one of: #{API_HOSTS.keys.join(", ")}" if !API_HOSTS.key?(region)
|
68
|
+
|
69
|
+
base_url = API_HOSTS[region] if base_url == API_HOSTS[:us] && API_HOSTS.key?(region)
|
70
|
+
|
71
|
+
set_api_key(api_key)
|
72
|
+
set_connection_options(base_url, ca_file)
|
73
|
+
|
74
|
+
if logger.nil?
|
75
|
+
@logger = Logger.new(STDOUT).tap do |l|
|
76
|
+
l.level = Logger::WARN
|
77
|
+
end
|
78
|
+
else
|
79
|
+
unless LOG_LEVELS.all? { |lev| logger.respond_to?(lev) }
|
80
|
+
raise ArgumentError, "You must pass in a logger implementation that responds to the following messages: #{LOG_LEVELS}"
|
81
|
+
end
|
82
|
+
@logger = logger
|
83
|
+
end
|
84
|
+
|
85
|
+
if @logger.level < Logger::INFO
|
86
|
+
msg = <<-MSG
|
87
|
+
The Recurly logger should not be initialized
|
88
|
+
beyond the level INFO. It is currently configured to emit
|
89
|
+
headers and request / response bodies. This has the potential to leak
|
90
|
+
PII and other sensitive information and should never be used in production.
|
91
|
+
MSG
|
92
|
+
log_warn("SECURITY_WARNING", message: msg)
|
93
|
+
end
|
94
|
+
|
95
|
+
# execute block with this client if given
|
96
|
+
yield(self) if block_given?
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
# Used by the operations.rb file to interpolate paths
|
102
|
+
attr_reader :site_id
|
103
|
+
|
104
|
+
def pager(path, **options)
|
105
|
+
Pager.new(
|
106
|
+
client: self,
|
107
|
+
path: path,
|
108
|
+
options: options,
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def head(path, **options)
|
113
|
+
validate_options!(**options)
|
114
|
+
request = Net::HTTP::Head.new build_url(path, options)
|
115
|
+
set_headers(request, options[:headers])
|
116
|
+
http_response = run_request(request, options)
|
117
|
+
handle_response! request, http_response
|
118
|
+
end
|
119
|
+
|
120
|
+
def get(path, **options)
|
121
|
+
validate_options!(**options)
|
122
|
+
|
123
|
+
request = Net::HTTP::Get.new build_url(path, options)
|
124
|
+
|
125
|
+
set_headers(request, options[:headers])
|
126
|
+
http_response = run_request(request, options)
|
127
|
+
handle_response! request, http_response
|
128
|
+
end
|
129
|
+
|
130
|
+
def post(path, request_data = nil, request_class = nil, **options)
|
131
|
+
validate_options!(**options)
|
132
|
+
request = Net::HTTP::Post.new build_url(path, options)
|
133
|
+
request.set_content_type(JSON_CONTENT_TYPE)
|
134
|
+
set_headers(request, options[:headers])
|
135
|
+
if request_data
|
136
|
+
request_class.new(request_data).validate!
|
137
|
+
request.body = JSON.dump(request_data)
|
138
|
+
end
|
139
|
+
http_response = run_request(request, options)
|
140
|
+
handle_response! request, http_response
|
141
|
+
end
|
142
|
+
|
143
|
+
def put(path, request_data = nil, request_class = nil, **options)
|
144
|
+
validate_options!(**options)
|
145
|
+
request = Net::HTTP::Put.new build_url(path, options)
|
146
|
+
request.set_content_type(JSON_CONTENT_TYPE)
|
147
|
+
set_headers(request, options[:headers])
|
148
|
+
if request_data
|
149
|
+
request_class.new(request_data).validate!
|
150
|
+
json_body = JSON.dump(request_data)
|
151
|
+
request.body = json_body
|
152
|
+
end
|
153
|
+
http_response = run_request(request, options)
|
154
|
+
handle_response! request, http_response
|
155
|
+
end
|
156
|
+
|
157
|
+
def delete(path, **options)
|
158
|
+
validate_options!(**options)
|
159
|
+
request = Net::HTTP::Delete.new build_url(path, options)
|
160
|
+
set_headers(request, options[:headers])
|
161
|
+
http_response = run_request(request, options)
|
162
|
+
handle_response! request, http_response
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
@connection_pool = Recurly::ConnectionPool.new
|
168
|
+
|
169
|
+
class << self
|
170
|
+
# @return [Recurly::ConnectionPool]
|
171
|
+
attr_accessor :connection_pool
|
172
|
+
end
|
173
|
+
|
174
|
+
def run_request(request, options = {})
|
175
|
+
self.class.connection_pool.with_connection(uri: @base_uri, ca_file: @ca_file) do |http|
|
176
|
+
set_http_options(http, options)
|
177
|
+
|
178
|
+
retries = 0
|
179
|
+
|
180
|
+
begin
|
181
|
+
http.start unless http.started?
|
182
|
+
|
183
|
+
log_attrs = {
|
184
|
+
method: request.method,
|
185
|
+
path: request.path,
|
186
|
+
}
|
187
|
+
if @logger.level < Logger::INFO
|
188
|
+
log_attrs[:request_body] = request.body
|
189
|
+
# No need to log the authorization header
|
190
|
+
headers = request.to_hash.reject { |k, _| k&.downcase == "authorization" }
|
191
|
+
log_attrs[:request_headers] = headers
|
192
|
+
end
|
193
|
+
|
194
|
+
log_info("Request", **log_attrs)
|
195
|
+
start = Time.now
|
196
|
+
response = http.request(request)
|
197
|
+
elapsed = Time.now - start
|
198
|
+
|
199
|
+
# GETs are safe to retry after a server error, requests with an Idempotency-Key will return the prior response
|
200
|
+
if response.kind_of?(Net::HTTPServerError) && request.is_a?(Net::HTTP::Get)
|
201
|
+
retries += 1
|
202
|
+
log_info("Retrying", retries: retries, **log_attrs)
|
203
|
+
start = Time.now
|
204
|
+
response = http.request(request) if retries < MAX_RETRIES
|
205
|
+
elapsed = Time.now - start
|
206
|
+
end
|
207
|
+
|
208
|
+
if @logger.level < Logger::INFO
|
209
|
+
log_attrs[:response_body] = response.body
|
210
|
+
log_attrs[:response_headers] = response.to_hash
|
211
|
+
end
|
212
|
+
log_info("Response", time_ms: (elapsed * 1_000).floor, status: response.code, **log_attrs)
|
213
|
+
|
214
|
+
response
|
215
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ECONNABORTED,
|
216
|
+
Errno::EPIPE, Errno::ETIMEDOUT, Net::OpenTimeout, EOFError, SocketError => ex
|
217
|
+
retries += 1
|
218
|
+
if retries < MAX_RETRIES
|
219
|
+
retry
|
220
|
+
end
|
221
|
+
|
222
|
+
if ex.kind_of?(Net::OpenTimeout) || ex.kind_of?(Errno::ETIMEDOUT)
|
223
|
+
raise Recurly::Errors::TimeoutError, "Request timed out"
|
224
|
+
end
|
225
|
+
|
226
|
+
raise Recurly::Errors::ConnectionFailedError, "Failed to connect to Recurly: #{ex.message}"
|
227
|
+
rescue Timeout::Error
|
228
|
+
raise Recurly::Errors::TimeoutError, "Request timed out"
|
229
|
+
rescue OpenSSL::SSL::SSLError => ex
|
230
|
+
raise Recurly::Errors::SSLError, ex.message
|
231
|
+
rescue StandardError => ex
|
232
|
+
raise Recurly::Errors::NetworkError, ex.message
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def set_headers(request, additional_headers = {})
|
238
|
+
# TODO this is undocumented until we finalize it
|
239
|
+
additional_headers.each { |header, v| request[header] = v } if additional_headers
|
240
|
+
|
241
|
+
request["Accept"] = "application/vnd.recurly.#{api_version}".chomp # got this method from operations.rb
|
242
|
+
request["Authorization"] = "Basic #{Base64.encode64(@api_key)}".chomp
|
243
|
+
request["User-Agent"] = "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}"
|
244
|
+
|
245
|
+
unless request.is_a?(Net::HTTP::Get) || request.is_a?(Net::HTTP::Head)
|
246
|
+
request["Idempotency-Key"] ||= generate_idempotency_key
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# from https://github.com/rails/rails/blob/6-0-stable/activesupport/lib/active_support/core_ext/securerandom.rb
|
251
|
+
def generate_idempotency_key(n = 16)
|
252
|
+
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
|
253
|
+
idx = byte % 64
|
254
|
+
idx = SecureRandom.random_number(36) if idx >= 36
|
255
|
+
BASE36_ALPHABET[idx]
|
256
|
+
end.join
|
257
|
+
end
|
258
|
+
|
259
|
+
def set_http_options(http, options)
|
260
|
+
http.open_timeout = options[:open_timeout] || 20
|
261
|
+
http.read_timeout = options[:read_timeout] || 60
|
262
|
+
end
|
263
|
+
|
264
|
+
def handle_response!(request, http_response)
|
265
|
+
response = HTTP::Response.new(http_response, request)
|
266
|
+
raise_api_error!(http_response, response) unless http_response.kind_of?(Net::HTTPSuccess)
|
267
|
+
resource = if response.body
|
268
|
+
if http_response.content_type&.include?(JSON_CONTENT_TYPE)
|
269
|
+
JSONParser.parse(self, response.body)
|
270
|
+
elsif BINARY_TYPES.include?(http_response.content_type)
|
271
|
+
FileParser.parse(response.body)
|
272
|
+
else
|
273
|
+
raise Recurly::Errors::InvalidContentTypeError, "Unexpected content type: #{http_response.content_type}"
|
274
|
+
end
|
275
|
+
else
|
276
|
+
Resources::Empty.new
|
277
|
+
end
|
278
|
+
# Keep this interface "private"
|
279
|
+
resource.instance_variable_set(:@response, response)
|
280
|
+
resource
|
281
|
+
end
|
282
|
+
|
283
|
+
def raise_api_error!(http_response, response)
|
284
|
+
if response.content_type.include?(JSON_CONTENT_TYPE)
|
285
|
+
error = JSONParser.parse(self, response.body)
|
286
|
+
begin
|
287
|
+
error_class = Errors::APIError.error_class(error.type)
|
288
|
+
raise error_class.new(error.message, response, error)
|
289
|
+
rescue NameError
|
290
|
+
error_class = Errors::APIError.from_response(http_response)
|
291
|
+
raise error_class.new("Unknown Error", response, error)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
error_class = Errors::APIError.from_response(http_response)
|
296
|
+
|
297
|
+
if error_class <= Recurly::Errors::APIError
|
298
|
+
error = Recurly::Resources::Error.new(message: "#{http_response.code}: #{http_response.message}")
|
299
|
+
raise error_class.new(error.message, response, error)
|
300
|
+
else
|
301
|
+
raise error_class, "#{http_response.code}: #{http_response.message}"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def read_headers(response)
|
306
|
+
if !@_ignore_deprecation_warning && response.headers["Recurly-Deprecated"]&.upcase == "TRUE"
|
307
|
+
log_warn("DEPRECTATION WARNING", message: "Your current API version \"#{api_version}\" is deprecated and will be sunset on #{response.headers["Recurly-Sunset-Date"]}")
|
308
|
+
end
|
309
|
+
response
|
310
|
+
end
|
311
|
+
|
312
|
+
def validate_options!(**options)
|
313
|
+
invalid_options = options.keys.reject do |k|
|
314
|
+
ALLOWED_OPTIONS.include?(k)
|
315
|
+
end
|
316
|
+
if invalid_options.any?
|
317
|
+
joinedKeys = invalid_options.join(", ")
|
318
|
+
joinedOptions = ALLOWED_OPTIONS.join(", ")
|
319
|
+
raise ArgumentError, "Invalid options: '#{joinedKeys}'. Allowed options: '#{joinedOptions}'"
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def validate_path_parameters!(**options)
|
324
|
+
# Check to see that we are passing the correct data types
|
325
|
+
# This prevents a confusing error if the user passes in a non-primitive by mistake
|
326
|
+
options.each do |k, v|
|
327
|
+
unless [String, Symbol, Integer, Float].include?(v.class)
|
328
|
+
message = "We cannot build the url with the given argument #{k}=#{v.inspect}."
|
329
|
+
if k =~ /_id$/
|
330
|
+
message << " Since this appears to be an id, perhaps you meant to pass in a String?"
|
331
|
+
end
|
332
|
+
raise ArgumentError, message
|
333
|
+
end
|
334
|
+
end
|
335
|
+
# Check to make sure that parameters are not empty string values
|
336
|
+
empty_strings = options.select { |_, v| v.is_a?(String) && v.strip.empty? }
|
337
|
+
if empty_strings.any?
|
338
|
+
raise ArgumentError, "#{empty_strings.keys.join(", ")} cannot be an empty string"
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def interpolate_path(path, **options)
|
343
|
+
validate_path_parameters!(**options)
|
344
|
+
options.each do |k, v|
|
345
|
+
# We need to encode the values for the url
|
346
|
+
options[k] = ERB::Util.url_encode(v.to_s)
|
347
|
+
end
|
348
|
+
path = path.gsub("{", "%{")
|
349
|
+
path % options
|
350
|
+
end
|
351
|
+
|
352
|
+
def set_api_key(api_key)
|
353
|
+
@api_key = api_key.to_s
|
354
|
+
end
|
355
|
+
|
356
|
+
def set_connection_options(base_url, ca_file)
|
357
|
+
@base_uri = URI.parse(base_url)
|
358
|
+
@ca_file = ca_file
|
359
|
+
end
|
360
|
+
|
361
|
+
def build_url(path, options)
|
362
|
+
path = scope_by_site(path, options)
|
363
|
+
query_params = map_array_params(options.fetch(:params, {}))
|
364
|
+
if query_params.any?
|
365
|
+
"#{path}?#{URI.encode_www_form(query_params)}"
|
366
|
+
else
|
367
|
+
path
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Converts array parameters to CSV strings to maintain consistency with
|
372
|
+
# how the server expects the request to be formatted while providing the
|
373
|
+
# developer with an array type to maintain developer happiness!
|
374
|
+
def map_array_params(params)
|
375
|
+
params.map do |key, param|
|
376
|
+
[key, param.is_a?(Array) ? param.join(",") : param]
|
377
|
+
end.to_h
|
378
|
+
end
|
379
|
+
|
380
|
+
def scope_by_site(path, options)
|
381
|
+
if site = site_id || options[:site_id]
|
382
|
+
# Ensure that we are only including the site_id once because the Pager operations
|
383
|
+
# will use the cursor returned from the API which may already have these components
|
384
|
+
path.start_with?("/sites/#{site}") ? path : "/sites/#{site}#{path}"
|
385
|
+
else
|
386
|
+
path
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Define a private `log_<level>` method for each log level
|
391
|
+
LOG_LEVELS.each do |level|
|
392
|
+
define_method "log_#{level}" do |tag, **attrs|
|
393
|
+
@logger.send(level, "Recurly") do
|
394
|
+
msg = attrs.each_pair.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
395
|
+
"[#{tag}] #{msg}"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/https"
|
4
|
+
|
5
|
+
module Recurly
|
6
|
+
class ConnectionPool
|
7
|
+
def initialize
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@pool = Hash.new { |h, k| h[k] = [] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_connection(uri:, ca_file: nil)
|
13
|
+
http = nil
|
14
|
+
@mutex.synchronize do
|
15
|
+
http = @pool[[uri.host, uri.port]].pop
|
16
|
+
end
|
17
|
+
|
18
|
+
# create connection if the pool was empty
|
19
|
+
http ||= init_http_connection(uri, ca_file)
|
20
|
+
|
21
|
+
response = yield http
|
22
|
+
|
23
|
+
if http.started?
|
24
|
+
@mutex.synchronize do
|
25
|
+
@pool[[uri.host, uri.port]].push(http)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
def init_http_connection(uri, ca_file)
|
33
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
34
|
+
http.use_ssl = uri.scheme == "https"
|
35
|
+
http.ca_file = ca_file
|
36
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
37
|
+
http.keep_alive_timeout = 600
|
38
|
+
|
39
|
+
http
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,90 @@
|
|
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
|
+
"504" => "TimeoutError",
|
12
|
+
"304" => "NotModifiedError",
|
13
|
+
"400" => "BadRequestError",
|
14
|
+
"401" => "UnauthorizedError",
|
15
|
+
"402" => "PaymentRequiredError",
|
16
|
+
"403" => "ForbiddenError",
|
17
|
+
"404" => "NotFoundError",
|
18
|
+
"406" => "NotAcceptableError",
|
19
|
+
"412" => "PreconditionFailedError",
|
20
|
+
"422" => "UnprocessableEntityError",
|
21
|
+
"429" => "TooManyRequestsError",
|
22
|
+
}
|
23
|
+
|
24
|
+
class ResponseError < Errors::APIError; end
|
25
|
+
|
26
|
+
class ServerError < ResponseError; end
|
27
|
+
|
28
|
+
class InternalServerError < ServerError; end
|
29
|
+
|
30
|
+
class ServiceNotAvailableError < InternalServerError; end
|
31
|
+
|
32
|
+
class TaxServiceError < InternalServerError; end
|
33
|
+
|
34
|
+
class BadGatewayError < ServerError; end
|
35
|
+
|
36
|
+
class ServiceUnavailableError < ServerError; end
|
37
|
+
|
38
|
+
class TimeoutError < ServerError; end
|
39
|
+
|
40
|
+
class RedirectionError < ResponseError; end
|
41
|
+
|
42
|
+
class NotModifiedError < ResponseError; end
|
43
|
+
|
44
|
+
class ClientError < Errors::APIError; end
|
45
|
+
|
46
|
+
class BadRequestError < ClientError; end
|
47
|
+
|
48
|
+
class InvalidContentTypeError < BadRequestError; end
|
49
|
+
|
50
|
+
class UnauthorizedError < ClientError; end
|
51
|
+
|
52
|
+
class PaymentRequiredError < ClientError; end
|
53
|
+
|
54
|
+
class ForbiddenError < ClientError; end
|
55
|
+
|
56
|
+
class InvalidApiKeyError < ForbiddenError; end
|
57
|
+
|
58
|
+
class InvalidPermissionsError < ForbiddenError; end
|
59
|
+
|
60
|
+
class NotFoundError < ClientError; end
|
61
|
+
|
62
|
+
class NotAcceptableError < ClientError; end
|
63
|
+
|
64
|
+
class UnknownApiVersionError < NotAcceptableError; end
|
65
|
+
|
66
|
+
class UnavailableInApiVersionError < NotAcceptableError; end
|
67
|
+
|
68
|
+
class InvalidApiVersionError < NotAcceptableError; end
|
69
|
+
|
70
|
+
class PreconditionFailedError < ClientError; end
|
71
|
+
|
72
|
+
class UnprocessableEntityError < ClientError; end
|
73
|
+
|
74
|
+
class ValidationError < UnprocessableEntityError; end
|
75
|
+
|
76
|
+
class MissingFeatureError < UnprocessableEntityError; end
|
77
|
+
|
78
|
+
class TransactionError < UnprocessableEntityError; end
|
79
|
+
|
80
|
+
class SimultaneousRequestError < UnprocessableEntityError; end
|
81
|
+
|
82
|
+
class ImmutableSubscriptionError < UnprocessableEntityError; end
|
83
|
+
|
84
|
+
class InvalidTokenError < UnprocessableEntityError; end
|
85
|
+
|
86
|
+
class TooManyRequestsError < ClientError; end
|
87
|
+
|
88
|
+
class RateLimitedError < TooManyRequestsError; end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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]
|
25
|
+
def self.from_response(response)
|
26
|
+
if Recurly::Errors::ERROR_MAP.has_key?(response.code)
|
27
|
+
Recurly::Errors.const_get(Recurly::Errors::ERROR_MAP[response.code])
|
28
|
+
else
|
29
|
+
Recurly::Errors::APIError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(message, response = nil, error = nil)
|
34
|
+
super(message)
|
35
|
+
@response = response
|
36
|
+
@recurly_error = error
|
37
|
+
end
|
38
|
+
|
39
|
+
def status_code
|
40
|
+
@response.status
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_response
|
44
|
+
@response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
require_relative "./errors/api_errors"
|
50
|
+
require_relative "./errors/network_errors"
|
51
|
+
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
|