module Helio::Util
Constants
- COLOR_CODES
private
- OPTS_COPYABLE
Options that should be copyable from one
HelioObject
to another including options that may be internal.- OPTS_PERSISTABLE
Options that should be persisted between API requests. This includes client, which is an object containing an HTTP client to reuse.
- OPTS_USER_SPECIFIED
Options that a user is allowed to specify.
Public Class Methods
Transforms an array into a hash with integer keys. Used for a small number of API endpoints. If the argument is not an Array, return it unchanged. Example: [{foo: 'bar'}] => {“0” => {foo: “bar”}}
# File lib/helio/util.rb, line 145 def self.array_to_hash(array) case array when Array hash = {} array.each_with_index { |v, i| hash[i.to_s] = v } hash else array end end
# File lib/helio/util.rb, line 230 def self.check_api_token!(key) raise TypeError, "api_token must be a string" unless key.is_a?(String) key end
# File lib/helio/util.rb, line 225 def self.check_string_argument!(key) raise TypeError, "argument must be a string" unless key.is_a?(String) key end
Converts a hash of fields or an array of hashes into a HelioObject
or array of +HelioObject+s. These new objects will be created as a concrete type as dictated by their `object` field (e.g. an `object` value of `charge` would create an instance of Charge
), but if `object` is not present or of an unknown type, the newly created instance will fall back to being a HelioObject
.
Attributes¶ ↑
-
data
- Hash of fields and values to be converted into aHelioObject
. -
opts
- Options forHelioObject
like an API key that will be reused on subsequent API calls.
# File lib/helio/util.rb, line 65 def self.convert_to_helio_object(data, opts = {}) case data when Array data.map { |i| convert_to_helio_object(i, opts) } when Hash # Try converting to a known object class. If none available, fall back to generic HelioObject object_classes.fetch(data[:object_type], HelioObject).construct_from(data, opts) else data end end
Encodes a hash of parameters in a way that's suitable for use as query parameters in a URI or as form parameters in a request body. This mainly involves escaping special characters from parameter keys and values (e.g. `&`).
# File lib/helio/util.rb, line 137 def self.encode_parameters(params) Util.flatten_params(params) .map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join("&") end
# File lib/helio/util.rb, line 101 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) File.open(file) { |f| } rescue StandardError false else true end
# File lib/helio/util.rb, line 167 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) check_array_of_maps_start_keys!(value) result += flatten_params_array(value, calculated_key) else result << [calculated_key, value] end end result end
# File lib/helio/util.rb, line 187 def self.flatten_params_array(value, calculated_key) result = [] value.each do |elem| if elem.is_a?(Hash) result += flatten_params(elem, "#{calculated_key}[]") elsif elem.is_a?(Array) result += flatten_params_array(elem, calculated_key) else result << ["#{calculated_key}[]", elem] end end result end
# File lib/helio/util.rb, line 93 def self.log_debug(message, data = {}) if !Helio.logger.nil? || !Helio.log_level.nil? && Helio.log_level <= Helio::LEVEL_DEBUG log_internal(message, data, color: :blue, level: Helio::LEVEL_DEBUG, logger: Helio.logger, out: $stdout) end end
# File lib/helio/util.rb, line 77 def self.log_error(message, data = {}) if !Helio.logger.nil? || !Helio.log_level.nil? && Helio.log_level <= Helio::LEVEL_ERROR log_internal(message, data, color: :cyan, level: Helio::LEVEL_ERROR, logger: Helio.logger, out: $stderr) end end
# File lib/helio/util.rb, line 85 def self.log_info(message, data = {}) if !Helio.logger.nil? || !Helio.log_level.nil? && Helio.log_level <= Helio::LEVEL_INFO log_internal(message, data, color: :cyan, level: Helio::LEVEL_INFO, logger: Helio.logger, out: $stdout) end 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.
# File lib/helio/util.rb, line 240 def self.normalize_headers(headers) headers.each_with_object({}) do |(k, v), new_headers| if k.is_a?(Symbol) k = titlecase_parts(k.to_s.tr("_", "-")) elsif k.is_a?(String) k = titlecase_parts(k) end new_headers[k] = v end end
# File lib/helio/util.rb, line 201 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
The secondary opts argument can either be a string or hash Turn this value into an api_token and a set of headers
# File lib/helio/util.rb, line 213 def self.normalize_opts(opts) case opts when String { api_token: opts } when Hash check_api_token!(opts.fetch(:api_token)) if opts.key?(:api_token) opts.clone else raise TypeError, "normalize_opts expects a string or a hash" end end
# File lib/helio/util.rb, line 42 def self.object_classes @object_classes ||= { # data structures ListObject::OBJECT_NAME => ListObject, # business objects CustomerList::OBJECT_NAME => CustomerList, Participant::OBJECT_NAME => Participant, } end
# File lib/helio/util.rb, line 27 def self.objects_to_ids(h) case h when APIResource h.id when Hash res = {} h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? } res when Array h.map { |v| objects_to_ids(v) } else h 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.
# File lib/helio/util.rb, line 255 def self.request_id_dashboard_url(request_id, api_token) env = !api_token.nil? && api_token.start_with?("sk_live") ? "live" : "test" "https://helio.zurb.com/#{env}/logs/#{request_id}" end
Constant time string comparison to prevent timing attacks Code borrowed from ActiveSupport
# File lib/helio/util.rb, line 262 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.zero? end
# File lib/helio/util.rb, line 113 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
Encodes a string in a way that makes it suitable for use in a set of query parameters in a URI or in a set of form parameters in a request body.
# File lib/helio/util.rb, line 159 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
Private Class Methods
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}]
We have to produce something that looks like this:
arr[][a]=1&arr[][b]=2&arr[][a]=3&arr[][b]=4
The only way for the server to recognize that this is a two item array is that it notices the repetition of element “a”, so it's key that these repeated elements are encoded first.
This method is invoked for any arrays being encoded and checks that if the array contains all non-empty maps, that each of those maps must start with the same key so that their boundaries can be properly encoded.
# File lib/helio/util.rb, line 306 def self.check_array_of_maps_start_keys!(arr) expected_key = nil arr.each do |item| break unless item.is_a?(Hash) break if item.count.zero? first_key = item.first[0] if expected_key if expected_key != first_key raise ArgumentError, "All maps nested in an array should start with the same key " \ "(expected starting key '#{expected_key}', got '#{first_key}')" end else expected_key = first_key end end end
Uses an ANSI escape code to colorize text if it's going to be sent to a TTY.
# File lib/helio/util.rb, line 329 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
Turns an integer log level into a printable name.
# File lib/helio/util.rb, line 341 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
TODO: Make these named required arguments when we drop support for Ruby 2.0.
# File lib/helio/util.rb, line 353 def self.log_internal(message, data = {}, color: nil, level: nil, logger: nil, out: nil) data_str = data.reject { |_k, v| v.nil? } .map do |(k, v)| format("%s=%s", colorize(k, color, !out.nil? && out.isatty), 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=%s %s", wrap_logfmt_value(message), data_str)) elsif out.isatty out.puts format("%s %s %s", colorize(level_name(level)[0, 4].upcase, color, out.isatty), message, data_str) else out.puts format("message=%s level=%s %s", wrap_logfmt_value(message), level_name(level), data_str) end end
# File lib/helio/util.rb, line 372 def self.titlecase_parts(s) s.split("-") .reject { |p| p == "" } .map { |p| p[0].upcase + p[1..-1].downcase } .join("-") end
Wraps a value in double quotes if it looks sufficiently complex so that it can be read by logfmt parsers.
# File lib/helio/util.rb, line 382 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. format(%("%s"), 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