ryanuber/oaf

View on GitHub
lib/oaf/util.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Oaf::Util
  extend self

  # Determines if a line of output looks anything like an HTTP header
  # declaration.
  #
  # == Parameters:
  # line::
  #   A line of text to examine
  #
  # == Returns:
  # A boolean, true if it can be read as a header string, else false.
  #
  def is_http_header? line
    line.split(':').length == 2
  end

  # Retrieves a hash in the form `name` => `value` from a string that
  # describes an HTTP response header.
  #
  # == Parameters:
  # line::
  #   A line of text to parse
  #
  # == Returns
  # A hash in the form `name` => `value`, or `nil`
  #
  def get_http_header line
    return nil if not is_http_header? line
    parts = line.split(':')
    {parts[0].strip => parts[1].strip}
  end

  # Retrieves the numeric value from a line of text as an HTTP status code.
  #
  # == Parameters:
  # line::
  #   A line of text to parse
  #
  # == Returns:
  # An integer if valid, else `nil`.
  #
  def get_http_status line
    is_http_status?(line) ? line.to_i : nil
  end

  # Determines if an HTTP status code is valid per RFC2616
  #
  # == Parameters:
  # code::
  #   A number to validate
  #
  # == Returns
  # A boolean, true if valid, else false.
  #
  def is_http_status? code
    (200..206).to_a.concat((300..307).to_a).concat((400..417).to_a) \
      .concat((500..505).to_a).include? code.to_i
  end

  # Convert various pieces of data about the request into a single hash, which
  # can then be passed on as environment data to a script.
  #
  # == Parameters:
  # req_path::
  #   A string with the HTTP request path
  # headers::
  #   A hash of request headers
  # params::
  #   A hash of request parameters
  # body::
  #   A string containing the request body
  #
  # == Returns:
  # A flat hash containing namespaced environment parameters
  #
  def prepare_environment req_path, headers, params, body
    result = Hash.new
    {'header' => headers, 'param' => params}.each do |prefix, data|
      data.each do |name, value|
        result.merge! Oaf::Util.environment_item prefix, name, value
      end
    end
    result.merge! Oaf::Util.environment_item 'request', 'path', req_path
    result.merge Oaf::Util.environment_item 'request', 'body', body
  end

  # Prepares a key for placement in the execution environment. This includes
  # namespacing variables and converting characters to predictable and
  # easy-to-use names.
  #
  # == Parameters:
  # prefix::
  #   A prefix for the key. This helps with separation.
  # key::
  #   The key to sanitize
  #
  # == Returns:
  # A string with the prepared value
  #
  def prepare_key prefix, key
    "oaf_#{prefix}_#{key.gsub('-', '_').downcase}"
  end

  # Formats a single environment item into a hash, which can be merged into a
  # collective environment mapping later on.
  #
  # == Parameters:
  # prefix::
  #   The prefix for the type of item being added.
  # key::
  #   The key name of the environment property
  # value::
  #   The value for the environment property
  #
  # == Returns:
  # A hash with prepared values ready to merge into an environment hash
  #
  def environment_item prefix, key, value
    {Oaf::Util.prepare_key(prefix, key) => Oaf::Util.flatten(value)}
  end

  # Flatten a hash or array into a string. This is useful for preparing some
  # data for passing in via the environment, because multi-dimension data
  # structures are not supported for that.
  #
  # == Parameters:
  # data::
  #   The data to flatten
  #
  # == Returns:
  # A flattened string. It will be empty if the object passed in was not
  # flatten-able.
  #
  def flatten data
    result = ''
    if data.kind_of? Hash
      data.each do |key, val|
        val = Oaf::Util.flatten val if not val.kind_of? String
        result += "#{key}#{val}"
      end
    elsif data.kind_of? Array
      data.each do |item|
        item = Oaf::Util.flatten item if not item.kind_of? String
        result += item
      end
    elsif data.kind_of? String
      result = data
    else
      result = ''
    end
    result
  end

  # Given an array of text lines, iterate over each of them and determine if
  # they may be interpreted as headers or status. If they can, add them to
  # the result.
  #
  # == Parameters:
  # text::
  #   Plain text data to examine
  #
  # == Returns:
  # A 3-item structure containing headers, status, and the number of lines
  # which the complete metadata (including the "---" delimiter) occupies.
  #
  def parse_http_meta text
    headers = {}
    status = 200
    size = 0
    parts = text.split /^---$/
    if parts.length > 1
      for part in parts.last.split "\n"
        if Oaf::Util.is_http_header? part
          headers.merge! Oaf::Util.get_http_header part
        elsif Oaf::Util.is_http_status? part
          status = Oaf::Util.get_http_status part
        else next
        end
        size += size == 0 ? 2 : 1  # compensate for delimiter
      end
    end
    [headers, status, size]
  end

  # Return a default response string indicating that nothing could be
  # done and no response files were found.
  #
  # == Returns:
  # A string with response output for parsing.
  #
  def get_default_response
    "oaf: Not Found\n---\n404"
  end

  # Returns the path to the file to use for the request. If the provided
  # file path does not exist, this method will search for a file within
  # the same directory matching the default convention "_*_".
  #
  # == Parameters:
  # root::
  #   The root path to search within.
  # req_path::
  #   The HTTP request path
  # method::
  #   The HTTP method of the request
  # default::
  #   An optional path to a default response file
  #
  # == Returns:
  # The path to a file to use, or `nil` if none is found.
  #
  def get_request_file root, req_path, method, default=''
    path = File.join root, req_path
    file = "#{path}.#{method}"

    return file if File.file? file

    Dir.glob(File.join(path, "_*_.#{method}")).each do |f|
      return f
    end
    Dir.glob(File.join(File.dirname(file), "_*_.#{method}")).each do |f|
      return f
    end
    File.file?(default.to_s) ? default : nil
  end

  # Fork a new process, in which we can safely modify the running environment
  # and run a command, sending data back to the parent process using an IO
  # pipe. This can be done in a single line in ruby >= 1.9, but we will do it
  # the hard way to maintain compatibility with older rubies.
  #
  # == Parameters:
  # path::
  #   The base path where the script file exists
  # env::
  #   The environment data to use in the subprocess.
  # command::
  #   The command to execute against the server
  #
  # == Returns:
  # A string of stderr concatenated to stdout.
  #
  def run_buffered path, env, command
    begin
      Dir.chdir path
    rescue Errno::ENOENT => e
      return e.message
    end
    out, wout = IO.pipe
    pid = fork do
      out.close
      ENV.replace env
      wout.write %x(#{command} 2>&1)
      at_exit { exit! }
    end
    wout.close
    Process.wait pid
    out.read
  end

  # Executes a file, or reads its contents if it is not executable, passing
  # it the request headers and body as arguments, and returns the result.
  #
  # == Parameters:
  # req_path::
  #   The HTTP request path
  # file::
  #   The path to the file to use for output
  # headers::
  #   The HTTP request headers to pass
  # body::
  #   The HTTP request body to pass
  # params::
  #   The HTTP request parameters to pass
  #
  # == Returns:
  # The result from the file, or a default result if the file is not found.
  #
  def get_output file, req_path=nil, headers=[], body=[], params=[]
    if file.nil?
      out = Oaf::Util.get_default_response
    elsif File.executable? file
      env = Oaf::Util.prepare_environment req_path, headers, params, body
      out = Oaf::Util.run_buffered File.dirname(file), env, file
    else
      out = File.open(file).read
    end
    out
  end
end