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

array_to_hash(array) click to toggle source

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
check_api_token!(key) click to toggle source
# 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
check_string_argument!(key) click to toggle source
# 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
convert_to_helio_object(data, opts = {}) click to toggle source

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 a HelioObject.

  • opts - Options for HelioObject 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
encode_parameters(params) click to toggle source

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_readable(file) click to toggle source
# 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
flatten_params(params, parent_key = nil) click to toggle source
# 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
flatten_params_array(value, calculated_key) click to toggle source
# 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
log_debug(message, data = {}) click to toggle source
# 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
log_error(message, data = {}) click to toggle source
# 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
log_info(message, data = {}) click to toggle source
# 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
normalize_headers(headers) click to toggle source

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
normalize_id(id) click to toggle source
# 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
normalize_opts(opts) click to toggle source

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
object_classes() click to toggle source
# 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
objects_to_ids(h) click to toggle source
# 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
request_id_dashboard_url(request_id, api_token) click to toggle source

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
secure_compare(a, b) click to toggle source

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
symbolize_names(object) click to toggle source
# 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
url_encode(key) click to toggle source

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

check_array_of_maps_start_keys!(arr) click to toggle source

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
colorize(val, color, isatty) click to toggle source

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
level_name(level) click to toggle source

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
log_internal(message, data = {}, color: nil, level: nil, logger: nil, out: nil) click to toggle source

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
titlecase_parts(s) click to toggle source
# 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
wrap_logfmt_value(val) click to toggle source

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