persistent-cache 0.0.8 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,11 +1,25 @@
1
1
  # Persistent::Cache
2
2
 
3
- Persistent cache behaves like a hash, with a sqlite3 storage back-end.
3
+ Persistent cache behaves like a hash, with a pluggable back-end. Currently sqlite3 and file system directory back-ends are provided. The cache defaults to type STORAGE_SQLITE
4
4
 
5
- Values in the cache have a default freshness period of 15465600 ms. This can be configured in the cache initializer. Setting fresh = nil indicates that data remains fresh for-ever. Each user of the cache may have his own independent freshness value. If stale data is requested from the cache, nil is returned. Updates to the cache are written to the sqlite3 storage, with SQL driver timeout set to 30 seconds. Data is marshalled before storage. If a key is not found in the cache, nil is returned. Setting the value of a key in the cache to nil deletes the entry from the sqlite database.
5
+ Values in the cache have a default freshness period of 15465600 ms. This can be configured in the cache initializer. Setting fresh = nil indicates that data remains fresh for-ever. Each user of the cache may have his own independent freshness value. If stale data is requested from the cache, nil is returned. Data is marshalled before storage. If a key is not found in the cache, nil is returned. Setting the value of a key in the cache to nil deletes the entry. If required, creation time of an entry can be specified using set(key, value, timestamp)
6
6
 
7
7
  This gem is sponsored by Hetzner (Pty) Ltd - http://hetzner.co.za
8
8
 
9
+ ## StorageSQLite
10
+
11
+ Updates to the cache are written to the sqlite3 storage, with SQL driver timeout set to 30 seconds.
12
+
13
+ ## StorageDirectory
14
+
15
+ Keys are required to be strings that are valid for use as directory names. The cache then stores from a storage root (configured in the StorageDirector constructor) with a subdirectory for each key, and a file called 'cache' for the value. The first line in the cache file is the timestamp of the entry.
16
+
17
+ When a StorageDirectory is used, it can be asked whether a key is present and what the path to a cache value is using:
18
+
19
+ get_value_path(key)
20
+
21
+ key_cached?(key)
22
+
9
23
  ## Installation
10
24
 
11
25
  Add this line to your application's Gemfile:
@@ -60,6 +74,10 @@ Or install it yourself as:
60
74
 
61
75
  cache = Persistent::Cache.new("/tmp/my-persistent-cache", nil) # for-ever fresh
62
76
 
77
+ cache = Persistent::Cache.new("/tmp/directory-cache", nil, STORAGE_DIRECTORY)
78
+
79
+ cache.set("mykey", "myvalue", Time.now) # explicitly set creation time
80
+
63
81
  Please send feedback and comments to the authors at:
64
82
 
65
83
  Wynand van Dyk <wynand.van.dyk@hetzner.co.za>
@@ -1,19 +1,36 @@
1
1
  require "persistent-cache/version"
2
2
  require "sqlite3"
3
+ require "persistent-cache/storage/storage_sq_lite"
3
4
 
4
5
  module Persistent
5
6
  class Cache
7
+ STORAGE_SQLITE = 'sqlite' unless defined? STORAGE_SQLITE
8
+ STORAGE_DIRECTORY = 'directory' unless defined? STORAGE_DIRECTORY
9
+
6
10
  # Fresh is 1 day less than the bacula default job retention time. If this is configured differently, FRESH should be updated as well.
7
11
  FRESH = 15465600; FRESH.freeze
8
- DB_TABLE = "key_value"; DB_TABLE.freeze
9
12
 
10
- attr_accessor :database_details
13
+ attr_accessor :storage_details
14
+ attr_accessor :storage
15
+ attr_accessor :fresh
16
+
17
+ def initialize(storage_details, fresh = FRESH, storage = STORAGE_SQLITE)
18
+ raise ArgumentError.new("No storage details provided") if storage_details.nil? or storage_details == ""
11
19
 
12
- def initialize(database_details, fresh = FRESH)
13
- @database_details = database_details
14
- @db_handle = connect_to_database
15
- @db_handle.busy_timeout = 30000
20
+ @storage = StorageSQLite.new(storage_details) if storage == STORAGE_SQLITE
21
+ @storage = StorageDirectory.new(storage_details) if storage == STORAGE_DIRECTORY
16
22
  @fresh = fresh
23
+ @storage_details = storage_details
24
+
25
+ raise ArgumentError.new("Unsupported storage type #{storage}}") if @storage.nil?
26
+ end
27
+
28
+ def set(key, value, timestamp)
29
+ if value.nil?
30
+ delete_entry(Marshal.dump(key))
31
+ else
32
+ save_key_value_pair(Marshal.dump(key), Marshal.dump(value), timestamp)
33
+ end
17
34
  end
18
35
 
19
36
  def []=(key, value)
@@ -35,29 +52,28 @@ module Persistent
35
52
  end
36
53
 
37
54
  def size
38
- @db_handle.execute("SELECT value FROM #{DB_TABLE}").size
55
+ @storage.size
39
56
  end
40
57
 
41
58
  def keys
