kissifer-hash-persistent 0.1.1 → 0.2.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/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