module Stripe::Util
def self.check_api_key!(key)
def self.check_api_key!(key) raise TypeError, "api_key must be a string" unless key.is_a?(String) key end
def self.check_string_argument!(key)
def self.check_string_argument!(key) raise TypeError, "argument must be a string" unless key.is_a?(String) key end
def self.colorize(val, color, isatty)
Uses an ANSI escape code to colorize text if it's going to be sent to a
def self.colorize(val, color, isatty) return val unless isatty mode = 0 # default foreground = 30 + COLOR_CODES.fetch(color) background = 40 + COLOR_CODES.fetch(:default) "\033[#{mode};#{foreground};#{background}m#{val}\033[0m" end
def self.convert_to_stripe_object(data, opts = {})
* +opts+ - Options for +StripeObject+ like an API key that will be reused
will be reused on subsequent API calls.
* +params+ - Params for +StripeObject+ like filters used in search that
* +data+ - Hash of fields and values to be converted into a StripeObject.
==== Attributes
to being a +StripeObject+.
present or of an unknown type, the newly created instance will fall back
`charge` would create an instance of +Charge+), but if `object` is not
type as dictated by their `object` field (e.g. an `object` value of
array of +StripeObject+s. These new objects will be created as a concrete
Converts a hash of fields or an array of hashes into a +StripeObject+ or
def self.convert_to_stripe_object(data, opts = {}) convert_to_stripe_object_with_params(data, {}, opts) end
def self.convert_to_stripe_object_with_params(data, params, opts = {})
* +opts+ - Options for +StripeObject+ like an API key that will be reused
* +data+ - Hash of fields and values to be converted into a StripeObject.
==== Attributes
to being a +StripeObject+.
present or of an unknown type, the newly created instance will fall back
`charge` would create an instance of +Charge+), but if `object` is not
type as dictated by their `object` field (e.g. an `object` value of
array of +StripeObject+s. These new objects will be created as a concrete
Converts a hash of fields or an array of hashes into a +StripeObject+ or
def self.convert_to_stripe_object_with_params(data, params, opts = {}) opts = normalize_opts(opts) case data when Array 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_name = data[:object] || data["object"] obj = object_classes.fetch(object_name, StripeObject) .construct_from(data, opts) # set filters so that we can fetch the same limit, expansions, and # predicates when accessing the next and previous pages if obj && (obj.is_a?(SearchResultObject) || obj.is_a?(ListObject)) obj.filters = params.dup end obj else data end end
def self.custom_method(resource, target, name, http_verb, http_path)
adds a `capture` class method to the resource class that, when called,
custom_method :capture, http_verb: post
For example, this call:
- target: the class that custom static method will be added to
- resource: the resource implementation class
the name is used as the path
- http_path: the path to append to the resource's URL. If not provided,
- http_verb: the HTTP verb for the API request (:get, :post, or :delete)
- name: the name of the custom method to create (as a symbol)
following parameters:
non-CRUDL API requests, e.g. capturing charges. custom_method takes the
Adds a custom method to a resource class. This is used to add support for
def self.custom_method(resource, target, name, http_verb, http_path) unless %i[get post delete].include?(http_verb) raise ArgumentError, "Invalid http_verb value: #{http_verb.inspect}. Should be one " \ "of :get, :post or :delete." end unless target.respond_to?(:resource_url) raise ArgumentError, "Invalid target value: #{target}. Target class should have a " \ "`resource_url` method." end http_path ||= name.to_s target.define_singleton_method(name) do |id, params = {}, opts = {}| unless id.is_a?(String) raise ArgumentError, "id should be a string representing the ID of an API resource" end url = "#{target.resource_url}/"\ "#{CGI.escape(id)}/"\ "#{CGI.escape(http_path)}" resp, opts = resource.execute_resource_request( http_verb, url, params, opts ) Util.convert_to_stripe_object_with_params(resp.data, params, opts) end end
def self.encode_parameters(params)
involves escaping special characters from parameter keys and values (e.g.
parameters in a URI or as form parameters in a request body. This mainly
Encodes a hash of parameters in a way that's suitable for use as query
def self.encode_parameters(params) Util.flatten_params(params) .map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join("&") end
def self.flatten_params(params, parent_key = nil)
def self.flatten_params(params, parent_key = nil) result = [] # do not sort the final output because arrays (and arrays of hashes # especially) can be order sensitive, but do sort incoming parameters params.each do |key, value| calculated_key = parent_key ? "#{parent_key}[#{key}]" : key.to_s if value.is_a?(Hash) result += flatten_params(value, calculated_key) elsif value.is_a?(Array) result += flatten_params_array(value, calculated_key) else result << [calculated_key, value] end end result end
def self.flatten_params_array(value, calculated_key)
def self.flatten_params_array(value, calculated_key) result = [] value.each_with_index do |elem, i| if elem.is_a?(Hash) result += flatten_params(elem, "#{calculated_key}[#{i}]") elsif elem.is_a?(Array) result += flatten_params_array(elem, calculated_key) else result << ["#{calculated_key}[#{i}]", elem] end end result end
def self.level_name(level)
def self.level_name(level) case level when LEVEL_DEBUG then "debug" when LEVEL_ERROR then "error" when LEVEL_INFO then "info" else level end end
def self.log_debug(message, data = {})
def self.log_debug(message, data = {}) config = data.delete(:config) || Stripe.config logger = config.logger || Stripe.logger if !logger.nil? || !config.log_level.nil? && config.log_level <= Stripe::LEVEL_DEBUG log_internal(message, data, color: :blue, level: Stripe::LEVEL_DEBUG, logger: Stripe.logger, out: $stdout) end end
def self.log_error(message, data = {})
def self.log_error(message, data = {}) config = data.delete(:config) || Stripe.config logger = config.logger || Stripe.logger if !logger.nil? || !config.log_level.nil? && config.log_level <= Stripe::LEVEL_ERROR log_internal(message, data, color: :cyan, level: Stripe::LEVEL_ERROR, logger: Stripe.logger, out: $stderr) end end
def self.log_info(message, data = {})
def self.log_info(message, data = {}) config = data.delete(:config) || Stripe.config logger = config.logger || Stripe.logger if !logger.nil? || !config.log_level.nil? && config.log_level <= Stripe::LEVEL_INFO log_internal(message, data, color: :cyan, level: Stripe::LEVEL_INFO, logger: Stripe.logger, out: $stdout) end end
def self.log_internal(message, data = {}, color:, level:, logger:, out:)
def self.log_internal(message, data = {}, color:, level:, logger:, out:) data_str = data.reject { |_k, v| v.nil? } .map do |(k, v)| format("%<key>s=%<value>s", key: colorize(k, color, logger.nil? && !out.nil? && out.isatty), value: wrap_logfmt_value(v)) end.join(" ") if !logger.nil? # the library's log levels are mapped to the same values as the # standard library's logger logger.log(level, format("message=%<message>s %<data_str>s", message: wrap_logfmt_value(message), data_str: data_str)) elsif out.isatty out.puts format("%<level>s %<message>s %<data_str>s", level: colorize(level_name(level)[0, 4].upcase, color, out.isatty), message: message, data_str: data_str) else out.puts format("message=%<message>s level=%<level>s %<data_str>s", message: wrap_logfmt_value(message), level: level_name(level), data_str: data_str) end end
def self.monotonic_time
and test stubbing. Returns time in seconds since the event used for
Shortcut for getting monotonic time, mostly for purposes of line length
to calculate an elapsed duration.
the use of the system's monotonic clock especially where comparing times
updating its value or a reconcilation via NTP. For this reason, prefer
`Time.now` can be unstable in cases like an administrator manually
def self.monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end
def self.normalize_headers(headers)
certain key values when the user could have set them with a variety of
example, `request-id` becomes `Request-Id`. This is useful for extracting
hyphen-delimited section starts with a single capitalized letter. For
Normalizes header keys so that they're all lower case and each
def self.normalize_headers(headers) headers.each_with_object({}) do |(k, v), new_headers| k = k.to_s.tr("_", "-") if k.is_a?(Symbol) k = k.split("-").reject(&:empty?).map(&:capitalize).join("-") new_headers[k] = v end end
def self.normalize_id(id)
def self.normalize_id(id) if id.is_a?(Hash) # overloaded id params_hash = id.dup id = params_hash.delete(:id) else params_hash = {} end [id, params_hash] end
def self.normalize_opts(opts)
The secondary opts argument can either be a string or hash
def self.normalize_opts(opts) case opts when String { api_key: opts } when Hash # If the user is using request signing for authentication, # no need to check the api_key per request. if !(opts.key?(:client) && opts.fetch(:client).config.authenticator) && opts.key?(:api_key) check_api_key!(opts.fetch(:api_key)) end # Explicitly use dup here instead of clone to avoid preserving freeze # state on input params. opts.dup else raise TypeError, "normalize_opts expects a string or a hash" end end
def self.object_classes
def self.object_classes @object_classes ||= Stripe::ObjectTypes.object_names_to_classes end
def self.object_name_matches_class?(object_name, klass)
def self.object_name_matches_class?(object_name, klass) Util.object_classes[object_name] == klass end
def self.objects_to_ids(obj)
def self.objects_to_ids(obj) case obj when APIResource obj.id when Hash res = {} obj.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? } res when Array obj.map { |v| objects_to_ids(v) } else obj end end
def self.request_id_dashboard_url(request_id, api_key)
ID value and an API key, which is used to attempt to extract whether the
Generates a Dashboard link to inspect a request ID based off of a request
def self.request_id_dashboard_url(request_id, api_key) env = !api_key.nil? && api_key.start_with?("sk_live") ? "live" : "test" "https://dashboard.stripe.com/#{env}/logs/#{request_id}" end
def self.secure_compare(str_a, str_b)
Constant time string comparison to prevent timing attacks
def self.secure_compare(str_a, str_b) return false unless str_a.bytesize == str_b.bytesize l = str_a.unpack "C#{str_a.bytesize}" res = 0 str_b.each_byte { |byte| res |= byte ^ l.shift } res.zero? end
def self.symbolize_names(object)
def self.symbolize_names(object) case object when Hash new_hash = {} object.each do |key, value| key = (begin key.to_sym rescue StandardError key end) || key new_hash[key] = symbolize_names(value) end new_hash when Array object.map { |value| symbolize_names(value) } else object end end
def self.url_encode(key)
query parameters in a URI or in a set of form parameters in a request
Encodes a string in a way that makes it suitable for use in a set of
def self.url_encode(key) CGI.escape(key.to_s). # Don't use strict form encoding by changing the square bracket control # characters back to their literals. This is fine by the server, and # makes these parameter strings easier to read. gsub("%5B", "[").gsub("%5D", "]") end
def self.wrap_logfmt_value(val)
Wraps a value in double quotes if it looks sufficiently complex so that
def self.wrap_logfmt_value(val) # If value is any kind of number, just allow it to be formatted directly # to a string (this will handle integers or floats). return val if val.is_a?(Numeric) # Hopefully val is a string, but protect in case it's not. val = val.to_s # Some values returned by the server are encoded in ASCII-8BIT before # being parsed as UTF-8 by Marshal. If we don't transform these here, then # puts will fail as it tries to render UTF-8 characters as ASCII-8BIT # which is not valid. if val && val.encoding == Encoding::ASCII_8BIT # Dup the string as it is a frozen literal. val = val.dup.force_encoding("UTF-8") end if %r{[^\w\-/]} =~ val # If the string contains any special characters, escape any double # quotes it has, remove newlines, and wrap the whole thing in quotes. format(%("%<value>s"), value: val.gsub('"', '\"').delete("\n")) else # Otherwise use the basic value if it looks like a standard set of # characters (and allow a few special characters like hyphens, and # slashes) val end end