42
- @db_handle.execute("SELECT key FROM #{DB_TABLE}").collect { |key|
59
+ @storage.keys.collect { |key|
43
60
  Marshal.load(key[0])
44
61
  }
45
62
  end
46
63
 
47
64
  def clear
48
- @db_handle.execute("DELETE FROM #{DB_TABLE}")
65
+ @storage.clear
49
66
  end
50
67
 
51
- # Methods not supported by this implementation
52
68
  private
53
69
 
54
- def save_key_value_pair(serialized_key, serialized_value)
70
+ def save_key_value_pair(serialized_key, serialized_value, timestamp = nil)
55
71
  delete_entry(serialized_key)
56
- @db_handle.execute("INSERT INTO #{DB_TABLE} (key, value, timestamp) VALUES(?, ?, ?)",serialized_key, serialized_value, Time.now.to_s)
72
+ @storage.save_key_value_pair(serialized_key, serialized_value, timestamp)
57
73
  end
58
74
 
59
75
  def lookup_key(serialized_key)
60
- result = @db_handle.execute("SELECT value, timestamp FROM #{DB_TABLE} WHERE key=?", serialized_key)
76
+ result = @storage.lookup_key(serialized_key)
61
77
  return nil if nil_result?(result)
62
78
  return nil if stale_entry?(serialized_key, result)
63
79
 
@@ -66,6 +82,7 @@ module Persistent
66
82
 
67
83
  def stale_entry?(serialized_key, result)
68
84
  return false if @fresh.nil?
85
+
69
86
  timestamp = Time.parse(result[0][1])
70
87
  if ((Time.now - timestamp) > FRESH)
71
88
  delete_entry(serialized_key)
@@ -75,26 +92,7 @@ module Persistent
75
92
  end
76
93
 
77
94
  def delete_entry(serialized_key)
78
- @db_handle.execute("DELETE FROM #{DB_TABLE} WHERE key=?", serialized_key)
79
- end
80
-
81
- def open_database
82
- @handle = SQLite3::Database.open(@database_details)
83
- end
84
-
85
- def create_database
86
- @handle = SQLite3::Database.new(@database_details)
87
- create_table
88
- @handle
89
- end
90
-
91
- def create_table
92
- @handle.execute("CREATE TABLE #{DB_TABLE}(key TEXT PRIMARY KEY, value TEXT, timestamp TEXT)")
93
- @handle
94
- end
95
-
96
- def connect_to_database
97
- File.exists?(@database_details) ? open_database : create_database
95
+ @storage.delete_entry(serialized_key)
98
96
  end
99
97
 
100
98
  def nil_result?(result)
@@ -0,0 +1,165 @@
1
+ require 'fileutils'
2
+
3
+ module Persistent
4
+ class StorageDirectory
5
+ CACHE_FILE = "cache" unless defined? CACHE_FILE; CACHE_FILE.freeze
6
+
7
+ attr_accessor :storage_root
8
+
9
+ def initialize(storage_details)
10
+ raise ArgumentError.new("Storage details not provided") if storage_details.nil? or storage_details == ""
11
+ @storage_root = storage_details
12
+ connect_to_database
13
+ end
14
+
15
+ def connect_to_database
16
+ FileUtils.makedirs([@storage_root]) if not File.exists?(@storage_root)
17
+ end
18
+
19
+ def save_key_value_pair(key, value, timestamp = nil)
20
+ prepare_to_store_key_value(key, value, timestamp)
21
+ store_key_value(key, value, get_time(timestamp)) if not value.nil?
22
+ end
23
+
24
+ def lookup_key(key)
25
+ validate_key(key)
26
+ return [] if not File.exists? compile_value_path(key)
27
+ lookup_key_value_timestamp(key)
28
+ end
29
+
30
+ def delete_entry(key)
31
+ validate_key(key)
32
+ FileUtils.rm_rf(compile_key_path(key))
33
+ end
34
+
35
+ def size
36
+ count = Dir::glob("#{@storage_root}/**/").size
37
+ # if the directory does not exist, count == 0, which is what we want
38
+ return 0 if count == 0
39
+ # if the directory does exist, but is empty, count == 1, namely the directory itself, and we want to return 0 (i.e. count - 1)
40
+ # if the directory does exist and includes subdirectories, the directory itself is still counted as well, and we want to return count - 1
41
+ return count - 1
42
+ end
43
+
44
+ def keys
45
+ return [] if size == 0
46
+ list_keys_sorted
47
+ end
48
+
49
+ def clear
50
+ keys.each do |key|
51
+ delete_entry(key[0])
52
+ end
53
+ end
54
+
55
+ def get_value_path(key)
56
+ validate_key(key)
57
+
58
+ return nil if not key_cached?(key)
59
+
60
+ compile_value_path(key)
61
+ end
62
+
63
+ def key_cached?(key)
64
+ # don't read the value here, as it may be very large - rather look whether the key is present
65
+ File.exists? compile_key_path(key)
66
+ end
67
+
68
+ private
69
+
70
+ def list_keys_sorted
71
+ result = []
72
+ append_keys(result).sort
73
+ end
74
+
75
+ def append_keys(result)
76
+ get_key_directories.each do |dir|
77
+ result << extract_key_from_directory(dir)
78
+ end
79
+ result
80
+ end
81
+
82
+ def get_key_directories
83
+ subdirectories = Dir::glob("#{@storage_root}/**/")
84
+ #exclude the storage root directory itself
85
+ subdirectories[1..-1]
86
+ end
87
+
88
+ def extract_key_from_directory(dir)
89
+ key = dir.match(/#{@storage_root}\/(\w+)\//)[1]
90
+ [key]
91
+ end
92
+
93
+ def compile_key_path(key)
94
+ "#{@storage_root}/#{key}"
95
+ end
96
+
97
+ def compile_value_path(key)
98
+ "#{compile_key_path(key)}/#{CACHE_FILE}"
99
+ end
100
+
101
+ def validate_save_key_value_pair(key, value)
102
+ validate_key(key)
103
+ raise ArgumentError.new("Only string values allowed") if not value.is_a?(String)
104
+ end
105
+
106
+ def lookup_key_value_timestamp(key)
107
+ result = [[],[]]
108
+ data = File.read(compile_value_path(key))
109
+ format_value_timestamp(result, data)
110
+ end
111
+
112
+ def format_value_timestamp(result, data)
113
+ result[0][0] = data.lines.to_a[1..-1].join
114
+ result[0][1] = data.lines.to_a[0..0].join.split("\n")[0]
115
+ result
116
+ end
117
+
118
+ def validate_key(key)
119
+ raise ArgumentError.new("Only string keys allowed") if not key.is_a?(String)
120
+ end
121
+
122
+ def empty_key_value(key)
123
+ FileUtils.makedirs([compile_key_path(key)]) if not File.exists?(compile_key_path(key))
124
+ FileUtils.rm_f(compile_value_path(key))
125
+ end
126
+
127
+ def get_time(timestamp)
128
+ timestamp.nil? ? Time.now.to_s : timestamp.to_s
129
+ end
130
+
131
+ def prepare_to_store_key_value(key, value, timestamp)
132
+ validate_save_key_value_pair(key, value)
133
+ delete_entry(key)
134
+ empty_key_value(key)
135
+ end
136
+
137
+ def store_key_value(key, value, timestamp)
138
+ tempfile = Tempfile.new("store_key_value")
139
+ store_value_timestamp_in_file(value, timestamp, tempfile)
140
+ sync_cache_value(key, tempfile)
141
+ end
142
+
143
+ def sync_cache_value(key, tempfile)
144
+ target = compile_value_path(key)
145
+ FileUtils.rm_f(target)
146
+ FileUtils.move(tempfile.path, target)
147
+ end
148
+
149
+ def store_value_timestamp_in_file(value, timestamp, tempfile)
150
+ output = File.open(tempfile, 'a')
151
+ write_value_timestamp(output, value, timestamp)
152
+ output.close
153
+ end
154
+
155
+ def write_value_timestamp(output, value, timestamp)
156
+ # Have to be explicit about writing to the file. 'puts' collapses newline at the end of the file and
157
+ # so does not guarantee exact replication of 'value' on lookup
158
+ output.write(timestamp)
159
+ output.write("\n")
160
+ output.write(value)
161
+ output.flush
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,84 @@
1
+ require "sqlite3"
2
+ require "eh/eh"
3
+
4
+ module Persistent
5
+ class StorageSQLite
6
+ DB_TABLE = "key_value" unless defined? DB_TABLE; DB_TABLE.freeze
7
+ DB_TIMEOUT = 30000 unless defined? DB_TIMEOUT; DB_TIMEOUT.freeze
8
+
9
+ attr_accessor :storage_details
10
+ attr_accessor :storage_handler
11
+
12
+ def initialize(storage_details)
13
+ raise ArgumentError.new("Storage details not provided") if storage_details.nil? or storage_details == ""
14
+ @storage_details = storage_details
15
+ @storage_handler = connect_to_database
16
+ @storage_handler.busy_timeout = 30000
17
+ end
18
+
19
+ def save_key_value_pair(serialized_key, serialized_value, timestamp = nil)
20
+ delete_entry(serialized_key)
21
+ time_entry = timestamp.nil? ? Time.now.to_s : timestamp.to_s
22
+ EH::retry!(:args => [serialized_key, serialized_value, time_entry]) do
23
+ @storage_handler.execute("INSERT INTO #{DB_TABLE} (key, value, timestamp) VALUES(?, ?, ?)",serialized_key, serialized_value, time_entry)
24
+ end
25
+ end
26
+
27
+ def lookup_key(serialized_key)
28
+ EH::retry!(:args => [serialized_key]) do
29
+ @storage_handler.execute("SELECT value, timestamp FROM #{DB_TABLE} WHERE key=?", serialized_key)
30
+ end
31
+ end
32
+
33
+ def delete_entry(serialized_key)
34
+ EH::retry!(:args => [serialized_key]) do
35
+ @storage_handler.execute("DELETE FROM #{DB_TABLE} WHERE key=?", serialized_key)
36
+ end
37
+ end
38
+
39
+ def size
40
+ EH::retry!(:args => []) do
41
+ @storage_handler.execute("SELECT value FROM #{DB_TABLE}").size
42
+ end
43
+ end
44
+
45
+ def keys
46
+ EH::retry!(:args => []) do
47
+ @storage_handler.execute("SELECT key FROM #{DB_TABLE}")
48
+ end
49
+ end
50
+
51
+ def clear
52
+ EH::retry!(:args => []) do
53
+ @storage_handler.execute("DELETE FROM #{DB_TABLE}")
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def connect_to_database
60
+ File.exists?(@storage_details) ? open_database : create_database
61
+ end
62
+
63
+ def open_database
64
+ EH::retry!(:args => []) do
65
+ SQLite3::Database.open(@storage_details)
66
+ end
67
+ end
68
+
69
+ def create_database
70
+ EH::retry!(:args => []) do
71
+ @storage_handler = SQLite3::Database.new(@storage_details)
72
+ create_table
73
+ end
74
+ @storage_handler
75
+ end
76
+
77
+ def create_table
78
+ EH::retry!(:args => []) do
79
+ @storage_handler.execute("CREATE TABLE #{DB_TABLE}(key TEXT PRIMARY KEY, value TEXT, timestamp TEXT)")
80
+ end
81
+ @storage_handler
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,5 @@
1
1
  module Persistent
2
2
  class Cache
3
- VERSION = "0.0.8"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
data/multidb CHANGED
Binary file
@@ -15,5 +15,8 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Persistent::Cache::VERSION
17
17
  gem.add_development_dependency 'rspec', '2.12.0'
18
+ gem.add_development_dependency 'simplecov'
19
+ gem.add_development_dependency 'simplecov-rcov'
18
20
  gem.add_dependency 'sqlite3', '1.3.7'
21
+ gem.add_dependency 'eh'
19
22
  end
@@ -1,74 +1,126 @@
1
1
  require 'spec_helper'
2
- require 'persistent-cache'
3
- require 'tempfile'
4
2
 
5
- describe Persistent::Cache do
6
- def get_database_name
7
- path = Tempfile.new("persistent-cache-spec-testdb").path
8
- FileUtils.rm_f(path)
9
- path
10
- end
3
+ require "persistent-cache"
4
+ require "persistent-cache/storage/storage_sq_lite"
11
5
 
6
+ describe Persistent::Cache do
12
7
  before :each do
13
8
  @db_name = get_database_name
9
+ @mock_storage = double(Persistent::StorageSQLite)
10
+ @test_key = "testkey"
11
+ @test_value = "testvalue"
12
+ FileUtils.rm_f(@db_name)
14
13
  end
15
14
 
15
+ context "when constructing" do
16
+ it "should receive database connection details and create a StorageSQLite instance if specified" do
17
+ @pcache = Persistent::Cache.new(@db_name, Persistent::Cache::STORAGE_SQLITE)
18
+ @pcache.class.should == Persistent::Cache
19
+ @pcache.storage.is_a?(Persistent::StorageSQLite).should == true
20
+ end
21
+
22
+ it "should raise an ArgumentError if storage details have not been provided" do
23
+ expect {
24
+ Persistent::Cache.new(nil)
25
+ }.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it "should remember the freshness interval if provided" do
29
+ @pcache = Persistent::Cache.new(@db_name, 123)
30
+ @pcache.fresh.should == 123
31
+ end
32
+
33
+ it "should remember the storage details provided" do
34
+ @pcache = Persistent::Cache.new(@db_name, 123)
35
+ @pcache.storage_details.should == @db_name
36
+ end
37
+
38
+ it "should default the freshness interval to FRESH if not provided" do
39
+ @pcache = Persistent::Cache.new(@db_name)
40
+ @pcache.fresh.should == Persistent::Cache::FRESH
41
+ end
42
+
43
+ it "should raise an ArgumentError if an unknown storage type has been provided" do
44
+ expect {
45
+ Persistent::Cache.new(@db_name, 100, "unknown")
46
+ }.to raise_error(ArgumentError)
47
+ end
48
+ end
16
49
 
17
50
  context "When assigning a value to a key" do
18
- it "should store the key/value pair in the db, with a timestamp" do
19
- start_time = Time.now - 1
51
+ it "should ask the storage handler to first delete, then save the key/value pair" do
52
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
53
+ @mock_storage.should_receive(:delete_entry)
54
+ @mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), nil)
20
55
  @pcache = Persistent::Cache.new(@db_name)
