object_cacheable 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +9 -0
- data/lib/cache_configs.yml +11 -0
- data/lib/cache_configuration.rb +41 -0
- data/lib/cache_utils.rb +52 -0
- data/lib/cacheable.rb +154 -0
- data/object_cacheable.gemspec +14 -0
- data/test/models/cacheable_test.rb +191 -0
- metadata +68 -0
data/Gemfile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require "#{File.dirname(__FILE__)}/cache_utils"
|
3
|
+
|
4
|
+
module Cacheable
|
5
|
+
def self.cache
|
6
|
+
CacheConfiguration.cache
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.logger
|
10
|
+
CacheConfiguration.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
class CacheConfiguration
|
14
|
+
@@cache_instance = nil
|
15
|
+
@@logger_instance = nil
|
16
|
+
|
17
|
+
def self.load_config
|
18
|
+
configs = YAML.load_file("#{File.dirname(__FILE__)}/cache_configs.yml")
|
19
|
+
env = environment
|
20
|
+
|
21
|
+
@@cache_instance = eval(configs[env]['cache'])
|
22
|
+
@@logger_instance = eval(configs[env]['logger'])
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.environment
|
26
|
+
Rails.env
|
27
|
+
rescue => error
|
28
|
+
'test'
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.cache
|
32
|
+
load_config if @@cache_instance.nil?
|
33
|
+
@@cache_instance
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.logger
|
37
|
+
load_config if @@logger_instance.nil?
|
38
|
+
@@logger_instance
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/cache_utils.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Cacheable
|
2
|
+
class Utils
|
3
|
+
def self.test_cache
|
4
|
+
Cacheable::TestCache.instance
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.test_logger
|
8
|
+
Cacheable::TestLogger.instance
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestCache
|
13
|
+
@@cache_storage = {}
|
14
|
+
@@cache_instance = nil
|
15
|
+
|
16
|
+
def self.instance
|
17
|
+
@@cache_instance ||= TestCache.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch(key)
|
21
|
+
@@cache_storage[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(key, value, options = {})
|
25
|
+
@@cache_storage[key] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(key)
|
29
|
+
@@cache_storage.delete(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
def keys
|
33
|
+
@@cache_storage.keys
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class TestLogger
|
38
|
+
@@logger_instance = nil
|
39
|
+
|
40
|
+
def self.instance
|
41
|
+
@@logger_instance ||= TestLogger.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def info(message)
|
45
|
+
puts message
|
46
|
+
end
|
47
|
+
|
48
|
+
alias_method :warning, :info
|
49
|
+
alias_method :error, :info
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/cacheable.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/cache_configuration"
|
2
|
+
|
3
|
+
module Cacheable
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
|
8
|
+
def self.method_missing(name, *args, &block)
|
9
|
+
query_attributes = []
|
10
|
+
|
11
|
+
if name.to_s.start_with? 'fetch_by_'
|
12
|
+
# puts("\n\nname = #{name}\nname['fetch_by_'.length, name.length] = #{name['fetch_by_'.length, name.length]}")
|
13
|
+
|
14
|
+
query_attributes = name['fetch_by_'.length, name.length].split('_and_')
|
15
|
+
return nil if query_attributes == []
|
16
|
+
|
17
|
+
# puts("\n\n Cacheable method_missing - query attributes = #{query_attributes}, arguments = #{args.inspect}\n\n")
|
18
|
+
|
19
|
+
find_in_cache(query_attributes,args) or refresh_cache(query_attributes, args)
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Use this method if you want to explicitly re-fetch the whole object with given attributes-values, and write the newly fetched object to the cache
|
27
|
+
def self.refresh_cache(attributes, args)
|
28
|
+
|
29
|
+
obj = fetch(attributes, args)
|
30
|
+
return nil if obj.nil?
|
31
|
+
|
32
|
+
obj.cached_by_attributes = attributes
|
33
|
+
return obj.update_cache
|
34
|
+
|
35
|
+
rescue => error
|
36
|
+
Cacheable::logger.error("\n\nError in Cacheable.refresh_cache error = #{error.message}\n#{error.backtrace.join("\n")}\n\n")
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# This method must be implemented by subclasses, because only subclasses know how to do the actual data fetch
|
41
|
+
def self.fetch(attributes,args)
|
42
|
+
raise 'The method <fetch> must be implemented by subclass'
|
43
|
+
end
|
44
|
+
|
45
|
+
# including classes that want to have time-based expire can implement this method
|
46
|
+
# can be 5.minutes, 1.day ...etc...
|
47
|
+
# 0 is never expire
|
48
|
+
def self.cache_expired_duration
|
49
|
+
600 # default to 10 minutes
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def self.find_in_cache(attributes,args)
|
54
|
+
cache_key = build_cache_key(attributes,args)
|
55
|
+
|
56
|
+
obj = Cacheable::cache.fetch(cache_key)
|
57
|
+
if cache_expired_duration > 0
|
58
|
+
cached_time = Cacheable::cache.fetch(build_cached_time_key(attributes,args)) || 0
|
59
|
+
return refresh_cache(attributes, args) if ( (Time.now.to_i - cached_time > cache_expired_duration) && obj)
|
60
|
+
end
|
61
|
+
|
62
|
+
obj
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.save_to_cache(attributes, args, obj)
|
66
|
+
cache_key = build_cache_key(attributes,args)
|
67
|
+
options = {}
|
68
|
+
|
69
|
+
if cache_expired_duration > 0
|
70
|
+
options = { expires_in: cache_expired_duration.to_i }
|
71
|
+
Cacheable::cache.write(build_cached_time_key(attributes,args), Time.now.to_i)
|
72
|
+
end
|
73
|
+
|
74
|
+
Cacheable::cache.write(cache_key, obj, options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.build_cache_key(attributes,args)
|
78
|
+
"#{self.name}_#{attributes.join('_')}_#{args.map(&:to_s).join('_')}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# store the time when the object is cached
|
82
|
+
def self.build_cached_time_key(attributes,args)
|
83
|
+
"#{build_cache_key(attributes,args)}_cached_time"
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end # End of class methods
|
88
|
+
|
89
|
+
# Instance methods
|
90
|
+
|
91
|
+
# Delete cached object from cache
|
92
|
+
# For example if you have a cached current_profile, and you want to delete it out of cache use current_profile.delete_from_cache
|
93
|
+
def delete_from_cache
|
94
|
+
cached_key_attributes = cached_by_attributes
|
95
|
+
raise "This object has never been cached in the cache #{self.inspect}" if cached_key_attributes.nil?
|
96
|
+
|
97
|
+
cached_key_attributes.each do |attribute_keys|
|
98
|
+
attribute_values = build_attribute_values(attribute_keys)
|
99
|
+
cache_key = self.class.build_cache_key(attribute_keys,attribute_values)
|
100
|
+
Cacheable::cache.delete(cache_key)
|
101
|
+
end
|
102
|
+
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# This method updates all instances of an object with all cached keys (cached by different attributes)
|
107
|
+
# If you have an instance of a cached object, you just modify it or partially modify it, and want to write it back to the cache
|
108
|
+
# then use this method. For example if you have a current_profile, and its info changed, use current_profile.update_cache
|
109
|
+
def update_cache
|
110
|
+
cached_key_attributes = cached_by_attributes
|
111
|
+
raise "This object has never been cached in the cache #{self.inspect}" if cached_key_attributes.nil?
|
112
|
+
|
113
|
+
cached_key_attributes.each do |attribute_keys|
|
114
|
+
attribute_values = build_attribute_values(attribute_keys)
|
115
|
+
self.class.save_to_cache(attribute_keys, attribute_values, self)
|
116
|
+
end
|
117
|
+
|
118
|
+
self
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# All the attributes that instances of classes are cached by are dynamically created and store in cache
|
124
|
+
# in order to support multiple runtimes of cache server
|
125
|
+
def cached_by_attributes=(attributes)
|
126
|
+
all_attributes = all_cached_by_attributes
|
127
|
+
cached_attributes_for_class = all_attributes[self.class.name] || []
|
128
|
+
|
129
|
+
cached_attributes_for_class << attributes if !cached_attributes_for_class.include?(attributes)
|
130
|
+
|
131
|
+
all_attributes[self.class.name] = cached_attributes_for_class
|
132
|
+
Cacheable::cache.write('all_cached_by_attributes', all_attributes)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def cached_by_attributes
|
137
|
+
all_cached_by_attributes[self.class.name]
|
138
|
+
end
|
139
|
+
|
140
|
+
def all_cached_by_attributes
|
141
|
+
Cacheable::cache.fetch('all_cached_by_attributes') || {}
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_attribute_values(attribute_keys)
|
145
|
+
attribute_values = []
|
146
|
+
|
147
|
+
attribute_keys.each do |attribute|
|
148
|
+
attribute_values << self.send(attribute)
|
149
|
+
end
|
150
|
+
|
151
|
+
attribute_values
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'object_cacheable'
|
3
|
+
s.version = '1.0.0'
|
4
|
+
s.summary = "Object-level caching"
|
5
|
+
s.description = "A gem for object-level caching."
|
6
|
+
s.authors = ['Linh Chau']
|
7
|
+
s.email = 'chauhonglinh@gmail.com'
|
8
|
+
s.files = [
|
9
|
+
'./Gemfile', './object_cacheable.gemspec',
|
10
|
+
'lib/cache_configs.yml', 'lib/cache_configuration.rb', 'lib/cache_utils.rb', 'lib/cacheable.rb',
|
11
|
+
'test/models/cacheable_test.rb'
|
12
|
+
]
|
13
|
+
s.homepage = 'https://github.com/linhchauatl/object_cacheable.git'
|
14
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Run test from the command line, gem root
|
2
|
+
# bundle exec ruby -Ilib test/models/cacheable_test.rb
|
3
|
+
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest-spec-context'
|
6
|
+
require 'cacheable'
|
7
|
+
require 'mocha'
|
8
|
+
|
9
|
+
class TestCacheable
|
10
|
+
include Cacheable
|
11
|
+
attr_accessor :id, :value_1, :value_2, :value_3
|
12
|
+
|
13
|
+
# This method mimics a real call to database or webservices
|
14
|
+
def self.fetch(attributes, args)
|
15
|
+
# Assume that the combination of (value_1 + value_2) will uniquely identify one instance - for testing purpose
|
16
|
+
# For testing convenience, this implementation returns the same value object for the following calls:
|
17
|
+
# obj1 = TestCacheable.fetch_by_id(1)
|
18
|
+
# obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 1', 'value 2')
|
19
|
+
|
20
|
+
# The calls with different ids will return different objects
|
21
|
+
|
22
|
+
if ( (args.size == 1) or (args.size == 3) )
|
23
|
+
return TestCacheable.new(*args)
|
24
|
+
elsif (args.size == 2)
|
25
|
+
# Because the initilize always expects value 1 before value 2, so if we have attributes sent as ['value_2', 'value_1'], we have to reverse the args
|
26
|
+
created_args = (attributes == ['value_1', 'value_2'])? args : args.reverse
|
27
|
+
|
28
|
+
return (args.sort == ['value 1', 'value 2'])? TestCacheable.new(1, *created_args) : TestCacheable.new( (args[0].to_s.hash + args[1].to_s.hash), *created_args)
|
29
|
+
end
|
30
|
+
|
31
|
+
return TestCacheable.new(100, 'test value 1', 'test value 2')
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(an_id, a_value_1 = 'value 1', a_value_2 = 'value 2')
|
36
|
+
self.id = an_id
|
37
|
+
|
38
|
+
self.value_1 = a_value_1
|
39
|
+
self.value_2 = a_value_2
|
40
|
+
|
41
|
+
|
42
|
+
self.value_3 = 'Original Value 3'
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(obj)
|
46
|
+
self.id == obj.id && self.value_1 == obj.value_1 && self.value_2 == obj.value_2 && self.value_3 == obj.value_3
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
describe Cacheable do
|
53
|
+
|
54
|
+
context 'fetch object' do
|
55
|
+
it 'calls the real fetch when the object is not in cache, then puts object in the cache' do
|
56
|
+
|
57
|
+
# First, make sure that there is nothing in cache
|
58
|
+
cached_obj = Cacheable::cache.fetch('TestCacheable_id_1')
|
59
|
+
cached_obj.must_be_nil
|
60
|
+
|
61
|
+
# Call the method that fetches the object and puts in cache
|
62
|
+
obj = TestCacheable.fetch_by_id(1)
|
63
|
+
obj.wont_be_nil
|
64
|
+
|
65
|
+
cached_obj = Cacheable::cache.fetch('TestCacheable_id_1')
|
66
|
+
cached_obj.must_equal obj
|
67
|
+
|
68
|
+
# Make sure that it doesn't make a real call to fetch the second time
|
69
|
+
TestCacheable.stubs(:fetch).returns(nil) # If this gets called, the result_obj will be nil
|
70
|
+
result_obj = TestCacheable.fetch_by_id(1)
|
71
|
+
result_obj.must_equal obj
|
72
|
+
|
73
|
+
# clean the cache after finish
|
74
|
+
obj.delete_from_cache
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'fetches and caches different objects separately' do
|
78
|
+
obj3 = TestCacheable.fetch_by_id(3)
|
79
|
+
obj3.wont_be_nil
|
80
|
+
|
81
|
+
obj4 = TestCacheable.fetch_by_id(4)
|
82
|
+
obj4.wont_be_nil
|
83
|
+
|
84
|
+
# Fetched objects must be different
|
85
|
+
obj3.wont_equal obj4
|
86
|
+
|
87
|
+
#Cached objects must be different
|
88
|
+
cached_obj3 = Cacheable::cache.fetch('TestCacheable_id_3')
|
89
|
+
cached_obj3.must_equal obj3
|
90
|
+
|
91
|
+
cached_obj4 = Cacheable::cache.fetch('TestCacheable_id_4')
|
92
|
+
cached_obj4.must_equal obj4
|
93
|
+
|
94
|
+
cached_obj3.wont_equal cached_obj4
|
95
|
+
|
96
|
+
# clean the cache after finish
|
97
|
+
obj3.delete_from_cache
|
98
|
+
obj4.delete_from_cache
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'fetches the same object with different ordered combinations of keys' do
|
102
|
+
obj1 = TestCacheable.fetch_by_value_1_and_value_2('key 1', 'key 2')
|
103
|
+
obj2 = TestCacheable.fetch_by_value_2_and_value_1('key 2', 'key 1')
|
104
|
+
|
105
|
+
obj1.must_equal obj2
|
106
|
+
|
107
|
+
cached_obj1 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_key 1_key 2')
|
108
|
+
cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_2_value_1_key 2_key 1')
|
109
|
+
|
110
|
+
cached_obj1.must_equal cached_obj2
|
111
|
+
|
112
|
+
# clean the cache after finish - make sure that obj1 and obj2 are both deleted from cache
|
113
|
+
obj1.delete_from_cache
|
114
|
+
cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_2_value_1_key 2_key 1')
|
115
|
+
cached_obj2.must_be_nil
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'fetches two different objects with revert values for keys' do
|
120
|
+
# These 2 different objects have revert values for keys,
|
121
|
+
# for example [firstname, lastname] = [John, Smith] and [fistname, lastname] = [Smith, John]
|
122
|
+
# Unlikely to happen, but we will test anyway
|
123
|
+
obj1 = TestCacheable.fetch_by_value_1_and_value_2('value 1234', 'value 5678')
|
124
|
+
obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 5678', 'value 1234')
|
125
|
+
|
126
|
+
# fetched objects are different
|
127
|
+
obj1.wont_equal obj2
|
128
|
+
|
129
|
+
cached_obj1 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 1234_value 5678')
|
130
|
+
cached_obj1.must_equal obj1
|
131
|
+
|
132
|
+
cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 5678_value 1234')
|
133
|
+
cached_obj2.must_equal obj2
|
134
|
+
|
135
|
+
# cached objects are different
|
136
|
+
cached_obj1.wont_equal cached_obj2
|
137
|
+
|
138
|
+
# clean the cache after finish
|
139
|
+
obj1.delete_from_cache
|
140
|
+
obj2.delete_from_cache
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'update object' do
|
146
|
+
it 'updates all instances of object in the cache' do
|
147
|
+
obj1 = TestCacheable.fetch_by_id(1)
|
148
|
+
obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 1', 'value 2')
|
149
|
+
|
150
|
+
# We expect that obj1 and obj2 are 2 cached instances of the same object:
|
151
|
+
obj1.must_equal obj2
|
152
|
+
|
153
|
+
# Now update obj1 and expect the value is changed in the cache
|
154
|
+
obj1.value_3 = 'New value 3'
|
155
|
+
obj1.update_cache
|
156
|
+
|
157
|
+
cached_obj1 = Cacheable::cache.fetch('TestCacheable_id_1')
|
158
|
+
cached_obj1.value_3.must_equal 'New value 3'
|
159
|
+
|
160
|
+
# We also expect that the obj_2 value_3 is also changed
|
161
|
+
cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 1_value 2')
|
162
|
+
cached_obj2.value_3.must_equal 'New value 3'
|
163
|
+
|
164
|
+
# clean the cache after finish
|
165
|
+
obj1.delete_from_cache
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'delete object' do
|
171
|
+
it 'deletes all instances of object in the cache' do
|
172
|
+
obj1 = TestCacheable.fetch_by_id(1)
|
173
|
+
obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 1', 'value 2')
|
174
|
+
|
175
|
+
# We expect that obj1 and obj2 are 2 cached instances of the same object:
|
176
|
+
obj1.must_equal obj2
|
177
|
+
|
178
|
+
# Delete obj1
|
179
|
+
obj1.delete_from_cache
|
180
|
+
|
181
|
+
cached_obj1 = Cacheable::cache.fetch('TestCacheable_id_1')
|
182
|
+
cached_obj1.must_be_nil
|
183
|
+
|
184
|
+
# The obj2 in the cache now must be nil too
|
185
|
+
cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 1_value 2')
|
186
|
+
cached_obj2.must_be_nil
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: object_cacheable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Linh Chau
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2013-03-28 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: A gem for object-level caching.
|
22
|
+
email: chauhonglinh@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- ./Gemfile
|
31
|
+
- ./object_cacheable.gemspec
|
32
|
+
- lib/cache_configs.yml
|
33
|
+
- lib/cache_configuration.rb
|
34
|
+
- lib/cache_utils.rb
|
35
|
+
- lib/cacheable.rb
|
36
|
+
- test/models/cacheable_test.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: https://github.com/linhchauatl/object_cacheable.git
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
segments:
|
51
|
+
- 0
|
52
|
+
version: "0"
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.6
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Object-level caching
|
67
|
+
test_files: []
|
68
|
+
|