traveladapter 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in traveladapter.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ traveladapter
2
+ =============
3
+
4
+ Wrapper around key-value stores used for caching.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake"
3
+ require "rake/testtask"
4
+
5
+ task :default => [ :unittests ]
6
+
7
+ Rake::TestTask.new("unittests") do |t|
8
+ t.pattern = "test/*_test.rb"
9
+ t.verbose = true
10
+ end
@@ -0,0 +1,2 @@
1
+ require "traveladapter/version"
2
+ require "traveladapter/adapter"
@@ -0,0 +1,216 @@
1
+ require 'thread'
2
+
3
+ module TravelAdapter
4
+
5
+ # Versatile wrapper around key-value stores, providing abstraction
6
+ # and expiration facility.
7
+ class Adapter
8
+
9
+ # Create a new instance of TravelAdapter::Adapter object.
10
+ #
11
+ # If no object is passed, the Adapter will internally use a Hash.
12
+ #
13
+ # @param [Object] store Underlying key-value store (eg. Hash, Redis,
14
+ # LevelDB etc.)
15
+ def initialize(store=nil)
16
+ @semaphore = Mutex::new
17
+ if store.nil?
18
+ @store = Hash::new
19
+ else
20
+ @store = store
21
+ end
22
+ end
23
+
24
+ # Decrement a counter stored under a particular key. A counter can be
25
+ # any numeric object, as long as it responds sensibly to + and -
26
+ # messages.
27
+ #
28
+ # @param [String] key Key used to find a counter object.
29
+ # @return [Numeric] Counter value after decrementing.
30
+ def decrement(key)
31
+ change_by(key, -1)
32
+ end
33
+
34
+ # Delete a key-value pair stored with a particular key.
35
+ #
36
+ # @param [String] key Key used to find a counter object.
37
+ # @return [Boolean] true if the operation succeeds, false if not
38
+ def delete(key)
39
+ begin
40
+ @semaphore.synchronize do
41
+ atomic_delete(key)
42
+ atomic_delete(marshal_key(key))
43
+ atomic_delete(expiration_key(key))
44
+ end
45
+ return true
46
+ rescue Exception
47
+ return false
48
+ end
49
+ end
50
+
51
+ # Get the value stored with a particular key.
52
+ #
53
+ # @param [String] key Key used to find the value.
54
+ # @return [Object] object stored with a given key. Returns nil if the
55
+ # object is not found or the key has expired.
56
+ def get(key)
57
+ return nil unless atomic_has_key?(key)
58
+ if expired?(key)
59
+ delete(key)
60
+ return nil
61
+ elsif needs_marshaling?(key)
62
+ return Marshal::load(atomic_get(key))
63
+ else
64
+ return atomic_get(key)
65
+ end
66
+ end
67
+
68
+ # Check if anything is stored with a particular key.
69
+ #
70
+ # @param [String] key Key used to find a counter object.
71
+ # @return [Boolean] true if the key is not expired and associated with
72
+ # a value, otherwise false.
73
+ def has_key?(key)
74
+ !get(key).nil?
75
+ end
76
+
77
+ # Increment a counter stored under a particular key. A counter can be
78
+ # any numeric object, as long as it responds sensibly to + and -
79
+ # messages.
80
+ #
81
+ # @param [String] key Key used to find a counter object.
82
+ # @return [Numeric] Counter value after incrementing.
83
+ def increment(key)
84
+ change_by(key, 1)
85
+ end
86
+
87
+ # Get non-expired keys stored.
88
+ #
89
+ # @param [Regexp] pattern Regular expression used to filter out keys.
90
+ # @return [Array] Non-expired keys matching the given pattern.
91
+ def keys(pattern=/.*/)
92
+ re = /\A__(expire|marshal)__:/
93
+ return atomic_keys.select do |k|
94
+ k.match(re).nil? and not expired?(k) and !k.match(pattern).nil?
95
+ end
96
+ end
97
+
98
+ # Set the value stored with a particular key.
99
+ #
100
+ # @param [String] key Key used to for the object.
101
+ # @param [String] value Value for the key.
102
+ # @param [Integer] ttl Time (in seconds) to keep the value.
103
+ # @return [Object] object stored.
104
+ def set(key, value, ttl=(10 << 32))
105
+ @semaphore.synchronize do
106
+ if value.is_a?(String)
107
+ atomic_set(marshal_key(key), "0")
108
+ else
109
+ atomic_set(marshal_key(key), "1")
110
+ value = Marshal::dump(value)
111
+ end
112
+ atomic_set(key, value)
113
+ set_ttl(key, ttl)
114
+ end
115
+ return value
116
+ end
117
+
118
+ # Change how long the value is to be stored.
119
+ #
120
+ # @param [String] key Key used to for the object.
121
+ # @param [Integer] ttl Time from now (in seconds) to keep the value.
122
+ # @return [Time] Time when the key expires.
123
+ def set_ttl(key, ttl)
124
+ new_ttl = Time::now.to_i + ttl
125
+ atomic_set(expiration_key(key), new_ttl.to_s)
126
+ return Time::at(new_ttl)
127
+ end
128
+
129
+ # Get expiration time.
130
+ #
131
+ # @param [String] key Key used to for the object.
132
+ # @return [Integer] Number of seconds until object expiration
133
+ def get_ttl(key)
134
+ expires = atomic_get(expiration_key(key))
135
+ return expires.nil? ? nil : (expires.to_i - Time::now.to_i)
136
+ end
137
+
138
+ private
139
+
140
+ def atomic_has_key?(key)
141
+ !atomic_get(key).nil?
142
+ end
143
+
144
+ def atomic_delete(key)
145
+ if @store.respond_to?(:delete)
146
+ @store.delete(key)
147
+ elsif @store.respond_to?(:del)
148
+ @store.del(key)
149
+ else
150
+ @store.atomic_set(key, nil)
151
+ end
152
+ end
153
+
154
+ def atomic_get(key)
155
+ if @store.respond_to?(:fetch)
156
+ begin
157
+ return @store.fetch(key)
158
+ rescue KeyError
159
+ return nil
160
+ end
161
+ elsif @store.respond_to?(:get)
162
+ return @store.get(key)
163
+ else
164
+ return @store[key]
165
+ end
166
+ end
167
+
168
+ def atomic_keys
169
+ @store.keys
170
+ end
171
+
172
+ def atomic_set(key, value)
173
+ if @store.respond_to?(:set)
174
+ @store.set(key, value)
175
+ elsif @store.respond_to?(:put)
176
+ @store.put(key, value)
177
+ else
178
+ @store[key] = value
179
+ end
180
+ end
181
+
182
+ def change_by(key, value)
183
+ @semaphore.synchronize do
184
+ current_value = atomic_get(key)
185
+ unless expired?(key)
186
+ new_value = Marshal::load(current_value) + value
187
+ atomic_set(key, Marshal::dump(new_value))
188
+ return new_value
189
+ else
190
+ return false
191
+ end
192
+ end
193
+ end
194
+
195
+ def expiration_key(key)
196
+ sprintf('__expire__:%s', key)
197
+ end
198
+
199
+ def marshal_key(key)
200
+ sprintf('__marshal__:%s', key)
201
+ end
202
+
203
+ def expired?(key)
204
+ get_ttl(key) <= 0
205
+ end
206
+
207
+ def needs_marshaling?(key)
208
+ atomic_get(marshal_key(key)) == "1"
209
+ end
210
+
211
+ alias :[] :get
212
+ alias :[]= :set
213
+
214
+ end
215
+
216
+ end
@@ -0,0 +1,3 @@
1
+ module TravelAdapter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,98 @@
1
+ require "test/unit"
2
+ require "mocha"
3
+ require "pry"
4
+
5
+ require "traveladapter"
6
+
7
+ Person = Struct::new(:name, :age)
8
+
9
+ class TravelAdapterTest < Test::Unit::TestCase
10
+
11
+ def setup
12
+ @h_adapter = TravelAdapter::Adapter::new
13
+ end
14
+
15
+ def test_set_and_get_without_ttl
16
+ @h_adapter.set("chunky", "bacon")
17
+ assert_equal @h_adapter.get("chunky"), "bacon", "Wrong key has been set"
18
+ assert_equal @h_adapter.keys, ["chunky"], "Unexpected set of keys"
19
+ end
20
+
21
+ def test_aliased_set_and_get_without_ttl
22
+ @h_adapter["chunky"] = "bacon"
23
+ assert_equal @h_adapter["chunky"], "bacon", "Wrong key has been set"
24
+ assert_equal @h_adapter.keys, ["chunky"], "Unexpected set of keys"
25
+ end
26
+
27
+ def test_with_miss
28
+ assert @h_adapter["chunky"].nil?, "Did not expected to find anything"
29
+ end
30
+
31
+ def test_with_marshaling
32
+ @h_adapter["p"] = Person::new("John Smith", 29)
33
+ p = @h_adapter["p"]
34
+ assert p.is_a?(Person), "Expected a Person object"
35
+ assert p.name.is_a?(String), "Expected a String object"
36
+ assert p.age.is_a?(Fixnum), "Expected a Fixnum object"
37
+ assert_equal @h_adapter.keys, ["p"], "Unexpected set of keys"
38
+ end
39
+
40
+ def test_set_and_get_with_ttl
41
+ Time::expects(:now).times(3).returns(*%w(0 3 6))
42
+ @h_adapter.set("chunky", "bacon", 5)
43
+ assert_equal @h_adapter.get("chunky"), "bacon", "Unexpected value"
44
+ assert @h_adapter.get("chunky").nil?, "Expected the key to expire"
45
+ end
46
+
47
+ def test_keys_without_pattern_or_ttl
48
+ @h_adapter.set("hello", "world")
49
+ @h_adapter.set("chunky", "bacon")
50
+ assert @h_adapter.keys.include?("hello")
51
+ assert @h_adapter.keys.include?("chunky")
52
+ end
53
+
54
+ def test_keys_with_pattern
55
+ @h_adapter.set("hello", "world")
56
+ @h_adapter.set("chunky", "bacon")
57
+ assert @h_adapter.keys(/el/).include?("hello")
58
+ assert !@h_adapter.keys(/el/).include?("chunky")
59
+ end
60
+
61
+ def test_keys_with_ttl
62
+ Time::expects(:now).times(6).returns(*%w(0 0 4 4 6 6))
63
+ @h_adapter.set("hello", "world", 5)
64
+ @h_adapter.set("chunky", "bacon", 10)
65
+ keys_at_4 = @h_adapter.keys
66
+ assert keys_at_4.include?("hello")
67
+ assert keys_at_4.include?("chunky")
68
+ keys_at_6 = @h_adapter.keys
69
+ assert !keys_at_6.include?("hello")
70
+ assert keys_at_6.include?("chunky")
71
+ end
72
+
73
+ def test_get_ttl
74
+ Time::expects(:now).times(2).returns(*%w(0 2))
75
+ @h_adapter.set("chunky", "bacon", 10)
76
+ assert_equal @h_adapter.get_ttl("chunky"), 8, "Unexpected TTL"
77
+ end
78
+
79
+ def test_get_ttl_for_a_nonexistent_key
80
+ assert @h_adapter.get_ttl("chunky").nil?, "Unexpected TTL"
81
+ end
82
+
83
+ def test_delete
84
+ @h_adapter["chunky"] = "bacon"
85
+ assert @h_adapter.has_key?("chunky"), "Key expected but not found"
86
+ @h_adapter.delete("chunky")
87
+ assert !@h_adapter.has_key?("chunky"), "Key not expected but found"
88
+ end
89
+
90
+ def test_increment_and_decrement
91
+ @h_adapter["counter"] = 0
92
+ assert_equal @h_adapter.increment("counter"), 1, "Expected the counter to be 1"
93
+ assert_equal @h_adapter.get("counter"), 1, "Expected the counter to be 1"
94
+ assert_equal @h_adapter.decrement("counter"), 0, "Expected the counter to be 0"
95
+ assert_equal @h_adapter.get("counter"), 0, "Expected the counter to be 0"
96
+ end
97
+
98
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "traveladapter/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "traveladapter"
7
+ s.version = TravelAdapter::VERSION
8
+ s.authors = ["Marcin Wyszynski"]
9
+ s.email = ["marcin.pixie@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Wrapper around key-value stores used for caching.}
12
+ s.description = %q{Wrapper around key-value stores used for caching.}
13
+
14
+ s.rubyforge_project = "traveladapter"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency("rake")
22
+ s.add_development_dependency("mocha")
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: traveladapter
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Marcin Wyszynski
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-07-29 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :development
36
+ version_requirements: *id002
37
+ description: Wrapper around key-value stores used for caching.
38
+ email:
39
+ - marcin.pixie@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - .gitignore
48
+ - Gemfile
49
+ - README.md
50
+ - Rakefile
51
+ - lib/traveladapter.rb
52
+ - lib/traveladapter/adapter.rb
53
+ - lib/traveladapter/version.rb
54
+ - test/traveladapter_test.rb
55
+ - traveladapter.gemspec
56
+ homepage: ""
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options: []
61
+
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ requirements: []
77
+
78
+ rubyforge_project: traveladapter
79
+ rubygems_version: 1.8.15
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Wrapper around key-value stores used for caching.
83
+ test_files:
84
+ - test/traveladapter_test.rb