21
- @pcache["testkey"] = "testvalue"
22
- handle = SQLite3::Database.open(@db_name)
23
- result = handle.execute "select value, timestamp from #{Persistent::Cache::DB_TABLE} where key=?", Marshal.dump("testkey")
24
- result.nil?.should == false
25
- result[0].nil?.should == false
26
- result[0][0].should == Marshal.dump("testvalue")
27
- test_time = Time.parse(result[0][1])
28
- test_time.should > start_time and test_time.should < start_time + 600
56
+ @pcache[@test_key] = @test_value
29
57
  end
30
58
 
31
- it "should overwrite the existing key/value pair if they already exist" do
59
+ it "should ask the storage handler to delete if the value is nil using []" do
60
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
61
+ @mock_storage.should_receive(:delete_entry).with(Marshal.dump(@test_key))
32
62
  @pcache = Persistent::Cache.new(@db_name)
33
- @pcache["testkey"] = "testvalue"
34
- @pcache["testkey"] = "testvalue2"
35
- handle = SQLite3::Database.open(@db_name)
36
- result = handle.execute "select value from #{Persistent::Cache::DB_TABLE} where key=?", Marshal.dump("testkey")
37
- result.nil?.should == false
38
- result[0].nil?.should == false
39
- result.size.should == 1
40
- result[0][0].should == Marshal.dump("testvalue2")
63
+ @pcache[@test_key] = nil
64
+ end
65
+
66
+ it "should ask the storage handler to delete if the value is nil using set()" do
67
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
68
+ @mock_storage.should_receive(:delete_entry).with(Marshal.dump(@test_key))
69
+ @pcache = Persistent::Cache.new(@db_name)
70
+ @pcache.set(@test_key, nil, Time.now)
41
71
  end
