transloadit/ruby-sdk

View on GitHub
lib/transloadit/request.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'transloadit'

require 'rest-client'
require 'openssl'

#
# Wraps requests to the Transloadit API. Ensures all API requests return a
# parsed Transloadit::Response, and abstracts away finding a lightly-used
# instance on startup.
#
class Transloadit::Request
  # The default Transloadit API endpoint.
  API_ENDPOINT = URI.parse('https://api2.transloadit.com/')

  # The default headers to send to the API.
  API_HEADERS  = { 'Transloadit-Client' => "ruby-sdk:#{Transloadit::VERSION}" }

  # The HMAC algorithm used for calculation request signatures.
  HMAC_ALGORITHM = OpenSSL::Digest.new('sha1')

  # @return [String] the API endpoint for the request
  attr_reader   :url

  # @return [String] the authentication secret to sign the request with
  attr_accessor :secret

  #
  # Prepares a request against an endpoint URL, optionally with an encryption
  # secret. If only a path is passed, the API will automatically determine the
  # best server to use. If a full URL is given, the host provided will be
  # used.
  #
  # @param [String] url    the API endpoint
  # @param [String] secret an optional secret with which to sign the request
  #
  def initialize(url, secret = nil)
    self.url    = URI.parse(url.to_s)
    self.secret = secret
  end

  #
  # Performs an HTTP GET to the request's URL. Takes an optional hash of
  # query params.
  #
  # @param  [Hash] params additional query parameters
  # @return [Transloadit::Response] the response
  #
  def get(params = {})
    self.request! do
      self.api[url.path + self.to_query(params)].get(API_HEADERS)
    end
  end

  #
  # Performs an HTTP DELETE to the request's URL. Takes an optional hash
  # containing the form-encoded payload.
  #
  # @param  [Hash] payload the payload to form-encode along with the POST
  # @return [Transloadit::Response] the response
  #
  def delete(payload = {})
    self.request! do
      options = {:payload => self.to_payload(payload)}
      self.api(options = options)[url.path].delete(API_HEADERS)
    end
  end

  #
  # Performs an HTTP POST to the request's URL. Takes an optional hash
  # containing the form-encoded payload.
  #
  # @param  [Hash] payload the payload to form-encode along with the POST
  # @return [Transloadit::Response] the response
  #
  def post(payload = {})
    self.request! do
      self.api[url.path].post(self.to_payload(payload), API_HEADERS)
    end
  end

  #
  # Performs an HTTP PUT to the request's URL. Takes an optional hash
  # containing the form-encoded payload.
  #
  # @param  [Hash] payload the payload to form-encode along with the POST
  # @return [Transloadit::Response] the response
  #
  def put(payload = {})
    self.request! do
      self.api[url.path].put(self.to_payload(payload), API_HEADERS)
    end
  end

  #
  # @return [String] a human-readable version of the prepared Request
  #
  def inspect
    self.url.to_s.inspect
  end

  protected

  attr_writer :url

  #
  # Retrieves the current API endpoint. If the URL of the request contains a
  # hostname, then the hostname is used as the base endpoint of the API.
  # Otherwise uses the class-level API base.
  #
  # @return [RestClient::Resource] the API endpoint for this instance
  #
  def api(options = {})
    @api ||= begin
      case self.url.host
        when String then RestClient::Resource.new(self.url.host, options = options)
        else RestClient::Resource.new(API_ENDPOINT.host, options = options)
      end
    end
  end

  #
  # Updates the POST payload passed to be compliant with the Transloadit API
  # spec. JSONifies the value for the +params+ key and signs the request with
  # the instance's +secret+ if it exists.
  #
  # @param  [Hash] the payload to update
  # @return [Hash] the Transloadit-compliant POST payload
  #
  def to_payload(payload = nil)
    return {} if payload.nil?
    return {} if payload.respond_to?(:empty?) and payload.empty?

    # TODO: refactor this, don't update a hash that's not ours
    payload.update :params    => MultiJson.dump(payload[:params])
    payload.update :signature => self.signature(payload[:params])
    payload.delete :signature if payload[:signature].nil?
    payload
  end

  #
  # Updates the GET/DELETE params hash to be compliant with the Transloadit
  # API by URI escaping and encoding the params hash, and attaching a
  # signature.
  #
  # @param  [Hash] params the params to encode
  # @return [String] the URI-encoded and escaped query parameters
  #
  def to_query(params = nil)
    return '' if params.nil?
    return '' if params.respond_to?(:empty?) and params.empty?

    escape    = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
    params_in_json = MultiJson.dump(params)
    uri_params = URI.escape(params_in_json, escape)

    params    = {
      :params    => uri_params,
      :signature => self.signature(params_in_json)
    }

    '?' + params.map {|k,v| "#{k}=#{v}" if v }.compact.join('&')
  end

  #
  # Wraps a request's results in a Transloadit::Response, even if an exception
  # is raised by RestClient.
  #
  def request!(&request)
    Transloadit::Response.new request.call
  rescue RestClient::Exception => e
    Transloadit::Response.new e.response
  end

  #
  # Computes the HMAC digest of the params hash, if a secret was given to the
  # instance.
  #
  # @param  [String] params the JSON encoded payload to sign
  # @return [String] the HMAC signature for the params
  #
  def signature(params)
    self.class._hmac(self.secret, params) if self.secret.to_s.length > 0
  end

  private

  #
  # Computes an HMAC digest from the key and message.
  #
  # @param  [String] key     the secret key to sign with
  # @param  [String] message the message to sign
  # @return [String]         the signature of the message
  #
  def self._hmac(key, message)
    OpenSSL::HMAC.hexdigest HMAC_ALGORITHM, key, message
  end
end