Created
June 19, 2012 13:29
-
-
Save dblock/2954175 to your computer and use it in GitHub Desktop.
Api Cache Helper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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