42
72
 
43
73
  it "should serialize the key and value for persistence" do
74
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
75
+ @mock_storage.should_receive(:delete_entry)
76
+ @mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), nil)
44
77
  @pcache = Persistent::Cache.new(@db_name)
45
- @pcache.should_receive(:save_key_value_pair).with(Marshal.dump("testkey"), Marshal.dump("testvalue"))
46
- @pcache["testkey"] = "testvalue"
78
+ @pcache[@test_key] = @test_value
79
+ end
80
+
81
+ it "should ask the storage handler to store the value, with a specific timestamp if specified" do
82
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
83
+ @mock_storage.should_receive(:delete_entry)
84
+ timestamp = Time.now - 100
85
+ @mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), timestamp)
86
+ @pcache = Persistent::Cache.new(@db_name)
87
+ @pcache.set(@test_key, @test_value, timestamp)
47
88
  end
48
89
  end
49
90
 
50
91
  context "When looking up a value given its key" do
51
- it "should retrieve the value from the database and deserialize the value" do
92
+ it "should retrieve the value from storage using lookup_key and deserialize the value" do
93
+ @mock_storage.should_receive(:delete_entry)
94
+ @mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), nil)
95
+ @mock_storage.should_receive(:lookup_key).with(Marshal.dump(@test_key)).and_return([[Marshal.dump(@test_value), Time.now.to_s]])
96
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
52
97
  @pcache = Persistent::Cache.new(@db_name)
53
- @pcache["testkey"] = "testvalue2"
54
- result = @pcache["testkey"]
55
- result.should == "testvalue2"
98
+ @pcache[@test_key] = @test_value
99
+ result = @pcache[@test_key]
100
+ result.should == @test_value
56
101
  end
57
102
 
58
103
  it "should return nil if a value exists but it not fresh" do
59
- fetch_stale_entry
104
+ @mock_storage.should_receive(:delete_entry)
105
+ @mock_storage.should_receive(:lookup_key).with(Marshal.dump(@test_key)).and_return([[Marshal.dump(@test_value), (Time.now - Persistent::Cache::FRESH).to_s]])
106
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
107
+
108
+ @pcache = Persistent::Cache.new(@db_name)
109
+ @pcache[@test_key].nil?.should == true
60
110
  end
61
111
 
62
112
  it "should remove from the cache an entry it encounters that is not fresh" do
63
- fetch_stale_entry
64
- result = @handle.execute("SELECT value FROM #{Persistent::Cache::DB_TABLE} WHERE key=?", Marshal.dump("oldkey"))
65
- result.size.should == 0
113
+ @mock_storage.should_receive(:delete_entry)
114
+ @mock_storage.should_receive(:lookup_key).with(Marshal.dump(@test_key)).and_return([[Marshal.dump(@test_value), (Time.now - Persistent::Cache::FRESH).to_s]])
115
+ Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
116
+
117
+ @pcache = Persistent::Cache.new(@db_name)
118
+ @pcache[@test_key]
66
119
  end
