coffee_table 0.0.3 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +22 -1
- data/README.textile +24 -18
- data/changelog.txt +11 -0
- data/coffee_table.gemspec +1 -0
- data/lib/coffee_table.rb +35 -33
- data/lib/coffee_table/key.rb +80 -0
- data/lib/{utility.rb → coffee_table/utility.rb} +0 -0
- data/lib/coffee_table/version.rb +1 -1
- data/spec/lib/coffee_table_spec.rb +182 -72
- data/spec/lib/key_spec.rb +69 -0
- data/spec/spec_helper.rb +7 -0
- metadata +26 -11
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,10 +1,21 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git@github.com:stewartmckee/sourcify.git
|
3
|
+
revision: cdef8e0b3b7017312407000985a279d6627c77e5
|
4
|
+
specs:
|
5
|
+
sourcify (0.6.0.rc3)
|
6
|
+
file-tail (~> 1.0.10)
|
7
|
+
ruby2ruby (~> 1.3.1)
|
8
|
+
ruby_parser (~> 2.3.1)
|
9
|
+
sexp_processor (~> 3.2.0)
|
10
|
+
|
1
11
|
PATH
|
2
12
|
remote: .
|
3
13
|
specs:
|
4
|
-
coffee_table (0.0
|
14
|
+
coffee_table (0.1.0)
|
5
15
|
activesupport
|
6
16
|
redis
|
7
17
|
rufus-scheduler
|
18
|
+
sourcify
|
8
19
|
|
9
20
|
GEM
|
10
21
|
remote: http://rubygems.org/
|
@@ -14,6 +25,8 @@ GEM
|
|
14
25
|
multi_json (~> 1.0)
|
15
26
|
awesome_print (1.1.0)
|
16
27
|
diff-lcs (1.1.3)
|
28
|
+
file-tail (1.0.12)
|
29
|
+
tins (~> 0.5)
|
17
30
|
i18n (0.6.1)
|
18
31
|
mock_redis (0.6.3)
|
19
32
|
multi_json (1.0.4)
|
@@ -26,9 +39,16 @@ GEM
|
|
26
39
|
rspec-expectations (2.12.1)
|
27
40
|
diff-lcs (~> 1.1.3)
|
28
41
|
rspec-mocks (2.12.1)
|
42
|
+
ruby2ruby (1.3.1)
|
43
|
+
ruby_parser (~> 2.0)
|
44
|
+
sexp_processor (~> 3.0)
|
45
|
+
ruby_parser (2.3.1)
|
46
|
+
sexp_processor (~> 3.0)
|
29
47
|
rufus-scheduler (2.0.17)
|
30
48
|
tzinfo (>= 0.3.23)
|
49
|
+
sexp_processor (3.2.0)
|
31
50
|
spork (0.9.2)
|
51
|
+
tins (0.7.0)
|
32
52
|
tzinfo (0.3.35)
|
33
53
|
|
34
54
|
PLATFORMS
|
@@ -42,4 +62,5 @@ DEPENDENCIES
|
|
42
62
|
redis
|
43
63
|
rspec
|
44
64
|
rufus-scheduler
|
65
|
+
sourcify!
|
45
66
|
spork
|
data/README.textile
CHANGED
@@ -1,15 +1,24 @@
|
|
1
1
|
|
2
|
-
h1. CoffeeTable v0.
|
2
|
+
h1. CoffeeTable v0.1.1
|
3
3
|
|
4
|
-
!https://secure.travis-ci.org/stewartmckee/coffee_table.png?branch=master!
|
5
4
|
!https://gemnasium.com/stewartmckee/coffee_table.png!
|
6
5
|
|
7
6
|
h2. Intro
|
8
7
|
|
9
|
-
CoffeeTable was born out of a frustration with the standard caching methods
|
8
|
+
CoffeeTable was born out of a frustration with the standard caching methods. Maintaining the cache keys constantly was a headache and 'bet its a caching issue' was a phrase uttered way too much. CoffeeTable was designed to take on the role of maintaining the cache keys for you, allowing you to concentrate on what is in the cache. It works by maintaining a list of its keys in a known format and when expiry is required for an object it knows which ones to expire. It also hopefully will be a perfromance boost for some cases where you are being overly cautious about clearing cache, a more targeted approach will improve performance.
|
10
9
|
|
11
10
|
h3. Installation
|
12
11
|
|
12
|
+
h4. Using Rails/Bundler
|
13
|
+
|
14
|
+
Put the following in your Gemfile and run 'bundle'
|
15
|
+
|
16
|
+
bc. gem 'coffee_table'
|
17
|
+
|
18
|
+
h4. Straight Ruby
|
19
|
+
|
20
|
+
Run the following at the command prompt
|
21
|
+
|
13
22
|
bc. gem install coffee_table
|
14
23
|
|
15
24
|
h2. Usage
|
@@ -18,12 +27,13 @@ h3. CoffeeTable::Cache
|
|
18
27
|
|
19
28
|
h4. new(options)
|
20
29
|
|
21
|
-
Creates a new cache object. You can pass
|
30
|
+
Creates a new cache object. You can pass options into this method to modify the cache behaviour.
|
22
31
|
|
23
|
-
* :enable_cache
|
24
|
-
* :redis_namespace defaults to ":coffee_table" and is set to seperate out the keys from other redis users
|
32
|
+
* :enable_cache This defaults to true, but can be set to false to disable the cache
|
33
|
+
* :redis_namespace defaults to ":coffee_table" and is set to seperate out the keys from other redis users or other caches
|
25
34
|
* :redis_server defaults to "127.0.0.1"
|
26
35
|
* :redis_port defaults to 6789
|
36
|
+
* :ignore_code_changes defaults to false. By default a md5 hash of the code in the block is included in the key, if you change the code, the key automatically invalidates. This is to protect against code changes that won't be picked up due to the cache returning.
|
27
37
|
|
28
38
|
|
29
39
|
h4. get_cache(initial_key, *related_objects, &block)
|
@@ -42,31 +52,23 @@ bc. user_details = @coffee_table.get_cache(:user_detail, User) do
|
|
42
52
|
@user.get_something_that_uses_all_users
|
43
53
|
end
|
44
54
|
|
45
|
-
|
55
|
+
This would be expired with 'expire_for(User)' which will clear all user cache items regardless of the specific object id.
|
46
56
|
|
47
57
|
The only required field is the first parameter, so you can create keys and cache as you normally would, ignoring the objects.
|
48
58
|
|
49
|
-
h4. expire_key(key)
|
50
|
-
|
51
|
-
This method directly expires a known key.
|
52
|
-
|
53
|
-
bc. @coffee_table.expire_key("user_detail_user[1]")
|
54
|
-
|
55
|
-
The above code would expire the above example of cache.
|
56
|
-
|
57
59
|
h4. expire_all
|
58
60
|
|
59
|
-
This method clears the whole cache.
|
61
|
+
This method clears the whole cache removing all cache items.
|
60
62
|
|
61
63
|
bc. @coffee_table.expire_all
|
62
64
|
|
63
65
|
h4. keys
|
64
66
|
|
65
|
-
This is a helper method to return the list of keys currently in the system. This list is maintained when cache is created and expired.
|
67
|
+
This is a helper method to return the list of keys currently in the system. This list is maintained when cache is created and expired. Can also be used for debug purposes when investigating an issue.
|
66
68
|
|
67
69
|
h4. expire_for(*objects)
|
68
70
|
|
69
|
-
This is the main expire method. In order to expire a cache, you
|
71
|
+
This is the main expire method. In order to expire a cache item, you pass in any objects that would be invalidated. With the above example this would be as follows.
|
70
72
|
|
71
73
|
bc. @coffee_table.expire_for(@user)
|
72
74
|
|
@@ -77,3 +79,7 @@ You can also expire for a whole class type
|
|
77
79
|
bc. @coffee_table.expire_for(User)
|
78
80
|
|
79
81
|
this would expire all keys that reference the user objects.
|
82
|
+
|
83
|
+
The best practice for this is to be as specific as you can when creating the key. Also creating more targeted cache items may be better in some situations than having one large cache fragment.
|
84
|
+
|
85
|
+
|
data/changelog.txt
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
0.1.1
|
3
|
+
- updated documentation
|
4
|
+
- refacotred get_cache method to fetch
|
5
|
+
|
6
|
+
0.1.0
|
7
|
+
|
8
|
+
- Started a change log
|
9
|
+
- Added sourcify to md5 hash the contents of the block to remove issues around code changes not being used due to cache
|
10
|
+
- Added a key class to handle key generation and management
|
11
|
+
|
data/coffee_table.gemspec
CHANGED
data/lib/coffee_table.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
require "coffee_table/version"
|
2
|
-
require "utility"
|
2
|
+
require "coffee_table/utility"
|
3
3
|
require "redis"
|
4
4
|
require 'rufus/scheduler'
|
5
5
|
require 'active_support/inflector'
|
6
|
+
require "sourcify"
|
7
|
+
require 'digest/md5'
|
6
8
|
|
7
9
|
module CoffeeTable
|
8
10
|
class Cache
|
9
11
|
|
10
12
|
include CoffeeTable::Utility
|
11
13
|
|
14
|
+
|
15
|
+
# initialize for coffee_table. takes options to setup behaviour of cache
|
12
16
|
def initialize(options={})
|
13
17
|
@options = options
|
14
18
|
|
@@ -16,13 +20,15 @@ module CoffeeTable
|
|
16
20
|
default_redis_namespace_to :coffee_table
|
17
21
|
default_redis_server_to "127.0.0.1"
|
18
22
|
default_redis_port_to 6789
|
23
|
+
default_ignore_code_changes_to false
|
19
24
|
|
20
|
-
|
25
|
+
@redis = Redis.new({:server => @options[:redis_server], :port => @options[:redis_port]})
|
21
26
|
@scheduler = Rufus::Scheduler.start_new
|
22
27
|
|
23
28
|
end
|
24
29
|
|
25
|
-
|
30
|
+
# main
|
31
|
+
def fetch(initial_key, *related_objects, &block)
|
26
32
|
|
27
33
|
raise CoffeeTableBlockMissingError, "No block given to generate cache from" unless block_given?
|
28
34
|
|
@@ -36,12 +42,14 @@ module CoffeeTable
|
|
36
42
|
# check objects are valid
|
37
43
|
related_objects.flatten.map{|o| raise CoffeeTableInvalidObjectError, "Objects passed in must have an id method or be a class" unless object_valid?(o)}
|
38
44
|
|
39
|
-
|
40
|
-
|
41
|
-
key = "#{initial_key}"
|
45
|
+
if @options[:ignore_code_changes]
|
46
|
+
block_key = ""
|
42
47
|
else
|
43
|
-
|
48
|
+
block_key = Digest::MD5.hexdigest(block.to_source)
|
44
49
|
end
|
50
|
+
|
51
|
+
# if first related_object is integer or fixnum it is used as an expiry time for the cache object
|
52
|
+
key = Key.new(initial_key, block_key, related_objects)
|
45
53
|
|
46
54
|
if @options[:enable_cache]
|
47
55
|
if options.has_key?(:expiry)
|
@@ -50,9 +58,9 @@ module CoffeeTable
|
|
50
58
|
expiry = nil
|
51
59
|
end
|
52
60
|
|
53
|
-
@redis.sadd "cache_keys", key unless @redis.sismember "cache_keys", key
|
54
|
-
if @redis.exists(key)
|
55
|
-
result = marshal_value(@redis.get(key))
|
61
|
+
@redis.sadd "cache_keys", key unless @redis.sismember "cache_keys", key.to_s
|
62
|
+
if @redis.exists(key.to_s)
|
63
|
+
result = marshal_value(@redis.get(key.to_s))
|
56
64
|
else
|
57
65
|
result = yield
|
58
66
|
# if its a relation, call all to get an array to cache the result
|
@@ -60,11 +68,11 @@ module CoffeeTable
|
|
60
68
|
# @logger.debug "Expanding ActiveRecord::Relation..."
|
61
69
|
# result = result.all
|
62
70
|
#end
|
63
|
-
@redis.set key, Marshal.dump(result)
|
71
|
+
@redis.set key.to_s, Marshal.dump(result)
|
64
72
|
unless expiry.nil?
|
65
|
-
@redis.expire key, expiry
|
73
|
+
@redis.expire key.to_s, expiry
|
66
74
|
@scheduler.in "#{expiry}s" do
|
67
|
-
@redis.srem "cache_keys", key
|
75
|
+
@redis.srem "cache_keys", key.to_s
|
68
76
|
end
|
69
77
|
end
|
70
78
|
end
|
@@ -74,9 +82,11 @@ module CoffeeTable
|
|
74
82
|
result
|
75
83
|
end
|
76
84
|
|
77
|
-
def expire_key(
|
78
|
-
|
79
|
-
|
85
|
+
def expire_key(key_value)
|
86
|
+
keys.map{|k| Key.parse(k)}.select{|key| key.has_element?(key_value) || key.to_s == key_value }.each do |key|
|
87
|
+
@redis.del(key.to_s)
|
88
|
+
@redis.srem "cache_keys", key.to_s
|
89
|
+
end
|
80
90
|
end
|
81
91
|
|
82
92
|
def expire_all
|
@@ -84,7 +94,7 @@ module CoffeeTable
|
|
84
94
|
end
|
85
95
|
|
86
96
|
def keys
|
87
|
-
@redis.smembers
|
97
|
+
@redis.smembers("cache_keys")
|
88
98
|
end
|
89
99
|
|
90
100
|
def expire_for(*objects)
|
@@ -99,28 +109,27 @@ module CoffeeTable
|
|
99
109
|
if perform_caching
|
100
110
|
deleted_keys = []
|
101
111
|
unless objects.count == 0
|
102
|
-
keys.each do |key|
|
112
|
+
keys.map{|k| Key.parse(k)}.each do |key|
|
103
113
|
expire = true
|
104
114
|
objects.each do |object|
|
105
|
-
mod_key = "|#{key}|"
|
106
115
|
if object.class == String || object.class == Symbol
|
107
|
-
unless
|
116
|
+
unless key.has_element?(object)
|
108
117
|
expire = false
|
109
118
|
end
|
110
119
|
elsif object.class == Class
|
111
120
|
object_type = underscore(object.to_s)
|
112
|
-
unless
|
121
|
+
unless key.has_element_type?(object_type) || key.has_element_type?(ActiveSupport::Inflector.pluralize(object_type))
|
113
122
|
expire = false
|
114
123
|
end
|
115
124
|
else
|
116
125
|
object_type = underscore(object.class.to_s)
|
117
|
-
unless
|
126
|
+
unless key.has_element?("#{object_type.to_sym}[#{object.id}]") or key.has_element?(object_type)
|
118
127
|
expire = false
|
119
128
|
end
|
120
129
|
end
|
121
130
|
end
|
122
131
|
if expire
|
123
|
-
expire_key(key)
|
132
|
+
expire_key(key.to_s)
|
124
133
|
deleted_keys << key
|
125
134
|
end
|
126
135
|
end
|
@@ -128,6 +137,8 @@ module CoffeeTable
|
|
128
137
|
deleted_keys
|
129
138
|
end
|
130
139
|
end
|
140
|
+
|
141
|
+
alias :get_cache :fetch
|
131
142
|
|
132
143
|
private
|
133
144
|
def marshal_value(value)
|
@@ -140,18 +151,9 @@ module CoffeeTable
|
|
140
151
|
end
|
141
152
|
result
|
142
153
|
end
|
143
|
-
def setup_redis
|
144
|
-
@redis = Redis.new #::Namespace.new(options[:redis_namespace], :redis => Redis.new({:server => @options[:redis_server], :port => @options[:redis_port]}))
|
145
|
-
end
|
146
154
|
def object_valid?(o)
|
147
155
|
o.respond_to?(:id) || o.class == Class
|
148
156
|
end
|
149
|
-
|
150
|
-
if o.class == Class
|
151
|
-
"#{ActiveSupport::Inflector.pluralize(underscore(o.to_s))}"
|
152
|
-
else
|
153
|
-
"#{underscore(o.class.to_s)}[#{o.id}]"
|
154
|
-
end
|
155
|
-
end
|
157
|
+
|
156
158
|
end
|
157
159
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "coffee_table/utility"
|
2
|
+
|
3
|
+
module CoffeeTable
|
4
|
+
class Key
|
5
|
+
include CoffeeTable::Utility
|
6
|
+
|
7
|
+
def self.parse(string)
|
8
|
+
elements = string.split("|").map{|e| decode_element(e) }
|
9
|
+
key = Key.new(elements[0], elements[1])
|
10
|
+
key.elements = elements[2..-1]
|
11
|
+
key
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(name, block_key, *objects)
|
15
|
+
@name = name
|
16
|
+
@block_key = block_key
|
17
|
+
@elements = objects.flatten.map{|o| key_for_object(o)}
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_element?(element)
|
21
|
+
matches?(element.to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_element_type?(element)
|
25
|
+
matches?(element.to_s + "[", :match => :start)
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
@name
|
30
|
+
end
|
31
|
+
|
32
|
+
def code_hash
|
33
|
+
@block_key
|
34
|
+
end
|
35
|
+
|
36
|
+
def elements
|
37
|
+
@elements
|
38
|
+
end
|
39
|
+
|
40
|
+
def elements=(elements)
|
41
|
+
@elements = elements
|
42
|
+
end
|
43
|
+
|
44
|
+
def <=>(o)
|
45
|
+
self.to_s <=> o.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
[encode_element(@name), encode_element(@block_key), @elements.map{|e| encode_element(e) }].flatten.join("|")
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def matches?(fragment, options={})
|
55
|
+
if options[:match] == :start
|
56
|
+
@name == fragment || !@elements.select{|e| e =~ /^#{Regexp.escape(fragment)}/ }.empty?
|
57
|
+
else
|
58
|
+
@name == fragment || @elements.include?(fragment)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def encode_element(element)
|
63
|
+
element.to_s.gsub("&", "&").gsub("|", "|")
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.decode_element(element)
|
67
|
+
element.to_s.gsub("|", "|").gsub("&", "&")
|
68
|
+
end
|
69
|
+
|
70
|
+
def key_for_object(o)
|
71
|
+
if o.class == Class
|
72
|
+
"#{ActiveSupport::Inflector.pluralize(underscore(o.to_s))}"
|
73
|
+
elsif o.kind_of?(String) || o.kind_of?(Symbol)
|
74
|
+
"#{underscore(o.to_s)}"
|
75
|
+
else
|
76
|
+
"#{underscore(o.class.to_s)}[#{o.id}]"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
File without changes
|
data/lib/coffee_table/version.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
|
-
describe CoffeeTable do
|
3
|
+
describe CoffeeTable::Cache do
|
4
4
|
|
5
5
|
before(:each) do
|
6
6
|
@coffee_table = CoffeeTable::Cache.new
|
7
7
|
end
|
8
8
|
|
9
9
|
specify { CoffeeTable::Cache.should respond_to :new}
|
10
|
-
specify { @coffee_table.should respond_to :
|
10
|
+
specify { @coffee_table.should respond_to :fetch}
|
11
11
|
specify { @coffee_table.should respond_to :expire_key}
|
12
12
|
specify { @coffee_table.should respond_to :expire_all}
|
13
13
|
specify { @coffee_table.should respond_to :keys}
|
@@ -22,69 +22,70 @@ describe CoffeeTable do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
describe "
|
25
|
+
describe "fetch" do
|
26
26
|
it "should raise an exception when block not given" do
|
27
|
-
lambda{@coffee_table.
|
27
|
+
lambda{@coffee_table.fetch("asdf")}.should raise_exception CoffeeTableBlockMissingError
|
28
28
|
end
|
29
29
|
it "should execute block when cache value not available" do
|
30
|
-
result = @coffee_table.
|
30
|
+
result = @coffee_table.fetch("asdf") do
|
31
31
|
"this is a value"
|
32
32
|
end
|
33
33
|
|
34
34
|
result.should == "this is a value"
|
35
35
|
end
|
36
36
|
it "should return cached value when cache available" do
|
37
|
-
|
38
|
-
|
37
|
+
value = "this is a value"
|
38
|
+
@coffee_table.fetch("asdf") do
|
39
|
+
value
|
39
40
|
end
|
40
|
-
|
41
|
-
|
41
|
+
value = "this is a changed value"
|
42
|
+
result = @coffee_table.fetch("asdf") do
|
43
|
+
value
|
42
44
|
end
|
43
45
|
|
44
46
|
result.should == "this is a value"
|
45
47
|
|
46
|
-
end
|
47
|
-
it "should execute block when store not available" do
|
48
|
-
@coffee_table.get_cache(:test_key) do
|
49
|
-
TESTVAR = "testvar"
|
50
|
-
"this is a value"
|
51
|
-
end
|
52
|
-
@coffee_table.get_cache(:test_key) do
|
53
|
-
TESTVAR = "testvar2"
|
54
|
-
"this is a value"
|
55
|
-
end
|
56
|
-
|
57
|
-
TESTVAR.should == "testvar"
|
58
|
-
|
59
48
|
end
|
60
49
|
|
61
50
|
context "keys" do
|
62
51
|
it "should create a key with just the initial key" do
|
63
|
-
|
52
|
+
md5 = md5_block do
|
64
53
|
"this is a changed value"
|
65
54
|
end
|
66
|
-
@coffee_table.
|
55
|
+
result = @coffee_table.fetch(:test_key) do
|
56
|
+
"this is a changed value"
|
57
|
+
end
|
58
|
+
@coffee_table.keys.should == ["test_key|#{md5}"]
|
67
59
|
end
|
68
60
|
|
69
61
|
it "should create key from class" do
|
70
|
-
|
62
|
+
md5 = md5_block do
|
71
63
|
"this is a changed value"
|
72
64
|
end
|
73
|
-
@coffee_table.
|
65
|
+
result = @coffee_table.fetch(:test_key, SampleClass) do
|
66
|
+
"this is a changed value"
|
67
|
+
end
|
68
|
+
@coffee_table.keys.should == ["test_key|#{md5}|sample_classes"]
|
74
69
|
end
|
75
70
|
|
76
71
|
it "should use class name for keys" do
|
77
|
-
|
72
|
+
md5 = md5_block do
|
73
|
+
"this is a changed value"
|
74
|
+
end
|
75
|
+
result = @coffee_table.fetch(:test_key, SampleClass.new(2)) do
|
78
76
|
"this is a changed value"
|
79
77
|
end
|
80
|
-
@coffee_table.keys.should == ["test_key|sample_class[2]"]
|
78
|
+
@coffee_table.keys.should == ["test_key|#{md5}|sample_class[2]"]
|
81
79
|
end
|
82
80
|
|
83
81
|
it "should use id from class in key" do
|
84
|
-
|
82
|
+
md5 = md5_block do
|
83
|
+
"this is a changed value"
|
84
|
+
end
|
85
|
+
result = @coffee_table.fetch(:test_key, SampleClass.new(2)) do
|
85
86
|
"this is a changed value"
|
86
87
|
end
|
87
|
-
@coffee_table.keys.should == ["test_key|sample_class[2]"]
|
88
|
+
@coffee_table.keys.should == ["test_key|#{md5}|sample_class[2]"]
|
88
89
|
end
|
89
90
|
|
90
91
|
end
|
@@ -93,18 +94,21 @@ describe CoffeeTable do
|
|
93
94
|
context "with related objects" do
|
94
95
|
it "should create a key from the id's of the related objects" do
|
95
96
|
test_object = SampleClass.new(9938)
|
96
|
-
|
97
|
+
md5 = md5_block do
|
98
|
+
"this is a changed value"
|
99
|
+
end
|
100
|
+
result = @coffee_table.fetch(:test_key, test_object) do
|
97
101
|
"this is a changed value"
|
98
102
|
end
|
99
103
|
|
100
|
-
@coffee_table.keys.should include "test_key|sample_class[9938]"
|
104
|
+
@coffee_table.keys.should include "test_key|#{md5}|sample_class[9938]"
|
101
105
|
|
102
106
|
end
|
103
107
|
it "should raise an exception if a related object does not respond_to id" do
|
104
108
|
test_object = SampleClassWithoutId.new
|
105
109
|
|
106
110
|
lambda {
|
107
|
-
result = @coffee_table.
|
111
|
+
result = @coffee_table.fetch(:test_key, test_object) do
|
108
112
|
"this is a changed value"
|
109
113
|
end
|
110
114
|
}.should raise_exception CoffeeTableInvalidObjectError, "Objects passed in must have an id method or be a class"
|
@@ -112,18 +116,21 @@ describe CoffeeTable do
|
|
112
116
|
end
|
113
117
|
|
114
118
|
it "should create a universal key if the objects passed in are an uninitialised class" do
|
119
|
+
md5 = md5_block do
|
120
|
+
"this is a changed value"
|
121
|
+
end
|
115
122
|
|
116
|
-
result = @coffee_table.
|
123
|
+
result = @coffee_table.fetch(:test_key, SampleClass) do
|
117
124
|
"this is a changed value"
|
118
125
|
end
|
119
126
|
|
120
|
-
@coffee_table.keys.should include "test_key|sample_classes"
|
127
|
+
@coffee_table.keys.should include "test_key|#{md5}|sample_classes"
|
121
128
|
end
|
122
129
|
|
123
130
|
end
|
124
131
|
context "with expiry" do
|
125
132
|
it "keys should update when cache expires" do
|
126
|
-
@coffee_table.
|
133
|
+
@coffee_table.fetch(:test_key, :expiry => 0.2) do
|
127
134
|
"object1"
|
128
135
|
end
|
129
136
|
@coffee_table.keys.count.should == 1
|
@@ -131,21 +138,23 @@ describe CoffeeTable do
|
|
131
138
|
@coffee_table.keys.count.should == 0
|
132
139
|
end
|
133
140
|
it "should not execute block during cache period" do
|
134
|
-
|
135
|
-
|
141
|
+
value = 'this is a value'
|
142
|
+
@coffee_table.fetch("asdf", :expiry => 1) do
|
143
|
+
value
|
136
144
|
end
|
137
|
-
|
138
|
-
|
145
|
+
value = 'this is a changed value'
|
146
|
+
result = @coffee_table.fetch("asdf") do
|
147
|
+
value
|
139
148
|
end
|
140
149
|
result.should == "this is a value"
|
141
150
|
|
142
151
|
end
|
143
152
|
it "should execute block and return value when cache has expired" do
|
144
|
-
@coffee_table.
|
153
|
+
@coffee_table.fetch("asdf", :expiry => 1) do
|
145
154
|
"this is a value"
|
146
155
|
end
|
147
156
|
sleep 2
|
148
|
-
result = @coffee_table.
|
157
|
+
result = @coffee_table.fetch("asdf") do
|
149
158
|
"this is a changed value"
|
150
159
|
end
|
151
160
|
result.should == "this is a changed value"
|
@@ -154,38 +163,129 @@ describe CoffeeTable do
|
|
154
163
|
end
|
155
164
|
|
156
165
|
describe "expire_key" do
|
166
|
+
|
167
|
+
before(:each) do
|
168
|
+
@proc_md51 = md5_block do
|
169
|
+
"object1"
|
170
|
+
end
|
171
|
+
@proc_md52 = md5_block do
|
172
|
+
"object2"
|
173
|
+
end
|
174
|
+
@proc_md53 = md5_block do
|
175
|
+
"object3"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
157
179
|
it "should expire the specified key" do
|
158
|
-
@coffee_table.
|
180
|
+
@coffee_table.fetch(:first_key) do
|
159
181
|
"object1"
|
160
182
|
end
|
161
|
-
@coffee_table.
|
183
|
+
@coffee_table.fetch(:second_key) do
|
162
184
|
"object2"
|
163
185
|
end
|
164
|
-
@coffee_table.
|
186
|
+
@coffee_table.fetch(:third_key) do
|
165
187
|
"object3"
|
166
188
|
end
|
167
189
|
|
168
|
-
@coffee_table.keys.sort.should == ["first_key", "second_key", "third_key"].sort
|
190
|
+
@coffee_table.keys.sort.should == ["first_key|#{@proc_md51}", "second_key|#{@proc_md52}", "third_key|#{@proc_md53}"].sort
|
169
191
|
@coffee_table.expire_key("second_key")
|
170
|
-
@coffee_table.keys.sort.should == ["first_key", "third_key"].sort
|
192
|
+
@coffee_table.keys.sort.should == ["first_key|#{@proc_md51}", "third_key|#{@proc_md53}"].sort
|
171
193
|
|
172
194
|
end
|
173
195
|
it "should not expire anything if no matches" do
|
174
|
-
@
|
196
|
+
@proc_md51 = md5_block do
|
175
197
|
"object1"
|
176
198
|
end
|
177
|
-
@
|
199
|
+
@proc_md52 = md5_block do
|
178
200
|
"object2"
|
179
201
|
end
|
180
|
-
@
|
202
|
+
@proc_md53 = md5_block do
|
181
203
|
"object3"
|
182
204
|
end
|
183
205
|
|
184
|
-
@coffee_table.
|
206
|
+
@coffee_table.fetch(:first_key) do
|
207
|
+
"object1"
|
208
|
+
end
|
209
|
+
@coffee_table.fetch(:second_key) do
|
210
|
+
"object2"
|
211
|
+
end
|
212
|
+
@coffee_table.fetch(:third_key) do
|
213
|
+
"object3"
|
214
|
+
end
|
215
|
+
|
216
|
+
@coffee_table.keys.sort.should == ["first_key|#{@proc_md51}", "second_key|#{@proc_md52}", "third_key|#{@proc_md53}"].sort
|
185
217
|
@coffee_table.expire_key("fourth_key")
|
186
|
-
@coffee_table.keys.sort.should == ["first_key", "second_key", "third_key"].sort
|
218
|
+
@coffee_table.keys.sort.should == ["first_key|#{@proc_md51}", "second_key|#{@proc_md52}", "third_key|#{@proc_md53}"].sort
|
187
219
|
|
188
220
|
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
context "monitoring code changes in block" do
|
225
|
+
context "changed block" do
|
226
|
+
|
227
|
+
it "should invalidate cache when block has changed" do
|
228
|
+
@coffee_table.fetch(:test_key) do
|
229
|
+
"object1"
|
230
|
+
end
|
231
|
+
|
232
|
+
result = @coffee_table.fetch(:test_key) do
|
233
|
+
"object2"
|
234
|
+
end
|
235
|
+
|
236
|
+
result.should == "object2"
|
237
|
+
end
|
238
|
+
|
239
|
+
it "should not invalidate block when block has not changed" do
|
240
|
+
object = "object1"
|
241
|
+
@coffee_table.fetch(:test_key) do
|
242
|
+
object
|
243
|
+
end
|
244
|
+
|
245
|
+
object = "object2"
|
246
|
+
result = @coffee_table.fetch(:test_key) do
|
247
|
+
object
|
248
|
+
end
|
249
|
+
|
250
|
+
result.should == "object1"
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should not be affected by whitespace only changes" do
|
254
|
+
object = "object1"
|
255
|
+
@coffee_table.fetch(:test_key) do
|
256
|
+
object
|
257
|
+
end
|
258
|
+
|
259
|
+
object = "object2"
|
260
|
+
result = @coffee_table.fetch(:test_key) do
|
261
|
+
|
262
|
+
object
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
result.should == "object1"
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
context "ignoring code changes" do
|
272
|
+
|
273
|
+
before(:each) do
|
274
|
+
@coffee_table = CoffeeTable::Cache.new(:ignore_code_changes => true)
|
275
|
+
end
|
276
|
+
|
277
|
+
it "should not invalidate cache when block has changed" do
|
278
|
+
@coffee_table.fetch(:test_key) do
|
279
|
+
"object1"
|
280
|
+
end
|
281
|
+
|
282
|
+
result = @coffee_table.fetch(:test_key) do
|
283
|
+
"object2"
|
284
|
+
end
|
285
|
+
|
286
|
+
result.should == "object1"
|
287
|
+
end
|
288
|
+
end
|
189
289
|
end
|
190
290
|
|
191
291
|
describe "expire_all" do
|
@@ -195,13 +295,13 @@ describe CoffeeTable do
|
|
195
295
|
object2 = [SampleClass.new(4), SampleClass.new(2), SampleClass.new(5)]
|
196
296
|
object3 = [SampleClass.new(7), SampleClass.new(2), SampleClass.new(8)]
|
197
297
|
|
198
|
-
@coffee_table.
|
298
|
+
@coffee_table.fetch(:first_key) do
|
199
299
|
"object1"
|
200
300
|
end
|
201
|
-
@coffee_table.
|
301
|
+
@coffee_table.fetch(:second_key) do
|
202
302
|
"object2"
|
203
303
|
end
|
204
|
-
@coffee_table.
|
304
|
+
@coffee_table.fetch(:third_key) do
|
205
305
|
"object3"
|
206
306
|
end
|
207
307
|
end
|
@@ -211,7 +311,7 @@ describe CoffeeTable do
|
|
211
311
|
@coffee_table.expire_all
|
212
312
|
@coffee_table.keys.count.should == 0
|
213
313
|
|
214
|
-
result = @coffee_table.
|
314
|
+
result = @coffee_table.fetch(:first_key) do
|
215
315
|
"changed value"
|
216
316
|
end
|
217
317
|
|
@@ -226,6 +326,16 @@ describe CoffeeTable do
|
|
226
326
|
@object2 = [SampleClass.new(4), SampleClass.new(2), SampleClass.new(5)]
|
227
327
|
@object3 = [SampleClass.new(7), SampleClass.new(2), SampleClass.new(8)]
|
228
328
|
|
329
|
+
@proc_md51 = md5_block do
|
330
|
+
"object1"
|
331
|
+
end
|
332
|
+
@proc_md52 = md5_block do
|
333
|
+
"object2"
|
334
|
+
end
|
335
|
+
@proc_md53 = md5_block do
|
336
|
+
"object3"
|
337
|
+
end
|
338
|
+
|
229
339
|
end
|
230
340
|
|
231
341
|
it "should return an array of string" do
|
@@ -233,34 +343,34 @@ describe CoffeeTable do
|
|
233
343
|
@coffee_table.keys.map{|key| key.should be_an_instance_of String}
|
234
344
|
end
|
235
345
|
it "should return key created without objects" do
|
236
|
-
@coffee_table.
|
346
|
+
@coffee_table.fetch(:first_key) do
|
237
347
|
"object1"
|
238
348
|
end
|
239
|
-
@coffee_table.
|
349
|
+
@coffee_table.fetch(:second_key) do
|
240
350
|
"object2"
|
241
351
|
end
|
242
|
-
@coffee_table.
|
352
|
+
@coffee_table.fetch(:third_key) do
|
243
353
|
"object3"
|
244
354
|
end
|
245
355
|
|
246
|
-
@coffee_table.keys.sort.should == ["first_key",
|
247
|
-
"second_key",
|
248
|
-
"third_key"].sort
|
356
|
+
@coffee_table.keys.sort.should == ["first_key|#{@proc_md51}",
|
357
|
+
"second_key|#{@proc_md52}",
|
358
|
+
"third_key|#{@proc_md53}"].sort
|
249
359
|
|
250
360
|
end
|
251
361
|
it "should return key created with objects and ids" do
|
252
|
-
@coffee_table.
|
362
|
+
@coffee_table.fetch(:first_key, @object1) do
|
253
363
|
"object1"
|
254
364
|
end
|
255
|
-
@coffee_table.
|
365
|
+
@coffee_table.fetch(:second_key, @object2) do
|
256
366
|
"object2"
|
257
367
|
end
|
258
|
-
@coffee_table.
|
368
|
+
@coffee_table.fetch(:third_key, @object3) do
|
259
369
|
"object3"
|
260
370
|
end
|
261
|
-
@coffee_table.keys.sort.should == ["first_key|sample_class[1]|sample_class[2]|sample_class[3]",
|
262
|
-
"second_key|sample_class[4]|sample_class[2]|sample_class[5]",
|
263
|
-
"third_key|sample_class[7]|sample_class[2]|sample_class[8]"].sort
|
371
|
+
@coffee_table.keys.sort.should == ["first_key|#{@proc_md51}|sample_class[1]|sample_class[2]|sample_class[3]",
|
372
|
+
"second_key|#{@proc_md52}|sample_class[4]|sample_class[2]|sample_class[5]",
|
373
|
+
"third_key|#{@proc_md53}|sample_class[7]|sample_class[2]|sample_class[8]"].sort
|
264
374
|
end
|
265
375
|
|
266
376
|
end
|
@@ -271,13 +381,13 @@ describe CoffeeTable do
|
|
271
381
|
object2 = [SampleClass.new(4), SampleClass.new(2), SampleClass.new(5)]
|
272
382
|
object3 = [SampleClass.new(7), SampleClass.new(2), SampleClass.new(8)]
|
273
383
|
|
274
|
-
@coffee_table.
|
384
|
+
@coffee_table.fetch(:first_key, object1) do
|
275
385
|
"object1"
|
276
386
|
end
|
277
|
-
@coffee_table.
|
387
|
+
@coffee_table.fetch(:second_key, object2) do
|
278
388
|
"object2"
|
279
389
|
end
|
280
|
-
@coffee_table.
|
390
|
+
@coffee_table.fetch(:third_key, object3) do
|
281
391
|
"object3"
|
282
392
|
end
|
283
393
|
end
|
@@ -318,7 +428,7 @@ describe CoffeeTable do
|
|
318
428
|
end
|
319
429
|
|
320
430
|
it "should expire all keys relating to a class if uninitialised class is passed in" do
|
321
|
-
@coffee_table.
|
431
|
+
@coffee_table.fetch(:fourth_key) do
|
322
432
|
"object4"
|
323
433
|
end
|
324
434
|
@coffee_table.keys.count.should == 4
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe CoffeeTable::Key do
|
4
|
+
|
5
|
+
context "has correct methods" do
|
6
|
+
it "should have a parse class method" do
|
7
|
+
CoffeeTable::Key.should respond_to :parse
|
8
|
+
end
|
9
|
+
it "should have a has_element? instance method" do
|
10
|
+
CoffeeTable::Key.new("name", "key").should respond_to :has_element?
|
11
|
+
end
|
12
|
+
it "should have a has_element_type? instance method" do
|
13
|
+
CoffeeTable::Key.new("name", "key").should respond_to :has_element_type?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "parsing a string" do
|
18
|
+
it "should parse a string into its elements" do
|
19
|
+
key = CoffeeTable::Key.parse("test|asdf|sample_class")
|
20
|
+
|
21
|
+
|
22
|
+
key.elements.count.should == 1
|
23
|
+
key.name.should == "test"
|
24
|
+
key.code_hash.should == "asdf"
|
25
|
+
key.elements[0].should == "sample_class"
|
26
|
+
|
27
|
+
end
|
28
|
+
it "should decode encoded elements" do
|
29
|
+
key = CoffeeTable::Key.parse("te|s&t|asdf|s&|sample_|s&class")
|
30
|
+
|
31
|
+
|
32
|
+
key.elements.count.should == 1
|
33
|
+
key.name.should == "te|s&t"
|
34
|
+
key.code_hash.should == "asdf|s&"
|
35
|
+
key.elements[0].should == "sample_|s&class"
|
36
|
+
end
|
37
|
+
it "should encode the key data" do
|
38
|
+
|
39
|
+
key = CoffeeTable::Key.new("te|s&t", "asdf|s&", "sample_|s&class")
|
40
|
+
|
41
|
+
key.name.should == "te|s&t"
|
42
|
+
key.code_hash.should == "asdf|s&"
|
43
|
+
key.elements[0].should == "sample_|s&class"
|
44
|
+
|
45
|
+
key.to_s.should == "te|s&t|asdf|s&|sample_|s&class"
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "matching keys" do
|
51
|
+
it "should match a key on its name" do
|
52
|
+
key = CoffeeTable::Key.new("name", "key", "value", ["value1", "value2"])
|
53
|
+
key.has_element?("name").should be_true
|
54
|
+
key.has_element?("key").should be_false
|
55
|
+
end
|
56
|
+
it "should match a key on its data" do
|
57
|
+
key = CoffeeTable::Key.new("name", "key", "value", ["value1", "value2"])
|
58
|
+
key.has_element?("key").should be_false
|
59
|
+
key.has_element?("value").should be_true
|
60
|
+
key.has_element?("value1").should be_true
|
61
|
+
key.has_element?("value2").should be_true
|
62
|
+
end
|
63
|
+
it "should match a key on a class type" do
|
64
|
+
key = CoffeeTable::Key.new("name", "key", "sample_class[3]", ["value1", "value2"])
|
65
|
+
key.has_element?("key").should be_false
|
66
|
+
key.has_element_type?("sample_class").should be_true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'digest/md5'
|
2
5
|
require 'spork'
|
3
6
|
require 'mock_redis'
|
4
7
|
require File.expand_path(File.dirname(__FILE__) + '/../../coffee_table/spec/lib/sample_class')
|
@@ -6,6 +9,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../coffee_table/spec/lib/
|
|
6
9
|
|
7
10
|
Spork.prefork do
|
8
11
|
require File.expand_path(File.dirname(__FILE__) + '/../../coffee_table/lib/coffee_table.rb')
|
12
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../coffee_table/lib/coffee_table/key.rb')
|
9
13
|
require File.expand_path(File.dirname(__FILE__) + '/../../coffee_table/lib/coffee_table/coffee_table_block_missing_error.rb')
|
10
14
|
require File.expand_path(File.dirname(__FILE__) + '/../../coffee_table/lib/coffee_table/coffee_table_invalid_object_error.rb')
|
11
15
|
end
|
@@ -35,3 +39,6 @@ def load_binary_sample(filename)
|
|
35
39
|
File.open(File.dirname(__FILE__) + "/samples/" + filename, 'rb')
|
36
40
|
end
|
37
41
|
|
42
|
+
def md5_block(&block)
|
43
|
+
Digest::MD5.hexdigest(block.to_source)
|
44
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coffee_table
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-02-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement: &
|
16
|
+
requirement: &70278554140820 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70278554140820
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis
|
27
|
-
requirement: &
|
27
|
+
requirement: &70278554140260 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70278554140260
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rufus-scheduler
|
38
|
-
requirement: &
|
38
|
+
requirement: &70278554139760 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70278554139760
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: activesupport
|
49
|
-
requirement: &
|
49
|
+
requirement: &70278554139200 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,18 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70278554139200
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: sourcify
|
60
|
+
requirement: &70278554138660 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70278554138660
|
58
69
|
description: rails cache gem to fragment cache with smart cache key management
|
59
70
|
email:
|
60
71
|
- stewart@theizone.co.uk
|
@@ -70,13 +81,16 @@ files:
|
|
70
81
|
- Gemfile.lock
|
71
82
|
- README.textile
|
72
83
|
- Rakefile
|
84
|
+
- changelog.txt
|
73
85
|
- coffee_table.gemspec
|
74
86
|
- lib/coffee_table.rb
|
75
87
|
- lib/coffee_table/coffee_table_block_missing_error.rb
|
76
88
|
- lib/coffee_table/coffee_table_invalid_object_error.rb
|
89
|
+
- lib/coffee_table/key.rb
|
90
|
+
- lib/coffee_table/utility.rb
|
77
91
|
- lib/coffee_table/version.rb
|
78
|
-
- lib/utility.rb
|
79
92
|
- spec/lib/coffee_table_spec.rb
|
93
|
+
- spec/lib/key_spec.rb
|
80
94
|
- spec/lib/sample_class.rb
|
81
95
|
- spec/lib/sample_class_without_id.rb
|
82
96
|
- spec/spec_helper.rb
|
@@ -106,6 +120,7 @@ specification_version: 3
|
|
106
120
|
summary: Gem to manage cache stored in redis
|
107
121
|
test_files:
|
108
122
|
- spec/lib/coffee_table_spec.rb
|
123
|
+
- spec/lib/key_spec.rb
|
109
124
|
- spec/lib/sample_class.rb
|
110
125
|
- spec/lib/sample_class_without_id.rb
|
111
126
|
- spec/spec_helper.rb
|