lib/stripe/util.rb in stripe-3.0.3 vs lib/stripe/util.rb in stripe-3.1.0
- old
+ new
@@ -87,10 +87,24 @@
else
data
end
end
+ def self.log_info(message, data = {})
+ if Stripe.log_level == Stripe::LEVEL_DEBUG ||Stripe.log_level == Stripe::LEVEL_INFO
+ log_internal(message, data, color: :cyan,
+ level: Stripe::LEVEL_INFO, out: $stdout)
+ end
+ end
+
+ def self.log_debug(message, data = {})
+ if Stripe.log_level == Stripe::LEVEL_DEBUG
+ log_internal(message, data, color: :blue,
+ level: Stripe::LEVEL_DEBUG, out: $stdout)
+ end
+ end
+
def self.file_readable(file)
# This is nominally equivalent to File.readable?, but that can
# report incorrect results on some more oddball filesystems
# (such as AFS)
begin
@@ -218,12 +232,63 @@
def self.check_api_key!(key)
raise TypeError.new("api_key must be a string") unless key.is_a?(String)
key
end
+ # Normalizes header keys so that they're all lower case and each
+ # hyphen-delimited section starts with a single capitalized letter. For
+ # example, `request-id` becomes `Request-Id`. This is useful for extracting
+ # certain key values when the user could have set them with a variety of
+ # diffent naming schemes.
+ def self.normalize_headers(headers)
+ headers.inject({}) do |new_headers, (k, v)|
+ if k.is_a?(Symbol)
+ k = titlecase_parts(k.to_s.gsub("_", "-"))
+ elsif k.is_a?(String)
+ k = titlecase_parts(k)
+ end
+
+ new_headers[k] = v
+ new_headers
+ end
+ end
+
+ # Generates a Dashboard link to inspect a request ID based off of a request
+ # ID value and an API key, which is used to attempt to extract whether the
+ # environment is livemode or testmode.
+ 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
+
+ # Constant time string comparison to prevent timing attacks
+ # Code borrowed from ActiveSupport
+ def self.secure_compare(a, b)
+ return false unless a.bytesize == b.bytesize
+
+ l = a.unpack "C#{a.bytesize}"
+
+ res = 0
+ b.each_byte { |byte| res |= byte ^ l.shift }
+ res == 0
+ end
+
private
+ COLOR_CODES = {
+ :black => 0, :light_black => 60,
+ :red => 1, :light_red => 61,
+ :green => 2, :light_green => 62,
+ :yellow => 3, :light_yellow => 63,
+ :blue => 4, :light_blue => 64,
+ :magenta => 5, :light_magenta => 65,
+ :cyan => 6, :light_cyan => 66,
+ :white => 7, :light_white => 67,
+ :default => 9
+ }
+ private_constant :COLOR_CODES
+
# We use a pretty janky version of form encoding (Rack's) that supports
# more complex data structures like maps and arrays through the use of
# specialized syntax. To encode an array of maps like:
#
# [{a: 1, b: 2}, {a: 3, b: 4}]
@@ -256,19 +321,73 @@
else
expected_key = first_key
end
end
end
+ private_class_method :check_array_of_maps_start_keys!
- # Constant time string comparison to prevent timing attacks
- # Code borrowed from ActiveSupport
- def self.secure_compare(a, b)
- return false unless a.bytesize == b.bytesize
+ # Uses an ANSI escape code to colorize text if it's going to be sent to a
+ # TTY.
+ def self.colorize(val, color, isatty)
+ return val unless isatty
- l = a.unpack "C#{a.bytesize}"
+ mode = 0 # default
+ foreground = 30 + COLOR_CODES.fetch(color)
+ background = 40 + COLOR_CODES.fetch(:default)
- res = 0
- b.each_byte { |byte| res |= byte ^ l.shift }
- res == 0
+ "\033[#{mode};#{foreground};#{background}m#{val}\033[0m"
end
+ private_class_method :colorize
+
+ # TODO: Make these named required arguments when we drop support for Ruby
+ # 2.0.
+ def self.log_internal(message, data = {}, color: nil, level: nil, out: nil)
+ data_str = data.select { |k,v| !v.nil? }.
+ map { |(k,v)|
+ "%s=%s" % [
+ colorize(k, color, out.isatty),
+ wrap_logfmt_value(v)
+ ]
+ }.join(" ")
+
+ if out.isatty
+ out.puts "%s %s %s" %
+ [colorize(level[0, 4].upcase, color, out.isatty), message, data_str]
+ else
+ out.puts "message=%s level=%s %s" %
+ [wrap_logfmt_value(message), level, data_str]
+ end
+ end
+ private_class_method :log_internal
+
+ def self.titlecase_parts(s)
+ s.split("-").
+ select { |p| p != "" }.
+ map { |p| p[0].upcase + p[1..-1].downcase }.
+ join("-")
+ end
+ private_class_method :titlecase_parts
+
+ # Wraps a value in double quotes if it looks sufficiently complex so that
+ # it can be read by logfmt parsers.
+ 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
+
+ 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.
+ %{"%s"} % val.gsub('"', '\"').gsub("\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
+ private_class_method :wrap_logfmt_value
end
end