67
120
 
68
121
  it "should return nil if a key is not in the database" do
69
122
  @pcache = Persistent::Cache.new(@db_name)
70
- @pcache["testkey"] = "testvalue2"
71
- result = @pcache["testkey2"]
123
+ result = @pcache["thiskeydoesnotexist"]
72
124
  result.nil?.should == true
73
125
  end
74
126
 
@@ -78,53 +130,6 @@ describe Persistent::Cache do
78
130
  @pcache.should_receive(:lookup_key).with(Marshal.dump("testkey"))
79
131
  @pcache["testkey"]
80
132
  end
81
-
82
- def fetch_stale_entry
83
- @pcache = Persistent::Cache.new(@db_name)
84
- timestamp = Time.now - Persistent::Cache::FRESH
85
- @handle = SQLite3::Database.open(@db_name)
86
- @handle.execute("INSERT INTO #{Persistent::Cache::DB_TABLE} (key, value, timestamp) VALUES ('#{Marshal.dump("oldkey")}', '#{Marshal.dump("oldvalue")}', '#{timestamp}')")
87
- @pcache["oldkey"].nil?.should == true
88
- end
89
- end
90
-
91
- context "when constructing" do
92
- it "should receive database connection details" do
93
- @pcache = Persistent::Cache.new(@db_name)
94
- @pcache.class.should == Persistent::Cache
95
- @pcache.database_details.should == @db_name
96
- end
97
-
98
- it "should create the database if it does not exist" do
99
- FileUtils.rm_f(@db_name)
100
- Persistent::Cache.new(@db_name)
101
- File.exists?(@db_name).should be_true
102
- end
103
-
104
- it "should create a key_value table with key (TEXT) and value (TEXT) and timestamp (TEXT) columns" do
105
- FileUtils.rm_f(@db_name)
106
- Persistent::Cache.new(@db_name)
107
- handle = SQLite3::Database.open(@db_name)
108
- result = handle.execute "PRAGMA table_info(#{Persistent::Cache::DB_TABLE})"
109
- result[0][1].should == "key"
110
- result[0][2].should == "TEXT"
111
- result[1][1].should == "value"
112
- result[1][2].should == "TEXT"
113
- result[2][1].should == "timestamp"
114
- result[2][2].should == "TEXT"
115
- end
116
-
117
-
118
- it "should use the existing database if it does exist" do
119
- FileUtils.rm_f(@db_name)
120
- handle = SQLite3::Database.new(@db_name)
121
- handle.execute "create table test123 ( id int );"
122
- handle.close
123
- Persistent::Cache.new(@db_name)
124
- handle = SQLite3::Database.open(@db_name)
125
- result = handle.execute "select name from sqlite_master where type='table'"
126
- result[0][0].should == "test123"
127
- end
128
133
  end
129
134
 
130
135
  context "it should behave like a cache" do
@@ -162,7 +167,7 @@ describe Persistent::Cache do
162
167
  100.times do |i|
163
168
  threads << Thread.new do
164
169
  Thread.current['pcache'] = Persistent::Cache.new("multidb")
165
- Thread.current['pcache']["multi_test"] += 1
170
+ Thread.current['pcache']["multi_test"] += 1 if not Thread.current['pcache'].nil? and not Thread.current['pcache']["multi_test"].nil?
166
171
  end
167
172
  end
168
173
  threads.each { |t| t.join }
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require 'rspec'
2
2
  require 'rspec/mocks'
3
+ require 'tempfile'
4
+ require 'simplecov'
5
+ require 'simplecov-rcov'
3
6
 
4
7
  # This file was generated by the `rspec --init` command. Conventionally, all
5
8
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
@@ -21,3 +24,15 @@ RSpec.configure do |config|
21
24
  # --seed 1234
22
25
  config.order = 'random'
23
26
  end
27
+
28
+ SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
29
+ SimpleCov.start do
30
+ add_filter "/spec/"
31
+ end
32
+
33
+ def get_database_name
34
+ path = Tempfile.new("persistent-cache-spec-testdb").path
35
+ FileUtils.rm_f(path)
36
+ path
37
+ end
38
+
@@ -0,0 +1,264 @@
1
+ require 'spec_helper'
2
+ require "persistent-cache/storage/storage_directory"
3
+
4
+ describe Persistent::StorageDirectory do
5
+ before :each do
6
+ @test_key = "testkey"
7
+ @test_value = "testvalue"
8
+ @db_name = get_database_name
9
+ @test_dir = "#{@db_name}/#{@test_key}"
10
+ @test_file = "#{@test_dir}/#{Persistent::StorageDirectory::CACHE_FILE}"
11
+ @test_data = "some data\nmoredata\n\n"
12
+
13
+ delete_database
14
+ @iut = Persistent::StorageDirectory.new(@db_name)
15
+ end
16
+
17
+ context "when constructed" do
18
+ it "should create the database if it does not exist" do
19
+ File.exists?(@db_name).should be_true
20
+ end
21
+
22
+ it "should propagate errors that are raised when failing to create a database" do
23
+ delete_database
24
+ FileUtils.should_receive(:makedirs).and_raise RuntimeError.new("testing")
25
+ expect {
26
+ @iut = Persistent::StorageDirectory.new(@db_name)
27
+ }.to raise_error RuntimeError
28
+ end
29
+
30
+ it "should use the existing database if it does exist" do
31
+ delete_database
32
+ FileUtils.makedirs([@db_name])
33
+ test_file = "#{@db_name}/hello"
34
+ `touch #{test_file}`
35
+ Persistent::StorageDirectory.new(@db_name)
36
+ File.exist?(test_file).should == true
37
+ end
38
+
39
+ it "should have a database" do
40
+ @iut.is_a?(Persistent::StorageDirectory).should == true
41
+ end
42
+
43
+ it "should raise an ArgumentError if storage details have not been provided" do
44
+ expect {
45
+ Persistent::StorageDirectory.new(nil)
46
+ }.to raise_error(ArgumentError)
47
+ end
48
+ end
49
+
50
+ context "when asked to store a key value pair" do
51
+ it "should create a directory named the same as the key, in the storage root, if that directory does not exist, and store the value in a file called CACHE_FILE" do
52
+ setup_test_entry
53
+ read_file_content(@test_file).should == @test_data
54
+ end
55
+
56
+ it "should default to storing the current time as the first line of the catalogue" do
57
+ now = Time.now
58
+ FileUtils.rm_f(@test_dir)
59
+ @iut.save_key_value_pair(@test_key, @test_data)
60
+ read_file_timestamp(@test_file) == now.to_s
61
+ end
62
+
63
+ it "should store a time specified as the first line of the catalogue" do
64
+ time = Time.now - 2500
65
+ FileUtils.rm_f(@test_dir)
66
+ @iut.save_key_value_pair(@test_key, @test_data, time)
67
+ read_file_timestamp(@test_file).should == time.to_s
68
+ end
69
+
70
+ it "should overwrite the existing key/value pair if they already exist" do
71
+ FileUtils.rm_f(@test_dir)
72
+ @iut.save_key_value_pair(@test_key, "old data")
73
+ @iut.save_key_value_pair(@test_key, @test_data)
74
+ File.exists?(@test_dir).should == true
75
+ File.exists?(@test_file).should == true
76
+ read_file_content(@test_file).should == @test_data
77
+ end
78
+
79
+ it "should raise an ArgumentError if the key is not a string" do
80
+ expect {
81
+ @iut.save_key_value_pair(1234, @test_data)
82
+ }.to raise_error ArgumentError
83
+ end
84
+
85
+ it "should raise an ArgumentError if the value is not a string" do
86
+ expect {
87
+ @iut.save_key_value_pair(@test_key, 1234)
88
+ }.to raise_error ArgumentError
89
+ end
90
+
91
+ it "should store the value exactly as given, regardless of newlines" do
92
+ @iut.save_key_value_pair(@test_key, "some data")
93
+ @iut.lookup_key(@test_key)[0][0].should == "some data"
94
+
95
+ @iut.save_key_value_pair(@test_key, "some data\n")
96
+ @iut.lookup_key(@test_key)[0][0].should == "some data\n"
97
+
98
+ @iut.save_key_value_pair(@test_key, "some data\n\n")
99
+ @iut.lookup_key(@test_key)[0][0].should == "some data\n\n"
100
+
101
+ @iut.save_key_value_pair(@test_key, "\nline 1\n23456\n\n\nsome data\n")
102
+ @iut.lookup_key(@test_key)[0][0].should == "\nline 1\n23456\n\n\nsome data\n"
103
+ end
104
+ end
105
+
106
+ context "When looking up a value given its key" do
107
+ it "should retrieve the contents of the catalogue file from the database, excluding the timestamp" do
108
+ setup_test_entry
109
+ result = @iut.lookup_key(@test_key)
110
+ result[0][0].should == @test_data
111
+ end
112
+
113
+ it "should retrieve the timestamp of the revision from the database" do
114
+ now = Time.now
115
+ setup_test_entry
116
+ result = @iut.lookup_key(@test_key)
117
+ result[0][1].should == now.to_s
118
+ end
119
+
120
+ it "should return an empty array if a key is not in the database" do
121
+ setup_test_entry
122
+ result = @iut.lookup_key("thiskeyshouldnotexist")
123
+ result.should == []
124
+ end
125
+
126
+ it "should raise an ArgumentError if the key is not a string" do
127
+ expect {
128
+ @iut.lookup_key(1234)
129
+ }.to raise_error ArgumentError
130
+ end
131
+ end
132
+
133
+ context "when asked to delete an entry" do
134
+ it "should not raise an error if the directory that results from the hash is not present" do
135
+ @iut.delete_entry("thiskeyshouldnotexist")
136
+ end
137
+
138
+ it "should delete the directory that results from the hash if it is present" do
139
+ setup_test_entry
140
+ @iut.delete_entry(@test_key)
141
+ @iut.lookup_key(@test_key).should == []
142
+ File.exists?(@test_dir).should == false
143
+ File.exists?(@test_file).should == false
144
+ end
145
+
146
+ it "should raise an ArgumentError if the key is not a string" do
147
+ expect {
148
+ @iut.delete_entry(1234)
149
+ }.to raise_error ArgumentError
150
+ end
151
+ end
152
+
153
+ context "when asked the size of the database" do
154
+ it "should return 0 if the database has no entries" do
155
+ @iut.size.should == 0
156
+ end
157
+
158
+ it "should return the number of entries" do
159
+ populate_database
160
+ @iut.size.should == 3
161
+ end
162
+
163
+ it "should return 0 if the database does not exist" do
164
+ delete_database
165
+ @iut.size.should == 0
166
+ end
167
+ end
168
+
169
+ context "when asked for the keys in the database" do
170
+ it "should return an empty array if there are no entries in the database" do
171
+ @iut.keys.should == []
172
+ end
173
+
174
+ it "should return the keys (directories) in the database" do
175
+ populate_database
176
+ @iut.keys.should == [["one"], ["three"], ["two"]]
177
+ end
178
+
179
+ it "should return the keys in a sorted array" do
180
+ populate_database
181
+ @iut.keys.should == [["one"], ["three"], ["two"]]
182
+ end
183
+
184
+ it "should not return the storage root itself" do
185
+ populate_database
186
+ @iut.keys.each do |key|
187
+ (key == "").should == false
188
+ (key == "/").should == false
189
+ end
190
+ end
191
+
192
+ it "should return the keys in an array, with each key in its own sub-array" do
193
+ populate_database
194
+ @iut.keys.is_a?(Array).should == true
195
+ @iut.keys[0].is_a?(Array).should == true
196
+ @iut.keys[0][0].is_a?(String).should == true
197
+ end
198
+ end
199
+
200
+ context "when asked to clear the database" do
201
+ it "should not delete the database root directory" do
202
+ setup_test_entry
203
+ @iut.clear
204
+ File.exists?(@test_file).should == false
205
+ File.exists?(@test_dir).should == false
206
+ File.exist?(@iut.storage_root).should == true
207
+ end
208
+
209
+ it "should delete all directories in the database" do
210
+ populate_database
211
+ @iut.clear
212
+ @iut.size.should == 0
213
+ end
214
+ end
215
+
216
+ context "when asked about the path to a key's cache value file" do
217
+ it "should return nil if the key is not in the cache" do
218
+ @iut.get_value_path(@test_key).should == nil
219
+ end
220
+
221
+ it "should return the path to the key's cache value file if the key is in the cache" do
222
+ @iut.save_key_value_pair(@test_key, @test_data)
223
+ @iut.get_value_path(@test_key).should == @test_file
224
+ end
225
+
226
+ it "should raise an ArgumentError if the key is not a string" do
227
+ expect {
228
+ @iut.get_value_path(123)
229
+ }.to raise_error ArgumentError
230
+ end
231
+ end
232
+
233
+ def populate_database
234
+ @iut.save_key_value_pair("one", "one")
235
+ @iut.save_key_value_pair("two", "two")
236
+ @iut.save_key_value_pair("three", "three")
237
+ end
238
+
239
+ def setup_test_entry
240
+ FileUtils.rm_f(@test_dir)
241
+ @iut.save_key_value_pair(@test_key, @test_data)
242
+ File.exists?(@test_dir).should == true
243
+ File.exists?(@test_file).should == true
244
+ end
245
+
246
+ def delete_database
247
+ FileUtils.rm_rf(@db_name)
248
+ File.exists?(@db_name).should == false
249
+ end
250
+
251
+ def read_file_data(file)
252
+ File.read(file).split("\n")
253
+ end
254
+
255
+ def read_file_timestamp(file)
256
+ result = File.read(file)
257
+ result.lines.to_a[0..0].join.split("\n")[0]
258
+ end
259
+
260
+ def read_file_content(file)
261
+ result = File.read(file)
262
+ result.lines.to_a[1..-1].join
263
+ end
264
+ end
@@ -0,0 +1,193 @@
1
+ require 'spec_helper'
2
+ require "persistent-cache/storage/storage_sq_lite"
3
+
4
+ describe Persistent::StorageSQLite do
5
+ before :each do
6
+ @db_name = get_database_name
7
+ delete_database
8
+ @test_key = "testkey"
9
+ @test_value = "testvalue"
10
+ @iut = Persistent::StorageSQLite.new(@db_name)
11
+ end
12
+
13
+ context "when constructed" do
14
+ it "should create the database if it does not exist" do
15
+ File.exists?(@db_name).should be_true
16
+ end
17
+
18
+ it "should create a key_value table with key (TEXT) and value (TEXT) and timestamp (TEXT) columns" do
19
+ handle = SQLite3::Database.open(@db_name)
20
+ result = handle.execute "PRAGMA table_info(#{Persistent::StorageSQLite::DB_TABLE})"
21
+ result[0][1].should == "key"
22
+ result[0][2].should == "TEXT"
23
+ result[1][1].should == "value"
24
+ result[1][2].should == "TEXT"
25
+ result[2][1].should == "timestamp"
26
+ result[2][2].should == "TEXT"
27
+ end
28
+
29
+
30
+ it "should use the existing database if it does exist" do
31
+ delete_database
32
+ handle = SQLite3::Database.new(@db_name)
33
+ handle.execute "create table test123 ( id int );"
34
+ handle.close
35
+ Persistent::StorageSQLite.new(@db_name)
36
+ handle = SQLite3::Database.open(@db_name)
37
+ result = handle.execute "select name from sqlite_master where type='table'"
38
+ result[0][0].should == "test123"
39
+ end
40
+
41
+ it "should have a database handler" do
42
+ @iut.storage_handler.is_a?(SQLite3::Database).should == true
43
+ end
44
+
45
+ it "should set the SQLite busy timeout to DB_TIMEOUT" do
46
+ delete_database
47
+ mock_database = double(SQLite3::Database)
48
+ mock_database.should_receive(:execute).at_least(0)
49
+ mock_database.should_receive(:busy_timeout=).with(Persistent::StorageSQLite::DB_TIMEOUT)
50
+ SQLite3::Database.should_receive(:new).and_return(mock_database)
51
+ Persistent::StorageSQLite.new(@db_name)
52
+ end
53
+
54
+ it "should raise an ArgumentError if storage details have not been provided" do
55
+ expect {
56
+ Persistent::StorageSQLite.new(nil)
57
+ }.to raise_error(ArgumentError)
58
+ end
59
+ end
60
+
61
+ context "when asked to store a key value pair" do
62
+ it "should store the key/value pair in the db, with the current time as timestamp" do
63
+ start_time = Time.now - 1
64
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
65
+ handle = SQLite3::Database.open(@db_name)
66
+ result = handle.execute "select value, timestamp from #{Persistent::StorageSQLite::DB_TABLE} where key=?", Marshal.dump(@test_key)
67
+ result.nil?.should == false
68
+ result[0].nil?.should == false
69
+ result[0][0].should == Marshal.dump(@test_value)
70
+ test_time = Time.parse(result[0][1])
71
+ test_time.should > start_time and test_time.should < start_time + 600
72
+ end
73
+
74
+ it "should store the key/value pair in the db, with a timestamp specified" do
75
+ test_time = (Time.now - 2500)
76
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value), test_time)
77
+ handle = SQLite3::Database.open(@db_name)
78
+ result = handle.execute "select value, timestamp from #{Persistent::StorageSQLite::DB_TABLE} where key=?", Marshal.dump(@test_key)
79
+ result.nil?.should == false
80
+ result[0].nil?.should == false
81
+ result[0][0].should == Marshal.dump(@test_value)
82
+ time_retrieved = Time.parse(result[0][1])
83
+ time_retrieved.to_s.should == test_time.to_s
84
+ end
85
+
86
+ it "should overwrite the existing key/value pair if they already exist" do
87
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
88
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump("testvalue2"))
89
+ handle = SQLite3::Database.open(@db_name)
90
+ result = handle.execute "select value from #{Persistent::StorageSQLite::DB_TABLE} where key=?", Marshal.dump(@test_key)
91
+ result.nil?.should == false
92
+ result[0].nil?.should == false
93
+ result.size.should == 1
94
+ result[0][0].should == Marshal.dump("testvalue2")
95
+ end
96
+ end
97
+
98
+ context "When looking up a value given its key" do
99
+ it "should retrieve the value from the database" do
100
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
101
+ result = @iut.lookup_key(Marshal.dump(@test_key))
102
+ result[0][0].should == Marshal.dump(@test_value)
103
+ end
104
+
105
+ it "should retrieve the timestamp when the value was stored from the database" do
106
+ now = Time.now.to_s
107
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
108
+ sleep 1
109
+ result = @iut.lookup_key(Marshal.dump(@test_key))
110
+ result[0][1].should == now
111
+ end
112
+
113
+ it "should return an empty array if a key is not in the database" do
114
+ @iut.delete_entry(Marshal.dump(@test_key))
115
+ result = @iut.lookup_key(Marshal.dump(@test_key))
116
+ result.should == []
117
+ result[0].should == nil
118
+ end
119
+ end
120
+
121
+ context "when asked to delete an entry" do
122
+ it "should not raise an error if the entry is not present" do
123
+ @iut.delete_entry(Marshal.dump("shouldnotbepresent"))
124
+ end
125
+
126
+ it "should delete the entry if it is present" do
127
+ @iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
128
+ result = @iut.lookup_key(Marshal.dump(@test_key))
129
+ result[0][0].should == Marshal.dump(@test_value)
130
+ @iut.delete_entry(Marshal.dump(@test_key))
131
+ result = @iut.lookup_key(Marshal.dump(@test_key))
132
+ result.should == []
133
+ end
134
+ end
135
+
136
+ context "when asked the size of the database" do
137
+ it "should return 0 if the database has no entries" do
138
+ @iut.size.should == 0
139
+ end
140
+
141
+ it "should return the number of entries" do
142
+ populate_database(@iut)
143
+ @iut.size.should == 3
144
+ end
145
+ end
146
+
147
+ context "when asked for the keys in the database" do
148
+ it "should return an empty array if there are no entries in the database" do
149
+ @iut.keys.should == []
150
+ end
151
+
152
+ it "should return the keys in the database" do
153
+ populate_database(@iut)
154
+ keys = @iut.keys.flatten
155
+ keys.include?(Marshal.dump("one")).should == true
156
+ keys.include?(Marshal.dump("two")).should == true
157
+ keys.include?(Marshal.dump("three")).should == true
158
+ @iut.size.should == 3
159
+ end
160
+
161
+ it "should return the keys in an array, with each key in its own sub-array" do
162
+ populate_database(@iut)
163
+ found = false
164
+ test = Marshal.dump("one")
165
+ found = true if (@iut.keys[0][0] == test or @iut.keys[0][1] == test or @iut.keys[0][2] == test)
166
+ found.should == true
167
+ end
168
+ end
169
+
170
+ context "when asked to clear the database" do
171
+ it "should not delete the database file" do
172
+ populate_database(@iut)
173
+ @iut.clear
174
+ File.exists?(@db_name).should be_true
175
+ end
176
+
177
+ it "should delete all entries in the database" do
178
+ populate_database(@iut)
179
+ @iut.clear
180
+ @iut.size.should == 0
181
+ end
182
+ end
183
+
184
+ def populate_database(iut)
185
+ iut.save_key_value_pair(Marshal.dump("one"), Marshal.dump("one"))
186
+ iut.save_key_value_pair(Marshal.dump("two"), Marshal.dump("two"))
187
+ iut.save_key_value_pair(Marshal.dump("three"), Marshal.dump("three"))
188
+ end
189
+
190
+ def delete_database
191
+ FileUtils.rm_f(@db_name)
192
+ end
193
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: persistent-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-05-08 00:00:00.000000000 Z
13
+ date: 2013-06-12 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rspec
@@ -28,6 +28,38 @@ dependencies:
28
28
  - - '='
29
29
  - !ruby/object:Gem::Version
30
30
  version: 2.12.0
31
+ - !ruby/object:Gem::Dependency
32
+ name: simplecov
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: simplecov-rcov
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
31
63
  - !ruby/object:Gem::Dependency
32
64
  name: sqlite3
33
65
  requirement: !ruby/object:Gem::Requirement
@@ -44,6 +76,22 @@ dependencies:
44
76
  - - '='
45
77
  - !ruby/object:Gem::Version
46
78
  version: 1.3.7
79
+ - !ruby/object:Gem::Dependency
80
+ name: eh
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
47
95
  description: Persistent Cache using SQLite
48
96
  email:
49
97
  - wvd@hetzner.co.za
@@ -60,11 +108,15 @@ files:
60
108
  - README.md
61
109
  - Rakefile
62
110
  - lib/persistent-cache.rb
111
+ - lib/persistent-cache/storage/storage_directory.rb
112
+ - lib/persistent-cache/storage/storage_sq_lite.rb
63
113
  - lib/persistent-cache/version.rb
64
114
  - multidb
65
115
  - persistent-cache.gemspec
66
116
  - spec/persistent-cache_spec.rb
67
117
  - spec/spec_helper.rb
118
+ - spec/storage/storage_directory_spec.rb
119
+ - spec/storage/storage_sqlite_spec.rb
68
120
  homepage: ''
69
121
  licenses: []
70
122
  post_install_message:
@@ -93,3 +145,5 @@ summary: Persistent Cache has a default freshness threshold of 179 days after wh
93
145
  test_files:
94
146
  - spec/persistent-cache_spec.rb
95
147
  - spec/spec_helper.rb
148
+ - spec/storage/storage_directory_spec.rb
149
+ - spec/storage/storage_sqlite_spec.rb