expiration-date 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +6 -1
- data/Manifest.txt +2 -1
- data/{README.txt → README.rdoc} +39 -35
- data/Rakefile +3 -1
- data/lib/expiration-date.rb +96 -47
- data/spec/expiration-date_spec.rb +73 -18
- metadata +7 -6
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
data/{README.txt → README.rdoc}
RENAMED
@@ -5,36 +5,17 @@
|
|
5
5
|
|
6
6
|
== DESCRIPTION:
|
7
7
|
|
8
|
-
|
9
|
-
green sticker screaming for all to hear "THIS BACON ONLY COSTS ONE DOLLAR!!!".
|
10
|
-
Screaming bacon always intrigues me, so I grabbed the nearest store manager. He
|
11
|
-
works at the coffee shop next door, but when I told him about the screaming
|
12
|
-
bacon he had to come and see for himself.
|
13
|
-
|
14
|
-
"Oh, it's a manager's special" he told me. "When products get too old the
|
15
|
-
manager will reduce the price so they sell more quickly. This allows new
|
16
|
-
products to be put on the shelves, and the store can make some money instead of
|
17
|
-
throwing out the expired products".
|
18
|
-
|
19
|
-
I stared at him blankly. He wandered back to the coffee shop and had a latte.
|
20
|
-
|
21
|
-
I continued to stand there thinking about expiring products and how Ruby could
|
22
|
-
benefit from neon green stickers and stale bacon. Eventually the grocery
|
23
|
-
store manager came by and asked me if everything was okay. I grabbed him by the
|
24
|
-
collar, pointed at the bacon and yelled "DO YOU KNOW WHAT THIS MEANS!?!?". I
|
25
|
-
ran from the store, grabbed my laptop, and whipped up this little gem.
|
8
|
+
Really simple caching by attaching an expiration date to attributes.
|
26
9
|
|
27
|
-
|
10
|
+
The ExpirationDate module adss two methods to a class -- expiring_attr and
|
11
|
+
expiring_class_attr. These two methods are used to declare attributes in the
|
12
|
+
instance and in the class, respectively, that will expire after some period of
|
13
|
+
seconds have elapsed. The attribute is re-initialized from the given block
|
14
|
+
after it has expired. This is a very simple form of caching.
|
28
15
|
|
29
|
-
Now ruby can expire it's bacon, too, just like the grocery store, and make room
|
30
|
-
for more bacon from the delivery truck.
|
31
16
|
|
32
17
|
== SYNOPSIS:
|
33
18
|
|
34
|
-
The ExpirationDate module adds the "expiring_attr" method to a class. This
|
35
|
-
method is used to define an attribute that will expire after some period of
|
36
|
-
seconds have elapsed.
|
37
|
-
|
38
19
|
A simple example demonstrating how the block gets called after the expiration
|
39
20
|
time is passed.
|
40
21
|
|
@@ -45,14 +26,9 @@ time is passed.
|
|
45
26
|
|
46
27
|
a = A.new
|
47
28
|
a.foo #=> 'foo'
|
48
|
-
a.foo.object_id #=> 123456
|
49
|
-
a.foo.object_id #=> 123456
|
50
|
-
|
51
|
-
sleep 61
|
52
|
-
a.foo.object_id #=> 654321
|
53
|
-
|
54
29
|
a.foo = 'bar'
|
55
30
|
a.foo #=> 'bar'
|
31
|
+
|
56
32
|
sleep 61
|
57
33
|
a.foo #=> 'foo'
|
58
34
|
|
@@ -62,13 +38,15 @@ database every five minutes. This assumes you have the 'activesupport' and
|
|
62
38
|
|
63
39
|
class MyModel < ::ActiveRecord::Base
|
64
40
|
include ExpirationDate
|
65
|
-
|
41
|
+
expiring_class_attr( :costly_data, 5.minutes ) {
|
66
42
|
models = MyModel.find( :all, :conditions => ['costly query conditions'] )
|
67
43
|
result = models.map {|m| # costly operations here}
|
68
44
|
result
|
69
45
|
}
|
70
46
|
end
|
71
47
|
|
48
|
+
MyModel.costly_data #=> result
|
49
|
+
|
72
50
|
Attributes can be expired manually, and the time it takes them to expire can be
|
73
51
|
modified as well.
|
74
52
|
|
@@ -83,11 +61,11 @@ modified as well.
|
|
83
61
|
sleep 60
|
84
62
|
demo.bar #=> 60 seconds ago
|
85
63
|
|
86
|
-
demo.
|
64
|
+
demo.expire_bar_now
|
87
65
|
demo.bar #=> now
|
88
66
|
|
89
|
-
demo.
|
90
|
-
demo.
|
67
|
+
demo.alter_bar_age( 10 )
|
68
|
+
demo.expire_bar_now
|
91
69
|
demo.bar #=> now
|
92
70
|
sleep 11
|
93
71
|
demo.bar #=> now
|
@@ -100,6 +78,32 @@ This is a pure ruby library. There are no requirements for using this code.
|
|
100
78
|
|
101
79
|
sudo gem install expiration-date
|
102
80
|
|
81
|
+
== FUN STORY:
|
82
|
+
|
83
|
+
Wandering the grocery aisle the other day I saw a package of bacon with a neon
|
84
|
+
green sticker screaming for all to hear "THIS BACON ONLY COSTS ONE DOLLAR!!!".
|
85
|
+
Screaming bacon always intrigues me, so I grabbed the nearest store manager. He
|
86
|
+
works at the coffee shop next door, but when I told him about the screaming
|
87
|
+
bacon he had to come and see for himself.
|
88
|
+
|
89
|
+
"Oh, it's a manager's special" he told me. "When products get too old the
|
90
|
+
manager will reduce the price so they sell more quickly. This allows new
|
91
|
+
products to be put on the shelves, and the store can make some money instead of
|
92
|
+
throwing out the expired products".
|
93
|
+
|
94
|
+
I stared at him blankly. He wandered back to the coffee shop and had a latte.
|
95
|
+
|
96
|
+
I continued to stand there thinking about expiring products and how Ruby could
|
97
|
+
benefit from neon green stickers and stale bacon. Eventually the grocery
|
98
|
+
store manager came by and asked me if everything was okay. I grabbed him by the
|
99
|
+
collar, pointed at the bacon and yelled "DO YOU KNOW WHAT THIS MEANS!?!?". I
|
100
|
+
ran from the store, grabbed my laptop, and whipped up this little gem.
|
101
|
+
|
102
|
+
EXPIRATION DATE (now with more neon green).
|
103
|
+
|
104
|
+
Now ruby can expire it's bacon, too, just like the grocery store, and make room
|
105
|
+
for more bacon from the delivery truck.
|
106
|
+
|
103
107
|
== LICENSE:
|
104
108
|
|
105
109
|
The MIT License
|
data/Rakefile
CHANGED
@@ -14,7 +14,9 @@ PROJ.url = 'http://codeforpeople.rubyforge.org/expiration-date'
|
|
14
14
|
PROJ.rubyforge.name = 'codeforpeople'
|
15
15
|
PROJ.rdoc.remote_dir = 'expiration-date'
|
16
16
|
PROJ.version = ExpirationDate::VERSION
|
17
|
-
PROJ.release_name = '
|
17
|
+
PROJ.release_name = 'Screaming Bacon'
|
18
|
+
PROJ.readme_file = 'README.rdoc'
|
19
|
+
PROJ.rdoc.include << 'README.rdoc'
|
18
20
|
|
19
21
|
PROJ.spec.opts << '--color'
|
20
22
|
|
data/lib/expiration-date.rb
CHANGED
@@ -4,7 +4,7 @@ require 'thread'
|
|
4
4
|
module ExpirationDate
|
5
5
|
|
6
6
|
# :stopdoc:
|
7
|
-
VERSION = '1.0
|
7
|
+
VERSION = '1.1.0'
|
8
8
|
ExpirationLabel = Struct.new(:mutex, :age, :expires_on)
|
9
9
|
# :startdoc:
|
10
10
|
|
@@ -28,7 +28,9 @@ module ExpirationDate
|
|
28
28
|
# Declares a new instance attribute that will expire after _age_ seconds
|
29
29
|
# and be replaced by the results of running the _block_. The block is
|
30
30
|
# lazily evaluated when the attribute is accessed after the expiration
|
31
|
-
# time.
|
31
|
+
# time. The block is evaluated in the context of the instance (as
|
32
|
+
# opposed to being evaluated in the context of the class where the block
|
33
|
+
# is declared).
|
32
34
|
#
|
33
35
|
# Obviously this scheme will only work if the attribute is only accessed
|
34
36
|
# using the setter and getter methods defined by this function.
|
@@ -39,9 +41,11 @@ module ExpirationDate
|
|
39
41
|
# end
|
40
42
|
#
|
41
43
|
# a = A.new
|
42
|
-
# a.foo
|
44
|
+
# a.foo #=> 'foo'
|
45
|
+
# a.foo = 'bar'
|
46
|
+
# a.foo #=> 'bar'
|
43
47
|
# sleep 61
|
44
|
-
# a.foo
|
48
|
+
# a.foo #=> 'foo'
|
45
49
|
#
|
46
50
|
def expiring_attr( name, age, &block )
|
47
51
|
raise ArgumentError, "a block must be given" if block.nil?
|
@@ -53,7 +57,7 @@ module ExpirationDate
|
|
53
57
|
self.class_eval <<-CODE, __FILE__, __LINE__
|
54
58
|
def #{name}
|
55
59
|
now = Time.now
|
56
|
-
label =
|
60
|
+
label = _expiration_labels[#{name.inspect}]
|
57
61
|
if label.expires_on.nil? || now >= label.expires_on
|
58
62
|
label.mutex.synchronize {
|
59
63
|
break unless label.expires_on.nil? || now >= label.expires_on
|
@@ -68,7 +72,7 @@ module ExpirationDate
|
|
68
72
|
|
69
73
|
def #{name}=( val )
|
70
74
|
now = Time.now
|
71
|
-
label =
|
75
|
+
label = _expiration_labels[#{name.inspect}]
|
72
76
|
label.mutex.synchronize {
|
73
77
|
@#{name} = val
|
74
78
|
label.age ||= #{age}
|
@@ -76,6 +80,83 @@ module ExpirationDate
|
|
76
80
|
}
|
77
81
|
@#{name}
|
78
82
|
end
|
83
|
+
|
84
|
+
def expire_#{name}_now
|
85
|
+
label = _expiration_labels[#{name.inspect}]
|
86
|
+
label.mutex.synchronize {label.expires_on = Time.now - 1}
|
87
|
+
@#{name}
|
88
|
+
end
|
89
|
+
|
90
|
+
def alter_#{name}_age( age )
|
91
|
+
label = _expiration_labels[#{name.inspect}]
|
92
|
+
label.mutex.synchronize {label.age = Float(age)}
|
93
|
+
end
|
94
|
+
CODE
|
95
|
+
end
|
96
|
+
|
97
|
+
# Declares a new class attribute that will expire after _age_ seconds
|
98
|
+
# and be replaced by the results of running the _block_. The block is
|
99
|
+
# lazily evaluated when the attribute is accessed after the expiration
|
100
|
+
# time. The block is evaluated in the context of the class.
|
101
|
+
#
|
102
|
+
# Obviously this scheme will only work if the attribute is only accessed
|
103
|
+
# using the setter and getter methods defined by this function.
|
104
|
+
#
|
105
|
+
# class A
|
106
|
+
# include ExpirationDate
|
107
|
+
# expiring_class_attr( :foo, 60 ) { 'foo' }
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# A.foo #=> 'foo'
|
111
|
+
# A.foo = 'bar'
|
112
|
+
# A.foo #=> 'bar'
|
113
|
+
# sleep 61
|
114
|
+
# A.foo #=> 'foo'
|
115
|
+
#
|
116
|
+
def expiring_class_attr( name, age, &block )
|
117
|
+
raise ArgumentError, "a block must be given" if block.nil?
|
118
|
+
|
119
|
+
name = name.to_sym
|
120
|
+
age = Float(age)
|
121
|
+
_class_managers_specials[name] = block
|
122
|
+
|
123
|
+
self.class_eval <<-CODE, __FILE__, __LINE__
|
124
|
+
def self.#{name}
|
125
|
+
now = Time.now
|
126
|
+
label = _class_expiration_labels[#{name.inspect}]
|
127
|
+
if label.expires_on.nil? || now >= label.expires_on
|
128
|
+
label.mutex.synchronize {
|
129
|
+
break unless label.expires_on.nil? || now >= label.expires_on
|
130
|
+
block = ::#{self.name}._class_managers_specials[#{name.inspect}]
|
131
|
+
@#{name} = instance_eval(&block)
|
132
|
+
label.age ||= #{age}
|
133
|
+
label.expires_on = now + label.age
|
134
|
+
}
|
135
|
+
end
|
136
|
+
@#{name}
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.#{name}=( val )
|
140
|
+
now = Time.now
|
141
|
+
label = _class_expiration_labels[#{name.inspect}]
|
142
|
+
label.mutex.synchronize {
|
143
|
+
@#{name} = val
|
144
|
+
label.age ||= #{age}
|
145
|
+
label.expires_on = now + label.age
|
146
|
+
}
|
147
|
+
@#{name}
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.expire_#{name}_now
|
151
|
+
label = _class_expiration_labels[#{name.inspect}]
|
152
|
+
label.mutex.synchronize {label.expires_on = Time.now - 1}
|
153
|
+
@#{name}
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.alter_#{name}_age( age )
|
157
|
+
label = _class_expiration_labels[#{name.inspect}]
|
158
|
+
label.mutex.synchronize {label.age = Float(age)}
|
159
|
+
end
|
79
160
|
CODE
|
80
161
|
end
|
81
162
|
|
@@ -86,55 +167,23 @@ module ExpirationDate
|
|
86
167
|
def _managers_specials
|
87
168
|
@_managers_specials ||= Hash.new
|
88
169
|
end
|
89
|
-
# :startdoc:
|
90
|
-
end
|
91
170
|
|
92
|
-
|
93
|
-
|
94
|
-
#
|
95
|
-
# expire_now( :foo )
|
96
|
-
#
|
97
|
-
def expire_now( name )
|
98
|
-
name = name.to_sym
|
99
|
-
if expiration_labels.key?(name)
|
100
|
-
now = Time.now
|
101
|
-
label = expiration_labels[name]
|
102
|
-
label.mutex.synchronize {
|
103
|
-
label.expires_on = now - 1
|
104
|
-
}
|
171
|
+
def _class_managers_specials
|
172
|
+
@_class_managers_specials ||= Hash.new
|
105
173
|
end
|
106
|
-
end
|
107
174
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
# immediately.
|
113
|
-
#
|
114
|
-
# Modify the 'foo' attribute so that it expires every 60 seconds.
|
115
|
-
#
|
116
|
-
# alter_expiration_label( :foo, 60 )
|
117
|
-
#
|
118
|
-
# Modify the 'bar' attribute so that it expires every two minutes. Make
|
119
|
-
# this new age take effect immediately.
|
120
|
-
#
|
121
|
-
# alter_expiration_label( :bar, 120 )
|
122
|
-
# expire_now( :bar )
|
123
|
-
#
|
124
|
-
def alter_expiration_label( name, age )
|
125
|
-
name = name.to_sym
|
126
|
-
if expiration_labels.key?(name)
|
127
|
-
label = expiration_labels[name]
|
128
|
-
label.mutex.synchronize {
|
129
|
-
label.age = Float(age)
|
130
|
-
}
|
175
|
+
def _class_expiration_labels
|
176
|
+
@_class_expiration_labels ||= Hash.new do |h,k|
|
177
|
+
h[k] = ExpirationLabel.new(Mutex.new)
|
178
|
+
end
|
131
179
|
end
|
180
|
+
# :startdoc:
|
132
181
|
end
|
133
182
|
|
134
183
|
# Accessor that returns the hash of ExpirationLabel objects.
|
135
184
|
#
|
136
|
-
def
|
137
|
-
@
|
185
|
+
def _expiration_labels
|
186
|
+
@_expiration_labels ||= Hash.new do |h,k|
|
138
187
|
h[k] = ExpirationLabel.new(Mutex.new)
|
139
188
|
end
|
140
189
|
end
|
@@ -7,8 +7,11 @@ class TestA
|
|
7
7
|
expiring_attr(:foo, 0.5) { 'foo' }
|
8
8
|
expiring_attr('bar', 10 ) { 'bar' }
|
9
9
|
expiring_attr(:me, 60 ) { self }
|
10
|
-
end
|
11
10
|
|
11
|
+
expiring_class_attr(:foo, 0.5) { 'class foo' }
|
12
|
+
expiring_class_attr('bar', 10 ) { 'class bar' }
|
13
|
+
expiring_class_attr(:me, 60 ) { self }
|
14
|
+
end
|
12
15
|
|
13
16
|
describe ExpirationDate do
|
14
17
|
it 'returns the same attribute before the expiration time' do
|
@@ -35,7 +38,7 @@ describe ExpirationDate do
|
|
35
38
|
bar.should be_equal(a.bar)
|
36
39
|
bar.should == 'bar'
|
37
40
|
|
38
|
-
a.
|
41
|
+
a.expire_bar_now
|
39
42
|
bar.should_not be_equal(a.bar)
|
40
43
|
a.bar.should == 'bar'
|
41
44
|
end
|
@@ -53,20 +56,20 @@ describe ExpirationDate do
|
|
53
56
|
a.foo.should == 'foo'
|
54
57
|
end
|
55
58
|
|
56
|
-
it
|
59
|
+
it "can alter an attribute's age" do
|
57
60
|
a = TestA.new
|
58
61
|
bar = a.bar
|
59
62
|
bar.should be_equal(a.bar)
|
60
63
|
bar.should == 'bar'
|
61
64
|
|
62
65
|
# this alteration takes place only after the attribute expires
|
63
|
-
a.
|
66
|
+
a.alter_bar_age 0.5
|
64
67
|
|
65
68
|
sleep 0.75
|
66
69
|
bar.should be_equal(a.bar)
|
67
70
|
|
68
71
|
# so now the age should only be 0.5 seconds
|
69
|
-
a.
|
72
|
+
a.expire_bar_now
|
70
73
|
bar.should_not be_equal(a.bar)
|
71
74
|
a.bar.should == 'bar'
|
72
75
|
|
@@ -74,23 +77,75 @@ describe ExpirationDate do
|
|
74
77
|
bar.should_not be_equal(a.bar)
|
75
78
|
end
|
76
79
|
|
77
|
-
it 'ignores requests on unknown expriation labels' do
|
78
|
-
a = TestA.new
|
79
|
-
|
80
|
-
h = a.expiration_labels
|
81
|
-
h.keys.length == 2
|
82
|
-
|
83
|
-
a.expire_now(:foobar)
|
84
|
-
h.keys.length == 2
|
85
|
-
|
86
|
-
a.alter_expiration_label(:foobar, 42)
|
87
|
-
h.keys.length == 2
|
88
|
-
end
|
89
|
-
|
90
80
|
it 'executes the block in the context of the instance' do
|
91
81
|
a = TestA.new
|
92
82
|
a.me.should be_equal(a)
|
93
83
|
end
|
84
|
+
|
85
|
+
describe 'when used at the class level' do
|
86
|
+
it 'returns the same attribute before the expiration time' do
|
87
|
+
foo = TestA.foo
|
88
|
+
foo.should be_equal(TestA.foo)
|
89
|
+
foo.should == 'class foo'
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'returns a new attribute after the expiration time' do
|
93
|
+
foo = TestA.foo
|
94
|
+
foo.should be_equal(TestA.foo)
|
95
|
+
foo.should == 'class foo'
|
96
|
+
|
97
|
+
sleep 0.75
|
98
|
+
foo.should_not be_equal(TestA.foo)
|
99
|
+
TestA.foo.should == 'class foo'
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'can immediately expire an attribute' do
|
103
|
+
bar = TestA.bar
|
104
|
+
bar.should be_equal(TestA.bar)
|
105
|
+
bar.should == 'class bar'
|
106
|
+
|
107
|
+
TestA.expire_bar_now
|
108
|
+
bar.should_not be_equal(TestA.bar)
|
109
|
+
TestA.bar.should == 'class bar'
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'can assign an attribute' do
|
113
|
+
TestA.foo.should == 'class foo'
|
114
|
+
|
115
|
+
foobar = 'foobar'
|
116
|
+
TestA.foo = foobar
|
117
|
+
TestA.foo.should be_equal(foobar)
|
118
|
+
|
119
|
+
sleep 0.75
|
120
|
+
TestA.foo.should_not be_equal(foobar)
|
121
|
+
TestA.foo.should == 'class foo'
|
122
|
+
end
|
123
|
+
|
124
|
+
it "can alter an attribute's age" do
|
125
|
+
bar = TestA.bar
|
126
|
+
bar.should be_equal(TestA.bar)
|
127
|
+
bar.should == 'class bar'
|
128
|
+
|
129
|
+
# this alteration takes place only after the attribute expires
|
130
|
+
TestA.alter_bar_age 0.5
|
131
|
+
|
132
|
+
sleep 0.75
|
133
|
+
bar.should be_equal(TestA.bar)
|
134
|
+
|
135
|
+
# so now the age should only be 0.5 seconds
|
136
|
+
TestA.expire_bar_now
|
137
|
+
bar.should_not be_equal(TestA.bar)
|
138
|
+
TestA.bar.should == 'class bar'
|
139
|
+
|
140
|
+
sleep 0.75
|
141
|
+
bar.should_not be_equal(TestA.bar)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'executes the block in the context of the class' do
|
145
|
+
TestA.me.should be_equal(TestA)
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
94
149
|
end
|
95
150
|
|
96
151
|
# EOF
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: expiration-date
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Pease
|
@@ -9,11 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-08-
|
12
|
+
date: 2008-08-25 00:00:00 -06:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description:
|
16
|
+
description: Really simple caching by attaching an expiration date to attributes. The ExpirationDate module adss two methods to a class -- expiring_attr and expiring_class_attr. These two methods are used to declare attributes in the instance and in the class, respectively, that will expire after some period of seconds have elapsed. The attribute is re-initialized from the given block after it has expired. This is a very simple form of caching.
|
17
17
|
email: tim.pease@gmail.com
|
18
18
|
executables: []
|
19
19
|
|
@@ -21,11 +21,12 @@ extensions: []
|
|
21
21
|
|
22
22
|
extra_rdoc_files:
|
23
23
|
- History.txt
|
24
|
-
- README.
|
24
|
+
- README.rdoc
|
25
25
|
files:
|
26
|
+
- .gitignore
|
26
27
|
- History.txt
|
27
28
|
- Manifest.txt
|
28
|
-
- README.
|
29
|
+
- README.rdoc
|
29
30
|
- Rakefile
|
30
31
|
- lib/expiration-date.rb
|
31
32
|
- spec/expiration-date_spec.rb
|
@@ -46,7 +47,7 @@ homepage: http://codeforpeople.rubyforge.org/expiration-date
|
|
46
47
|
post_install_message:
|
47
48
|
rdoc_options:
|
48
49
|
- --main
|
49
|
-
- README.
|
50
|
+
- README.rdoc
|
50
51
|
require_paths:
|
51
52
|
- lib
|
52
53
|
required_ruby_version: !ruby/object:Gem::Requirement
|