lib/dry_ice.rb
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