Skip to content

Instantly share code, notes, and snippets.

@dblock
Created June 19, 2012 13:29
Show Gist options
  • Save dblock/2954175 to your computer and use it in GitHub Desktop.
Save dblock/2954175 to your computer and use it in GitHub Desktop.
Api Cache Helper
# Written (mostly) by https://github.com/macreery (c) Art.sy, 2012, MIT License
module ApiCache
# cache wrapper, yields an executable block
def self.cache(options = {})
# options set default expiration time and force a miss if specified
options = standardize_options(options)
cache_options = options[:cache_options] || {}
cache_options[:expires_in] = 24.hours if not cache_options[:expires_in]
cache_key = keyize(options)
result = Rails.cache.fetch(cache_key, cache_options) do
object = yield
reset_cache_metadata(object, options)
object
end
Rails.cache.delete(cache_key) unless result
result
end
# metadata for cached objects:
# :etag - Unique hash of object content
# :last_modified - Timestamp of last modification event
def self.cache_metadata(options = {})
default_metadata = {
etag: etag_for(SecureRandom.uuid),
last_modified: Time.now
}
options = standardize_options(options)
Rails.cache.read(metadata_key(options)) || default_metadata
end
def self.reset_cache_metadata(object, options = {})
return unless object
metadata = {
etag: etag_for(object),
last_modified: Time.now
}
Rails.cache.write(metadata_key(options), metadata)
end
def self.etag_for(object)
# Try to serialize in a way such that the ETag matches that which would
# be returned by Rack::ETag at response time; fall back to Marshal.dump
serialization = case object
when String then object
when Hash then object.to_json
else Marshal.dump(object)
end
%("#{Digest::MD5.hexdigest(serialization)}")
end
# invalidate an object that has been cached
def self.invalidate(*args)
options = invalidate_args_to_options(*args)
reset_key_prefix_for(options[:klass], options[:object])
reset_key_prefix_for(options[:klass]) if options[:object]
end
private
def self.find_or_create_key_prefix_for(klass, object = nil)
cache_options = {}
Rails.cache.fetch(index_string_for(klass, object), cache_options) do
new_key_prefix_for(klass, object)
end
end
def self.reset_key_prefix_for(klass, object = nil)
cache_options = {}
Rails.cache.write(
index_string_for(klass, object),
new_key_prefix_for(klass, object),
cache_options
)
end
def self.new_key_prefix_for(klass, object = nil)
Digest::MD5.hexdigest("#{klass}/#{object || "*"}:#{SecureRandom.uuid}")
end
def self.metadata_key(options = {})
"#{keyize(options)}:meta"
end
# Key format:
# User/id=1,Widget/id=42,Gadget/*
def self.keyize(options = {})
key_hash = options[:params]
(options[:bind].is_a?(Array) ? options[:bind] : [options[:bind]]).compact.collect { |el|
if el[:object] && el[:object][:id].nil?
raise ":bind object arguments can only be keyed by :id"
end
find_or_create_key_prefix_for(el[:klass], el[:object])
}.join(",") + ":" +
Digest::MD5.hexdigest(
[ options[:path], options[:version], options[:caller] ].compact.join("\n") +
(key_hash || {}).delete_if { |k, v| v.nil? }.to_a.to_json
)
end
def self.index_string_for(klass, object = nil)
"INDEX:" +
if object && object[:id]
"#{klass}/id=#{object[:id]}"
else
"#{klass}/*"
end
end
# standardizes options, accepting the different hash formats
def self.standardize_options(options = {})
new_options = {}
new_options[:bind] = standardize_bind_option(options[:bind])
options.merge(new_options)
end
def self.standardize_bind_option(option)
case option
when Hash
option
when Array
bind_array_to_option(option)
when NilClass
nil
end
end
def self.bind_array_to_option(arr)
options = {}
case arr[0]
when Array
arr.collect { |subarr| standardize_bind_option(subarr) }
when Class
h = { klass: arr[0] }
h.merge!({ object: (arr[1].is_a?(Hash) ? arr[1] : { id: arr[1] }) }) if arr[1]
h
else
raise "Invalid argument to :bind option"
end
end
def self.invalidate_args_to_options(*args)
case args[0]
when Hash
args[0]
when Class
case args[1]
when Hash
{ klass: args[0], object: args[1] }
when NilClass
{ klass: args[0] }
end
else
raise "Invalid call to invalidate: call as invalidate(klass, identifier) or invalidate(options)"
end
end
end
# Written (mostly) by https://github.com/macreery (c) Art.sy, 2012, MIT License
module ApiCacheHelper
def cache(options = {})
ApiCache::cache(cache_options(options)) do
yield
end
end
def cache_or_304(options = {}, &block)
metadata = ApiCache.cache_metadata(cache_options(options)) || {}
error!("Not Modified", 304) if fresh?(metadata)
cache(options, &block)
end
def cache_options(options)
default_options.merge(options)
end
# invalidate a cache record
def invalidate(*args)
ApiCache::invalidate(*args)
end
private
def default_options
options = { }
options[:path] = request.path
if request.GET.any?
options[:params] = request.GET.dup
end
options[:version] = version if self.respond_to?(:version)
api_caller = caller.detect { |line| !(line =~ /\/api_cache_helper\.rb/) }
md = api_caller.match(/(app\/.*\.rb:[0-9]*):/)
options[:caller] = md[1] if md
options
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment