caches 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: dc1818f0238ced3bc043d36e2e5630c7428fbad3
4
+ data.tar.gz: 5e7f76557165b0c31e62fc52dbc8515943e983fd
5
+ !binary "U0hBNTEy":
6
+ metadata.gz: 8a7712863a9cdd2b4fbfc74f8b5898f279857d204677b5ba4ba26be846045fafd8c5932025e218ccd1cae34e5c0c927e95afd3c38de3e3bc5bd0581f075b8a0d
7
+ data.tar.gz: 874d0aa83657382395ac5ae9ce1df07447ebf45158456dac1051225106a186c819b150df963eeccb78027b454e7895c5a93a5c681e3e2c6681181703b26eb5ba
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .ruby-version
19
+ .ruby-gemset
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nathan Long
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # `caches`
2
+
3
+ `caches` is a Ruby gem, providing a small collection of caches with good performance and hash-like access patterns. Each is named for its cache expiration strategy - when it will drop a key.
4
+
5
+ ## Usage
6
+
7
+ `caches` provides the following classes.
8
+
9
+ ### Caches::TTL
10
+
11
+ TTL (Time To Live) remembers values for as many seconds as you tell it. The default is 3600 seconds (1 hour).
12
+
13
+ ```ruby
14
+ require 'caches/ttl'
15
+
16
+ h = Caches::TTL.new(ttl: 5)
17
+ h[:a] = 'aardvark'
18
+ h[:a] #=> 'aardvark'
19
+ sleep(6)
20
+ h[:a] #=> nil
21
+ ```
22
+
23
+ If you pass `refresh: true`, reading a value will reset its timer; otherwise, only writing will.
24
+
25
+ The `memoize` method fetches a key if it exists and isn't expired; otherwise, it calculates the value using the block and saves it.
26
+
27
+ ```ruby
28
+ h = Caches::TTL.new(ttl: 5)
29
+
30
+ # Runs the block
31
+ h.memoize(:a) { |k| calculation_for(k) }
32
+
33
+ # Returns the previously-calculated value
34
+ h.memoize(:a) { |k| calculation_for(k) }
35
+
36
+ sleep(6)
37
+
38
+ # Runs the block
39
+ h.memoize(:a) { |k| calculation_for(k) }
40
+ ```
41
+
42
+ ### Caches::LRU
43
+
44
+ LRU (Least Recently Used) remembers as many keys as you tell it to, dropping the least recently used key on each insert after its limit is reached.
45
+
46
+ ```ruby
47
+ require 'caches/lru'
48
+ h = Caches::LRU.new(max_keys: 3)
49
+ h[:a] = "aardvark"
50
+ h[:b] = "boron"
51
+ h[:c] = "cattail"
52
+ h[:d] = "dingo"
53
+ puts h[:a] # => nil
54
+ ```
55
+
56
+ ## Installation
57
+
58
+ Add this line to your application's Gemfile:
59
+
60
+ gem 'caches'
61
+
62
+ And then execute:
63
+
64
+ $ bundle
65
+
66
+ Or install it yourself as:
67
+
68
+ $ gem install caches
69
+
70
+ ## Coming Soon
71
+
72
+ Other classes with different cache expiration strategies.
73
+
74
+ ## Contributing
75
+
76
+ 1. Fork it
77
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
78
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
79
+ 4. Push to the branch (`git push origin my-new-feature`)
80
+ 5. Create new Pull Request
81
+
82
+ # Thanks
83
+
84
+ - Thanks to [FromAToB.com](http://www.fromatob.com) for giving me time to make this
85
+ - Thanks to [satyap](https://github.com/satyap); the original specs for `caches` were adapted from [volatile_hash](https://github.com/satyap/volatile_hash).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run the specs in documentation format"
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = '--format documentation'
7
+ end
8
+
9
+ task :console do
10
+ require 'irb'
11
+ require 'irb/completion'
12
+ require 'caches/all'
13
+ ARGV.clear
14
+ IRB.start
15
+ end
16
+
17
+ task :default => :spec
data/TODO.md ADDED
@@ -0,0 +1,29 @@
1
+ # TODO
2
+
3
+ ## General
4
+
5
+ - Refactor LinkedList
6
+ - Add benchmarks: speed (and maybe memory usage), runnable with rake task for all cache classes. Be able to compare over the course of development (eg, version 0.0.1 vs 0.0.3). Maybe output ASCII graphs for fun.
7
+ - Add Travis CI testing
8
+ - Give all caches a way to manually invalidate a given key. Return nil if it wasn't here and the value if it was.
9
+ - Add "least used" cache class
10
+ - Define `blank?`, etc on list and caches
11
+ - Should TTL keys be renewed by updates? LRU ones aren't. If the cache exists to be read, probably not.
12
+ - Ride a skateboard and, simultaneously, shoot a potato gun
13
+
14
+ ## New types
15
+
16
+ Build LU - Least Used. Expiration criteria is essentially "uses per second" - reads divided by time since write.
17
+
18
+ Implementation ideas:
19
+ - Passage of time doesn't change ranking of keys; only writing does.
20
+ - Keep a doubly-linked list of keys ordered by usefulness and a reference to the tail for easy dropping
21
+ - When updating a key, be able to quickly compare it with the one ahead of it and swap if it overtook it in rank
22
+ - Normal hash data structure has pointers into linked list segments
23
+
24
+
25
+ ## Benchmarks
26
+
27
+ TTL: Prepopulate instances differing in size by orders of magnitude. Measure N operations of various types. These vary on three axes: read/write, expired/fresh, and update/insert for writes. Output results as graph. Visually confirm that it appears O(1) and is reasonably fast (?) in absolute terms.
28
+
29
+ LRU: ?
data/caches.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'caches/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "caches"
8
+ spec.version = Caches::VERSION
9
+ spec.authors = ["Nathan Long"]
10
+ spec.email = ["nathanmlong@gmail.com"]
11
+ spec.description = %q{A small collection of hashes that cache}
12
+ spec.summary = %q{A small collection of hashes that cache}
13
+ spec.homepage = "https://github.com/nathanl/caches"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", "~> 2.14"
24
+
25
+ end
@@ -0,0 +1,19 @@
1
+ module Caches
2
+ module Accessible
3
+
4
+ def fetch(key, default = (default_omitted = true; nil))
5
+ return self[key] if data.has_key?(key)
6
+ return yield(key) if block_given?
7
+ return default unless default_omitted
8
+ raise KeyError
9
+ end
10
+
11
+ def memoize(key)
12
+ raise ArgumentError, "Block is required" unless block_given?
13
+ self[key] # triggers flush or refresh if expired
14
+ return self[key] if data.has_key?(key)
15
+ self[key] = yield(key) if block_given?
16
+ end
17
+
18
+ end
19
+ end
data/lib/caches/all.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative 'ttl'
2
+ require_relative 'lru'
@@ -0,0 +1,115 @@
1
+ module Caches
2
+ # This LinkedList class is unusual in that it gives direct access to its nodes.
3
+ # It trusts the user not to break it! The advantage is that outsiders with
4
+ # a node reference can reorder the list (eg, using #move_to_head) in O(1) time.
5
+ class LinkedList
6
+ include Enumerable
7
+
8
+ attr_reader :length
9
+
10
+ def initialize(item = nil)
11
+ if item
12
+ Node.new(item).tap { |node|
13
+ self.head = self.tail = node
14
+ self.length = 1
15
+ }
16
+ else
17
+ self.length = 0
18
+ end
19
+ end
20
+
21
+ def each
22
+ current = head
23
+ while current do
24
+ yield(current.value)
25
+ current = current.right
26
+ end
27
+ end
28
+
29
+ def append(item)
30
+ return initialize(item) if empty?
31
+ Node.new(item, left: tail).tap { |new_tail|
32
+ tail.right = new_tail
33
+ self.tail = new_tail
34
+ self.length = length + 1
35
+ }
36
+ end
37
+
38
+ def prepend(item)
39
+ return initialize(item) if empty?
40
+ Node.new(item, right: head).tap { |new_head|
41
+ head.left = new_head
42
+ self.head = new_head
43
+ self.length = length + 1
44
+ }
45
+ end
46
+ alias :unshift :prepend
47
+
48
+ def pop
49
+ tail.tap {
50
+ new_tail = tail.left
51
+ new_tail.right = nil
52
+ self.tail = new_tail
53
+ self.length = length - 1
54
+ }
55
+ end
56
+
57
+ def lpop
58
+ head.tap {
59
+ new_head = head.right
60
+ new_head.left = nil
61
+ self.head = new_head
62
+ self.length = length - 1
63
+ }
64
+ end
65
+ alias :shift :lpop
66
+
67
+ def move_to_head(node)
68
+ excise(node)
69
+ node.right = head
70
+ self.head = node
71
+ end
72
+
73
+ def empty?
74
+ length == 0
75
+ end
76
+
77
+ # TODO - move these to a module and use in caches, too
78
+ alias :blank? :empty?
79
+ def present?
80
+ !empty?
81
+ end
82
+ def presence
83
+ self if present?
84
+ end
85
+
86
+ private
87
+ attr_accessor :head, :tail
88
+ attr_writer :length
89
+
90
+ def excise(node)
91
+ raise InvalidNode unless node.is_a?(Node)
92
+ left = node.left
93
+ right = node.right
94
+ left.right = right unless left.nil?
95
+ right.left = left unless right.nil?
96
+ self.head = right if head == node
97
+ self.tail = left if tail == node
98
+ node
99
+ end
100
+
101
+ class Node
102
+ attr_accessor :value, :right, :left
103
+ def initialize(value, pointers = {})
104
+ self.value = value
105
+ self.right = pointers[:right]
106
+ self.left = pointers[:left]
107
+ end
108
+
109
+ def right?; !right.nil?; end
110
+ def left?; !left.nil? end
111
+ end
112
+
113
+ InvalidNode = Class.new(StandardError)
114
+ end
115
+ end
data/lib/caches/lru.rb ADDED
@@ -0,0 +1,47 @@
1
+ require_relative 'accessible'
2
+ require_relative 'linked_list'
3
+
4
+ module Caches
5
+ class LRU
6
+ include Accessible
7
+ attr_accessor :max_keys
8
+
9
+ def initialize(options = {})
10
+ self.max_keys = options.fetch(:max_keys, 20)
11
+ self.data = {}
12
+ self.keys = LinkedList.new
13
+ end
14
+
15
+ def [](key)
16
+ return nil unless data.has_key?(key)
17
+ value, node = data[key]
18
+ keys.move_to_head(node)
19
+ value
20
+ end
21
+
22
+ def []=(key, val)
23
+ if data.has_key?(key)
24
+ data[key][0] = val
25
+ else
26
+ node = keys.prepend(key)
27
+ data[key] = [val, node]
28
+ end
29
+ prune
30
+ val
31
+ end
32
+
33
+ def size
34
+ keys.length
35
+ end
36
+
37
+ private
38
+ attr_accessor :keys, :data
39
+
40
+ def prune
41
+ return unless keys.length > max_keys
42
+ expiring_node = keys.pop
43
+ data.delete(expiring_node.value)
44
+ end
45
+
46
+ end
47
+ end
data/lib/caches/ttl.rb ADDED
@@ -0,0 +1,42 @@
1
+ require_relative 'accessible'
2
+
3
+ module Caches
4
+ class TTL
5
+ include Accessible
6
+ attr_accessor :ttl, :refresh
7
+
8
+ def initialize(options = {})
9
+ self.ttl = options.fetch(:ttl) { 3600 }
10
+ self.refresh = !!(options.fetch(:refresh, false))
11
+ self.data = {}
12
+ end
13
+
14
+ def [](key)
15
+ return nil unless data.has_key?(key)
16
+ if (current_time - data[key][:time]) < ttl
17
+ data[key][:time] = current_time if refresh
18
+ data[key][:value]
19
+ else
20
+ data.delete(key)
21
+ nil
22
+ end
23
+ end
24
+
25
+ def []=(key, val)
26
+ data[key] = {time: current_time, value: val}
27
+ end
28
+
29
+ def size
30
+ data.keys.length
31
+ end
32
+
33
+ private
34
+
35
+ attr_accessor :data
36
+
37
+ def current_time
38
+ Time.now
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Caches
2
+ VERSION = "0.0.1"
3
+ end
data/lib/caches.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'time'
2
+ require "caches/version"
3
+
4
+ module Caches
5
+ end
@@ -0,0 +1,24 @@
1
+ shared_examples "fetch" do
2
+
3
+ it "returns the value if an existing key is accessed" do
4
+ expect(cache.fetch(:c)).to eq('Caspian')
5
+ end
6
+
7
+ it "raises an error if a missing key is accessed" do
8
+ expect{cache.fetch(:nonexistent)}.to raise_error(KeyError)
9
+ end
10
+
11
+ it "uses a default value if one is supplied" do
12
+ expect(cache.fetch(:nonexistent, 'hi')).to eq('hi')
13
+ end
14
+
15
+ it "gets its default from a block if one is supplied" do
16
+ expect(cache.fetch(:nonexistent) { |key| key.to_s.upcase }).to eq('NONEXISTENT')
17
+ end
18
+
19
+ it "prefers a block value to an immediate one (like native hashes do)" do
20
+ expect(cache.fetch(:nonexistent, 'hi') { |key| key.to_s.upcase }).to eq('NONEXISTENT')
21
+ end
22
+
23
+ end
24
+
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/caches/linked_list'
3
+
4
+ describe Caches::LinkedList do
5
+
6
+ let(:empty_list) { described_class.new }
7
+ let(:list) { described_class.new('lemur') }
8
+ let(:node_class) { Caches::LinkedList::Node }
9
+
10
+ it "can convert itself to an array of values" do
11
+ expect(list.to_a).to eq(['lemur'])
12
+ end
13
+
14
+ describe '#append' do
15
+
16
+ it "adds an item to the end" do
17
+ list.append('wombat')
18
+ expect(list.to_a).to eq(['lemur', 'wombat'])
19
+ end
20
+
21
+ it "returns a Node" do
22
+ node = list.append('wombat')
23
+ expect(node).to be_a(node_class)
24
+ end
25
+
26
+ it "sets up the list correctly if it was empty to start with" do
27
+ empty_list.append('jimmy')
28
+ expect{empty_list.prepend('jammy')}.not_to raise_error
29
+ end
30
+
31
+ end
32
+
33
+ describe "#prepend" do
34
+
35
+ it "adds an item to the beginning" do
36
+ list.prepend('vole')
37
+ expect(list.to_a).to eq(['vole', 'lemur'])
38
+ end
39
+
40
+ it "returns a Node" do
41
+ node = list.prepend('vole')
42
+ expect(node).to be_a(node_class)
43
+ end
44
+
45
+ it "sets up the list correctly if it was empty to start with" do
46
+ empty_list.prepend('jimmy')
47
+ expect{empty_list.append('jammy')}.not_to raise_error
48
+ end
49
+
50
+ end
51
+
52
+ it "knows its length" do
53
+ expect(list.length).to eq(1)
54
+ list.append('wombat')
55
+ list.prepend('vole')
56
+ expect(list.length).to eq(3)
57
+ end
58
+
59
+ describe "#move_to_head" do
60
+
61
+ context "when the argument is a Node" do
62
+
63
+ it "makes it the new head" do
64
+ list.append('mittens')
65
+ node = list.append('cozy')
66
+ list.move_to_head(node)
67
+ expect(list.to_a).to eq(%w[cozy lemur mittens])
68
+ end
69
+
70
+ end
71
+
72
+ context "when the argument is not a Node" do
73
+
74
+ it "raises an error" do
75
+ list.append('cozy')
76
+ expect{list.move_to_head('super duper')}.to raise_error(Caches::LinkedList::InvalidNode)
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ context "when there's more than one item" do
84
+
85
+ let(:list) {
86
+ list = described_class.new('lemur')
87
+ list.append('muskrat')
88
+ list
89
+ }
90
+
91
+ describe "#pop" do
92
+
93
+ it "removes the last item" do
94
+ list.pop
95
+ expect(list.to_a).to eq(['lemur'])
96
+ end
97
+
98
+ it "returns a node" do
99
+ expect(list.pop).to be_a(node_class)
100
+ end
101
+
102
+ end
103
+
104
+
105
+ describe "#lpop" do
106
+
107
+ it "removes the first item" do
108
+ list.lpop
109
+ expect(list.to_a).to eq(['muskrat'])
110
+ end
111
+
112
+ it "returns a node" do
113
+ expect(list.lpop).to be_a(node_class)
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+ require 'fetch_examples'
3
+ require_relative '../../lib/caches/lru'
4
+
5
+ describe Caches::LRU do
6
+
7
+ let(:options) { {max_keys: 4} }
8
+ let(:cache) {
9
+ described_class.new(options).tap {|c|
10
+ c[:a] = 'Alambil'
11
+ c[:b] = 'Belisar'
12
+ c[:c] = 'Caspian'
13
+ }
14
+ }
15
+
16
+ it "remembers its values" do
17
+ expect(cache[:a]).to eq('Alambil')
18
+ expect(cache[:b]).to eq('Belisar')
19
+ expect(cache[:c]).to eq('Caspian')
20
+ end
21
+
22
+ it "can report its size" do
23
+ expect(cache.size).to eq(3)
24
+ end
25
+
26
+ it "can grow up to the max size given" do
27
+ cache[:d] = 'Destrier'
28
+ expect(cache.size).to eq(4)
29
+ end
30
+
31
+ it "does not grow beyond the max size given" do
32
+ cache[:d] = 'Destrier'
33
+ cache[:e] = 'Erimon'
34
+ expect(cache.size).to eq(4)
35
+ end
36
+
37
+ it "drops the least-recently accessed key" do
38
+ cache[:d] = 'Destrier'
39
+ cache[:e] = 'Erimon'
40
+ expect(cache[:a]).to be_nil
41
+ end
42
+
43
+ it "counts a read as access" do
44
+ cache[:d] = 'Destrier'
45
+ cache[:a]
46
+ cache[:e] = 'Erimon'
47
+ expect(cache[:a]).to eq('Alambil')
48
+ expect(cache[:b]).to be_nil
49
+ end
50
+
51
+ include_examples "fetch"
52
+
53
+ describe "memoize" do
54
+
55
+ it "requires a block" do
56
+ expect{cache_memoize(:c)}.to raise_error
57
+ end
58
+
59
+ let(:greeting) { 'hi' }
60
+
61
+ context "when the key already exists" do
62
+
63
+ it "does not calculate the value" do
64
+ expect(greeting).not_to receive(:upcase)
65
+ cache.memoize(:c) { greeting.upcase }
66
+ end
67
+
68
+ end
69
+
70
+ context "when the key has not been set" do
71
+
72
+ let(:key) { :nonexistent }
73
+
74
+ it "calculates the value" do
75
+ expect(greeting).to receive(:upcase)
76
+ cache.memoize(:nonexistent) { greeting.upcase }
77
+ end
78
+
79
+ it "sets the value" do
80
+ cache.memoize(:nonexistent) { greeting.upcase }
81
+ expect(cache[:nonexistent]).to eq(greeting.upcase)
82
+ end
83
+
84
+ end
85
+
86
+ context "when the key has been dropped" do
87
+
88
+ before :each do
89
+ cache[:d] = 'Destrier'
90
+ cache[:e] = 'Erimon'
91
+ end
92
+
93
+ it "calculates the value" do
94
+ expect(greeting).to receive(:upcase)
95
+ cache.memoize(:a) { greeting.upcase }
96
+ end
97
+
98
+ it "sets the value" do
99
+ cache.memoize(:a) { greeting.upcase }
100
+ expect(cache[:a]).to eq(greeting.upcase)
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+
107
+ end
108
+
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+ require 'fetch_examples'
3
+ require_relative '../../lib/caches/ttl'
4
+
5
+ describe Caches::TTL do
6
+
7
+ let(:options) { {} }
8
+ let(:cache) {
9
+ described_class.new(options).tap {|c|
10
+ c[:c] = 'Caspian'
11
+ c.stub(:current_time).and_return(start_time)
12
+ }
13
+ }
14
+
15
+ let(:start_time) { Time.now }
16
+ let(:before_ttl) { start_time + 1800 }
17
+ let(:after_ttl) { start_time + 3601 }
18
+
19
+ it "can report its size" do
20
+ expect(cache.size).to eq(1)
21
+ end
22
+
23
+ it "remembers cached values before the TTL expires" do
24
+ expect(cache[:c]).to eq('Caspian')
25
+ cache.stub(:current_time).and_return(before_ttl)
26
+ expect(cache[:c]).to eq('Caspian')
27
+ end
28
+
29
+ it "forgets cached values after the TTL expires" do
30
+ expect(cache[:c]).to eq('Caspian')
31
+ cache.stub(:current_time).and_return(after_ttl)
32
+ expect(cache[:c]).to be_nil
33
+ end
34
+
35
+ it "continues returning nil for cached values after the TTL expires" do
36
+ expect(cache[:c]).to eq('Caspian')
37
+ cache.stub(:current_time).and_return(after_ttl)
38
+ expect(cache[:c]).to be_nil
39
+ expect(cache[:c]).to be_nil
40
+ end
41
+
42
+ it "resets TTL when an item is updated" do
43
+ cache.stub(:current_time).and_return(before_ttl)
44
+ cache[:c] = 'Cornelius'
45
+ cache.stub(:current_time).and_return(after_ttl)
46
+ expect(cache[:c]).to eq('Cornelius')
47
+ end
48
+
49
+ it "doesn't reset TTL when an item is accessed" do
50
+ cache.stub(:current_time).and_return(before_ttl)
51
+ expect(cache[:c]).to eq('Caspian')
52
+ cache.stub(:current_time).and_return(after_ttl)
53
+ expect(cache[:c]).to be_nil
54
+ end
55
+
56
+ context "when asked to refresh TTL on access" do
57
+
58
+ let(:options) { {refresh: true} }
59
+
60
+ it "keeps values that were accessed before the TTL expired" do
61
+ cache.stub(:current_time).and_return(before_ttl)
62
+ expect(cache[:c]).to eq('Caspian')
63
+ cache.stub(:current_time).and_return(after_ttl)
64
+ expect(cache[:c]).to eq('Caspian')
65
+ end
66
+
67
+ end
68
+
69
+ include_examples "fetch"
70
+
71
+ describe "memoize" do
72
+
73
+ it "requires a block" do
74
+ expect{cache_memoize(:c)}.to raise_error
75
+ end
76
+
77
+ let(:greeting) { 'hi' }
78
+
79
+ context "when the key already exists" do
80
+
81
+ context "before the TTL is up" do
82
+
83
+ it "returns the existing value" do
84
+ expect(cache.memoize(:c) { 'Eustace' } ).to eq('Caspian')
85
+ end
86
+
87
+ it "does not calculate the value" do
88
+ cache.stub(:current_time).and_return(before_ttl)
89
+ expect(greeting).not_to receive(:upcase)
90
+ cache.memoize(:c) { greeting.upcase }
91
+ end
92
+
93
+ end
94
+
95
+ context "after the TTL is up" do
96
+
97
+ it "recalculates the value" do
98
+ cache.stub(:current_time).and_return(after_ttl)
99
+ expect(greeting).to receive(:upcase)
100
+ cache.memoize(:c) { greeting.upcase }
101
+ end
102
+
103
+ it "returns the calculated value" do
104
+ cache.stub(:current_time).and_return(after_ttl)
105
+ expect(cache.memoize(:c) { |key| key.to_s.upcase }).to eq('C')
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+
112
+ context "when the key does not exist" do
113
+
114
+ it "calculates the value" do
115
+ expect(greeting).to receive(:upcase)
116
+ cache.memoize(:nonexistent) { greeting.upcase }
117
+ end
118
+
119
+ it "returns the value" do
120
+ expect(cache.memoize(:nonexistent) { |key| key.to_s.upcase }).to eq('NONEXISTENT')
121
+ end
122
+
123
+ it "does not calculate the value again within the TTL" do
124
+ cache.memoize(:nonexistent) { greeting.upcase }
125
+ cache.stub(:current_time).and_return(before_ttl)
126
+ expect(greeting).not_to receive(:upcase)
127
+ cache.memoize(:nonexistent) { greeting.upcase }
128
+ end
129
+
130
+ it "does calculate the value again after the TTL is up" do
131
+ cache.memoize(:nonexistent) { greeting.upcase }
132
+ cache.stub(:current_time).and_return(after_ttl)
133
+ expect(greeting).to receive(:upcase)
134
+ cache.memoize(:nonexistent) { greeting.upcase }
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ RSpec.configure do |config|
5
+ config.order = :random
6
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: caches
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Long
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.14'
55
+ description: A small collection of hashes that cache
56
+ email:
57
+ - nathanmlong@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - .gitignore
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - TODO.md
68
+ - caches.gemspec
69
+ - lib/caches.rb
70
+ - lib/caches/accessible.rb
71
+ - lib/caches/all.rb
72
+ - lib/caches/linked_list.rb
73
+ - lib/caches/lru.rb
74
+ - lib/caches/ttl.rb
75
+ - lib/caches/version.rb
76
+ - spec/fetch_examples.rb
77
+ - spec/hash_cache/linked_list_spec.rb
78
+ - spec/hash_cache/lru_spec.rb
79
+ - spec/hash_cache/ttl_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: https://github.com/nathanl/caches
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.0.14
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: A small collection of hashes that cache
105
+ test_files:
106
+ - spec/fetch_examples.rb
107
+ - spec/hash_cache/linked_list_spec.rb
108
+ - spec/hash_cache/lru_spec.rb
109
+ - spec/hash_cache/ttl_spec.rb
110
+ - spec/spec_helper.rb