homeflow/dry_ice

View on GitHub
lib/dry_ice.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'logger'
require 'fileutils'
require 'tmpdir'
require 'pathname'
require 'digest/md5'

module HTTParty #:nodoc:
  # == Caching for HTTParty
  # See documentation in HTTParty::Icebox::ClassMethods.cache
  #
  module DryIce

    module ClassMethods

      # Enable caching and set cache options
      # Returns memoized cache object
      #
      # Following options are available, default values are in []:
      #
      # +store+::       Storage mechanism for cached data (memory, filesystem, your own) [memory]
      # +timeout+::     Cache expiration in seconds [60]
      # +logger+::      Path to logfile or logger instance [nil, silent]
      #
      # Any additional options are passed to the Cache constructor
      #
      # Usage:
      #
      #   # Enable caching in HTTParty, in memory, for 1 minute
      #   cache # Use default values
      #
      #   # Enable caching in HTTParty, on filesystem (/tmp), for 10 minutes
      #   cache :store => 'file', :timeout => 600, :location => '/tmp/'
      #
      #   # Use your own cache store (see +AbstractStore+ class below)
      #   cache :store => 'memcached', :timeout => 600, :server => '192.168.1.1:1001'
      #
      def cache(cache, options = {})
        return @cache = nil unless cache
        raise "cache instance must respond_to #read, #write and #delete" unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
        @cache = IceCache.new(cache, options)
      end

      def get_cache
        @cache || false
      end

    end

    # When included, extend class with +cache+ method
    # and redefine +get+ method to use cache
    #
    def self.included(receiver) #:nodoc:
      receiver.extend ClassMethods
      receiver.class_eval do

        # Get reponse from network
        #
        # TODO: Why alias :new :old is not working here? Returns NoMethodError
        #
        def self.get_without_caching(path, options={})
          perform_request Net::HTTP::Get, path, options
        end

        # Get response from cache, if available
        #
        def self.get_with_caching(path, options={})
          return get_without_caching(path, options) unless get_cache
          key = path.downcase # this makes a copy of path
          key << options[:query].to_s if defined? options[:query]
          if res = get_cache.read(key) 
            return res
          else
            response =  get_without_caching(path, options)
            if cache_for = self.cache_response?(response)
              get_cache.write(key,response, :expires_in => cache_for)
              return response
            else
              return response
            end
          end
        end

        #returns falsy if the response should not be cached - otherwise returns the timeout in seconds to cache for
        def self.cache_response?(response)
           return false if !response.body
           return false unless response.code.to_s == "200"
           timeout = response.headers['cache-control'] && response.headers['cache-control'][/max-age=(\d+)/, 1].to_i()
           return false unless timeout && timeout != 0
           return timeout
        end

        # Redefine original HTTParty +get+ method to use cache
        #
        def self.get(path, options={})
          self.get_with_caching(path, options)
        end

      end
    end

    class IceCache

      require 'msgpack'

      def initialize(cache, options = {})
        @options = {:serialize => true}.merge(options)
        @cache = cache
      end

      def write(name, value, options = {})
        @cache.write(name, serialize_response(value), options)
      end


      def serialize_response(response)
        headers = response.headers.dup
        body = response.body.dup
        parsed_response = response.parsed_response
        if @options[:serialize]
          [headers,body,parsed_response].to_msgpack
        else
          [headers,body,parsed_response]
        end
      end

      def build_response(serialized_response)
        if @options[:serialize]
          serialized_response = MessagePack.unpack(serialized_response)
        end
        CachedHTTPartyResponse.new(serialized_response[0], serialized_response[1], serialized_response[2])
      end

      def read(*args)
        found = @cache.read(*args)
        build_response(found) if found
      end

      def exist?(*args)
        @cache.exist?(*args)
      end


    end

    class CachedHTTPartyResponse

      attr_accessor :headers, :body, :parsed_response

      def initialize(headers, body, parsed_response)
        @headers, @body, @parsed_response = headers, body, parsed_response
      end

    end

  end
end