kissifer-hash-persistent 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{hash-persistent}
5
- s.version = "0.1.1"
5
+ s.version = "0.2.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["kissifer"]
9
- s.date = %q{2009-05-27}
9
+ s.date = %q{2009-05-28}
10
10
  s.email = %q{tierneydrchris@gmail.com}
11
11
  s.extra_rdoc_files = [
12
12
  "LICENSE",
@@ -21,8 +21,10 @@ Gem::Specification.new do |s|
21
21
  "VERSION",
22
22
  "hash-persistent.gemspec",
23
23
  "lib/hash-persistent.rb",
24
+ "lib/hash-persistent/collection.rb",
24
25
  "lib/hash-persistent/counter.rb",
25
26
  "lib/hash-persistent/resource.rb",
27
+ "spec/collection_spec.rb",
26
28
  "spec/counter_spec.rb",
27
29
  "spec/resource_spec.rb",
28
30
  "spec/spec.opts",
@@ -34,7 +36,8 @@ Gem::Specification.new do |s|
34
36
  s.rubygems_version = %q{1.3.3}
35
37
  s.summary = %q{Library of base classes to simplify persisting objects in a moneta store}
36
38
  s.test_files = [
37
- "spec/counter_spec.rb",
39
+ "spec/collection_spec.rb",
40
+ "spec/counter_spec.rb",
38
41
  "spec/resource_spec.rb",
39
42
  "spec/spec_helper.rb"
40
43
  ]
@@ -3,3 +3,4 @@ $LOAD_PATH << File.dirname(__FILE__)
3
3
  require 'rubygems'
4
4
  require 'hash-persistent/counter'
5
5
  require 'hash-persistent/resource'
6
+ require 'hash-persistent/collection'
@@ -0,0 +1,50 @@
1
+ module HashPersistent
2
+ module Collection
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.module_eval do
7
+ include HashPersistent::Resource
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def attach(resource_class, collection_basis)
13
+ raise ArgumentError unless resource_class.included_modules.include?(HashPersistent::Resource)
14
+ raise ArgumentError unless resource_class.new.respond_to?(collection_basis)
15
+ @collection_basis = collection_basis
16
+ resource_class.on_save do |resource|
17
+ collected_resource_saved(resource)
18
+ end
19
+
20
+ resource_class.on_delete do |resource|
21
+ collected_resource_deleted(resource)
22
+ end
23
+ end
24
+
25
+ def collected_resource_saved(resource)
26
+ collection = find(resource.send(@collection_basis))
27
+ unless collection
28
+ collection = new
29
+ collection.collected_keys = []
30
+ collection.key = resource.send(@collection_basis)
31
+ end
32
+ collection.collected_keys << resource.key
33
+ collection.collected_keys.uniq!
34
+ collection.save
35
+ end
36
+
37
+ def collected_resource_deleted(resource)
38
+ collection = find(resource.send(@collection_basis))
39
+ collection.collected_keys.delete(resource.key)
40
+ if collection.collected_keys.empty?
41
+ collection.delete
42
+ else
43
+ collection.save
44
+ end
45
+ end
46
+ end
47
+
48
+ attr_accessor :collected_keys
49
+ end
50
+ end
@@ -18,6 +18,22 @@ module HashPersistent
18
18
  def find(key)
19
19
  @store[@prefix + key]
20
20
  end
21
+
22
+ def on_save(&block)
23
+ if block
24
+ @on_save = block
25
+ else
26
+ @on_save
27
+ end
28
+ end
29
+
30
+ def on_delete(&block)
31
+ if block
32
+ @on_delete = block
33
+ else
34
+ @on_delete
35
+ end
36
+ end
21
37
 
22
38
  attr_reader :store, :prefix
23
39
  end
@@ -31,13 +47,17 @@ module HashPersistent
31
47
  def save
32
48
  raise RuntimeError unless key
33
49
  self.class.store[prefix_key] = self
50
+ self.class.on_save.call(self) if self.class.on_save
34
51
  end
35
52
 
36
53
  def delete
37
- raise RuntimeError unless self.class.store.has_key?(prefix_key)
54
+ raise RuntimeError unless key
55
+ return unless self.class.store.has_key?(prefix_key)
56
+ # TODO concurrency here, the store's delete call should cope, but we don't want the callback if something else got in first?
57
+ # Alternative: ensure that callback handles this case, which is true of HashPersistent::Collection
38
58
  self.class.store.delete(prefix_key)
59
+ self.class.on_delete.call(self) if self.class.on_delete
39
60
  end
40
-
41
61
  end
42
62
  end
43
63
 
@@ -0,0 +1,195 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class CollectionFoo
4
+ include HashPersistent::Collection
5
+ end
6
+
7
+ class CollectedResource
8
+ include HashPersistent::Resource
9
+ attr_accessor :basis
10
+ end
11
+
12
+ class NotAResource
13
+ end
14
+
15
+ describe "A class that includes HashPersistent::Collection" do
16
+ it "should include the HashPersistent::Resource module" do
17
+ CollectionFoo.included_modules.should include (HashPersistent::Resource)
18
+ end
19
+
20
+ it "should allow itself to be attached to another HashPersistent::Resource, specifying an attribute/method for the collection basis" do
21
+ lambda{CollectionFoo.attach(CollectedResource, :basis)}.should_not raise_error
22
+ end
23
+
24
+ it "should not attach to the wrong class type" do
25
+ lambda{CollectionFoo.attach(NotAResource, :basis)}.should raise_error
26
+ lambda{CollectionFoo.attach(1, :basis)}.should raise_error
27
+ end
28
+
29
+ it "should verify a valid collection basis" do
30
+ lambda{CollectionFoo.attach(CollectedResource)}.should raise_error
31
+ lambda{CollectionFoo.attach(CollectedResource, :not_a_valid_attr)}.should raise_error
32
+ end
33
+
34
+ context "(when attached resources are saved/deleted)" do
35
+ it "should create a collection for a new basis" do
36
+ CollectionFoo.persist_to({}, "")
37
+ CollectedResource.persist_to({}, "")
38
+
39
+ CollectionFoo.attach(CollectedResource, :basis)
40
+ CollectionFoo.find("the_basis").should == nil
41
+
42
+ resource = CollectedResource.new
43
+ resource.basis = "the_basis"
44
+ resource.key = "fred"
45
+ resource.save
46
+
47
+ CollectionFoo.find("the_basis").should_not == nil
48
+ end
49
+
50
+ it "should report keys saved/added to the collection" do
51
+ CollectionFoo.persist_to({}, "")
52
+ CollectedResource.persist_to({}, "")
53
+
54
+ CollectionFoo.attach(CollectedResource, :basis)
55
+
56
+ resource = CollectedResource.new
57
+ resource.basis = "the_basis"
58
+ resource.key = "fred"
59
+ resource.save
60
+
61
+ CollectionFoo.find("the_basis").collected_keys.should == ["fred"]
62
+
63
+ resource = CollectedResource.new
64
+ resource.basis = "the_basis"
65
+ resource.key = "barney"
66
+ resource.save
67
+
68
+ CollectionFoo.find("the_basis").collected_keys.should == ["fred", "barney"]
69
+ end
70
+
71
+ it "should not duplicate keys when a resource is saved multiple times" do
72
+ CollectionFoo.persist_to({}, "")
73
+ CollectedResource.persist_to({}, "")
74
+
75
+ CollectionFoo.attach(CollectedResource, :basis)
76
+
77
+ resource = CollectedResource.new
78
+ resource.basis = "the_basis"
79
+ resource.key = "fred"
80
+ resource.save
81
+ resource.save
82
+
83
+ CollectionFoo.find("the_basis").collected_keys.should == ["fred"]
84
+ end
85
+
86
+ it "should not report keys with the wrong basis" do
87
+ CollectionFoo.persist_to({}, "")
88
+ CollectedResource.persist_to({}, "")
89
+
90
+ CollectionFoo.attach(CollectedResource, :basis)
91
+
92
+ resource = CollectedResource.new
93
+ resource.basis = "one_basis"
94
+ resource.key = "fred"
95
+ resource.save
96
+
97
+ CollectionFoo.find("another_basis").should == nil
98
+
99
+ resource = CollectedResource.new
100
+ resource.basis = "another_basis"
101
+ resource.key = "barney"
102
+ resource.save
103
+
104
+ CollectionFoo.find("one_basis").collected_keys.should == ["fred"]
105
+ CollectionFoo.find("another_basis").collected_keys.should == ["barney"]
106
+ end
107
+
108
+ it "should remove the key when a resource is deleted" do
109
+ CollectionFoo.persist_to({}, "")
110
+ CollectedResource.persist_to({}, "")
111
+
112
+ CollectionFoo.attach(CollectedResource, :basis)
113
+
114
+ resource = CollectedResource.new
115
+ resource.basis = "the_basis"
116
+ resource.key = "fred"
117
+ resource.save
118
+
119
+ resource = CollectedResource.new
120
+ resource.basis = "the_basis"
121
+ resource.key = "barney"
122
+ resource.save
123
+ resource.delete
124
+
125
+ CollectionFoo.find("the_basis").collected_keys.should == ["fred"]
126
+ end
127
+
128
+ it "should not complain when a not-present key is removed (either not added, or deleted twice)" do
129
+ CollectionFoo.persist_to({}, "")
130
+ CollectedResource.persist_to({}, "")
131
+
132
+ CollectionFoo.attach(CollectedResource, :basis)
133
+
134
+ resource = CollectedResource.new
135
+ resource.basis = "the_basis"
136
+ resource.key = "fred"
137
+ resource.save
138
+
139
+ resource = CollectedResource.new
140
+ resource.basis = "the_basis"
141
+ resource.key = "barney"
142
+
143
+ lambda{resource.delete}.should_not raise_error
144
+
145
+ resource.save
146
+ resource.delete
147
+ lambda{resource.delete}.should_not raise_error
148
+
149
+ CollectionFoo.find("the_basis").collected_keys.should == ["fred"]
150
+ end
151
+
152
+ it "should not complain when a resource with an unknown basis is removed" do
153
+ CollectionFoo.persist_to({}, "")
154
+ CollectedResource.persist_to({}, "")
155
+
156
+ CollectionFoo.attach(CollectedResource, :basis)
157
+
158
+ resource = CollectedResource.new
159
+ resource.basis = "the_basis"
160
+ resource.key = "fred"
161
+
162
+ lambda{resource.delete}.should_not raise_error
163
+ end
164
+
165
+ it "should remove the collection when its final key is removed" do
166
+ CollectionFoo.persist_to({}, "")
167
+ CollectedResource.persist_to({}, "")
168
+
169
+ CollectionFoo.attach(CollectedResource, :basis)
170
+
171
+ resource = CollectedResource.new
172
+ resource.basis = "the_basis"
173
+ resource.key = "fred"
174
+ resource.save
175
+
176
+ resource = CollectedResource.new
177
+ resource.basis = "the_basis"
178
+ resource.key = "barney"
179
+ resource.save
180
+
181
+ CollectedResource.find("fred").delete
182
+ CollectedResource.find("barney").delete
183
+
184
+ CollectionFoo.find("the_basis").should == nil
185
+ end
186
+
187
+ end
188
+
189
+ context "(concurrent access)" do
190
+ it "should be careful when creating and updating the collection resources"
191
+ # Cases to cover here...
192
+ # - delete last basis and add basis race condition
193
+ # - add basis and add basis race condition
194
+ end
195
+ end
data/spec/counter_spec.rb CHANGED
@@ -16,9 +16,7 @@ describe "HashPersistent::Counter" do
16
16
  CounterFoo.next_key.should_not == CounterFoo.next_key
17
17
  end
18
18
 
19
- it "should be careful when incrementing the key in multi-threaded/process environment" do
20
- pending
21
- end
19
+ it "should be careful when incrementing the key in multi-threaded/process environment"
22
20
  end
23
21
 
24
22
  context "when mixed-in to more than one class" do
@@ -63,12 +63,15 @@ describe "A class that includes HashPersistent::Resource" do
63
63
  lambda{ResourceFoo.new.save}.should raise_error
64
64
  end
65
65
 
66
- it "should not be deletable, even after a key is set" do
66
+ it "should not delete unless the key has been explictly set" do
67
+ lambda{ResourceFoo.new.delete}.should raise_error
68
+ end
69
+
70
+ it "should not complain when deleted, even after a key is set" do
67
71
  ResourceFoo.persist_to(Hash.new, "")
68
72
  resource = ResourceFoo.new
69
- lambda{resource.delete}.should raise_error
70
73
  resource.key = "fred"
71
- lambda{resource.delete}.should raise_error
74
+ lambda{resource.delete}.should_not raise_error
72
75
  end
73
76
 
74
77
  it "should not be findable via its key" do
@@ -135,13 +138,13 @@ describe "A class that includes HashPersistent::Resource" do
135
138
  ResourceFoo.find("fred").should == nil
136
139
  end
137
140
 
138
- it "should not be deletable" do
141
+ it "should not complain when deleted" do
139
142
  ResourceFoo.persist_to(Hash.new, "")
140
143
  resource = ResourceFoo.new
141
144
  resource.key = "fred"
142
145
  resource.save
143
146
  resource.delete
144
- lambda{resource.delete}.should raise_error
147
+ lambda{resource.delete}.should_not raise_error
145
148
  end
146
149
  end
147
150
 
@@ -230,6 +233,7 @@ describe "A class that includes HashPersistent::Resource" do
230
233
  end
231
234
 
232
235
  context "(multiple classes using module)" do
236
+ # TODO: should check the callbacks here? Seems over the top...
233
237
  it "should not cross-contaminate classes that include the module" do
234
238
  store_1 = Hash.new
235
239
  ResourceFoo.persist_to(store_1, "")
@@ -268,4 +272,54 @@ describe "A class that includes HashPersistent::Resource" do
268
272
  store_2.should == Hash.new
269
273
  end
270
274
  end
275
+
276
+ context "(callbacks)" do
277
+ it "should allow a class level callback to be set and retrieved for save events" do
278
+ ResourceFoo.on_save.should == nil
279
+ lambda{ResourceFoo.on_save do
280
+ "thing"
281
+ end}.should_not raise_error
282
+ ResourceFoo.on_save.call.should == "thing"
283
+ lambda{ResourceFoo.on_save(1)}.should raise_error
284
+ end
285
+
286
+ it "should allow a class level callback to be set and retrieved for delete events" do
287
+ ResourceFoo.on_delete.should == nil
288
+ lambda{ResourceFoo.on_delete do
289
+ "thing"
290
+ end}.should_not raise_error
291
+ ResourceFoo.on_delete.call.should == "thing"
292
+ lambda{ResourceFoo.on_delete(1)}.should raise_error
293
+ end
294
+
295
+ it "should yield an instance to the save callback when the instance is saved" do
296
+ yielded_instance = nil
297
+ ResourceFoo.on_save do |yielded|
298
+ yielded_instance = yielded
299
+ end
300
+
301
+ instance = ResourceFoo.new
302
+ instance.key = "1"
303
+ instance.save
304
+
305
+ yielded_instance.should_not == nil
306
+ yielded_instance.should == instance
307
+ end
308
+
309
+ it "should yield an instance to the delete callback when the instance is deleted" do
310
+ yielded_instance = nil
311
+ ResourceFoo.on_delete do |yielded|
312
+ yielded_instance = yielded
313
+ end
314
+
315
+ instance = ResourceFoo.new
316
+ instance.key = "1"
317
+ instance.save
318
+ instance.delete
319
+
320
+ yielded_instance.should_not == nil
321
+ yielded_instance.should == instance
322
+ end
323
+
324
+ end
271
325
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kissifer-hash-persistent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kissifer
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-27 00:00:00 -07:00
12
+ date: 2009-05-28 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -31,8 +31,10 @@ files:
31
31
  - VERSION
32
32
  - hash-persistent.gemspec
33
33
  - lib/hash-persistent.rb
34
+ - lib/hash-persistent/collection.rb
34
35
  - lib/hash-persistent/counter.rb
35
36
  - lib/hash-persistent/resource.rb
37
+ - spec/collection_spec.rb
36
38
  - spec/counter_spec.rb
37
39
  - spec/resource_spec.rb
38
40
  - spec/spec.opts
@@ -64,6 +66,7 @@ signing_key:
64
66
  specification_version: 3
65
67
  summary: Library of base classes to simplify persisting objects in a moneta store
66
68
  test_files:
69
+ - spec/collection_spec.rb
67
70
  - spec/counter_spec.rb
68
71
  - spec/resource_spec.rb
69
72
  - spec/spec_helper.rb