hold 1.0.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/lib/hold.rb ADDED
@@ -0,0 +1,14 @@
1
+ require_relative 'hold/interfaces'
2
+ require_relative 'hold/file/hash_repository'
3
+ require_relative 'hold/sequel'
4
+
5
+ module Hold; end
6
+ Persistence = Hold
7
+
8
+ module Kernel
9
+ def self.const_missing(const_name)
10
+ super unless const_name == :Persistence
11
+ warn "'Persistence' has been deprecated, please use 'Hold' instead"
12
+ Hold
13
+ end
14
+ end
@@ -0,0 +1,59 @@
1
+ require 'hold/interfaces'
2
+ require 'fileutils'
3
+
4
+ # A simple HashRepository (ie key/value store implementation) which stores each
5
+ # key in a separate file.
6
+ #
7
+ # * Keys must be suitable pathnames
8
+ # * Values must be strings
9
+ # * base_path should end with a /, or keys should start with a /, one or the other
10
+ # * subdirectories will be created as required if the keys contain path separators
11
+ # * watch out for per-directory file limits.
12
+ #
13
+ # NB: Not threadsafe for writes
14
+ module Hold
15
+ module File; end
16
+
17
+ class File::HashRepository
18
+ include Hold::HashRepository
19
+
20
+ def can_get_class?(klass); klass == String; end
21
+ def can_set_class?(klass); klass == String; end
22
+
23
+ def initialize(base_path)
24
+ @base_path = base_path
25
+ end
26
+
27
+ def path_to_key(key)
28
+ "#{@base_path}#{key}"
29
+ end
30
+
31
+ def set_with_key(key, value)
32
+ path = path_to_key(key)
33
+ FileUtils.mkdir_p(::File.dirname(path))
34
+ ::File.open(path, "wb") {|file| file.write(value.to_s)}
35
+ value
36
+ end
37
+
38
+ def get_with_key(key)
39
+ path = path_to_key(key)
40
+ begin
41
+ ::File.read(path)
42
+ rescue Errno::ENOENT
43
+ end
44
+ end
45
+
46
+ def clear_key(key)
47
+ path = path_to_key(key)
48
+ begin
49
+ ::File.unlink(path)
50
+ rescue Errno::ENOENT
51
+ end
52
+ end
53
+
54
+ def has_key?(key)
55
+ ::File.exist?(path_to_key(key))
56
+ end
57
+ alias_method :key?, :has_key?
58
+ end
59
+ end
@@ -0,0 +1,184 @@
1
+ require 'set'
2
+
3
+ module Hold
4
+
5
+ # These are a set of implementations of Hold interfaces based on in-memory storage.
6
+ # They're not threadsafe or for production use, but are here as lightweight implementations to use in
7
+ # tests, and for illustrative purposes.
8
+ module InMemory; end
9
+
10
+ ARG_EMPTY = Object.new.freeze # something different to everything else
11
+
12
+ class InMemory::Cell
13
+ include Hold::Cell
14
+
15
+ # new -- empty
16
+ # new(nil) -- non-empty, value is nil
17
+ # new(123) -- non-empty, value is 123
18
+ def initialize(value=ARG_EMPTY)
19
+ @value = value unless ARG_EMPTY.equal?(value)
20
+ end
21
+
22
+ def get
23
+ @value
24
+ end
25
+
26
+ def set(value)
27
+ @value = value
28
+ end
29
+
30
+ def empty?
31
+ !instance_variable_defined?(:@value)
32
+ end
33
+
34
+ def clear
35
+ remove_instance_variable(:@value) if instance_variable_defined?(:@value)
36
+ end
37
+ end
38
+
39
+ class InMemory::ArrayCell
40
+ include Hold::ArrayCell
41
+
42
+ def initialize(array=[])
43
+ @array = array
44
+ end
45
+
46
+ def get
47
+ @array.dup
48
+ end
49
+
50
+ def set(value)
51
+ @array.replace(value)
52
+ end
53
+
54
+ def get_slice(start,length)
55
+ @array[start, length]
56
+ end
57
+
58
+ def get_length
59
+ @array.length
60
+ end
61
+ end
62
+
63
+ class InMemory::ObjectCell < InMemory::Cell
64
+ include Hold::ObjectCell
65
+
66
+ def get
67
+ @value && @value.dup
68
+ end
69
+
70
+ def get_property(property_name)
71
+ @value && @value[property_name]
72
+ end
73
+
74
+ def set_property(property_name, value)
75
+ raise EmptyConflict unless @value
76
+ @value[property_name] = value
77
+ end
78
+
79
+ def clear_property(property_name)
80
+ raise EmptyConflict unless @value
81
+ @value.delete(property_name)
82
+ end
83
+
84
+ def has_property?(property_name)
85
+ raise EmptyConflict unless @value
86
+ @value.has_key?(property_name)
87
+ end
88
+ end
89
+
90
+ class InMemory::HashRepository
91
+ include Hold::HashRepository
92
+
93
+ def initialize
94
+ @hash = {}
95
+ end
96
+
97
+ def set_with_key(key, value)
98
+ @hash[key] = value
99
+ end
100
+
101
+ def get_with_key(key)
102
+ value = @hash[key] and value.dup
103
+ end
104
+
105
+ def clear_key(key)
106
+ @hash.delete(key)
107
+ end
108
+
109
+ def has_key?(key)
110
+ @hash.key?(key)
111
+ end
112
+ alias_method :key?, :has_key?
113
+ end
114
+
115
+ class InMemory::SetRepository
116
+ include Hold::SetRepository
117
+
118
+ def initialize
119
+ @set = Set.new
120
+ end
121
+
122
+ def store(value)
123
+ @set << value
124
+ end
125
+
126
+ def delete(value)
127
+ @set.delete(value)
128
+ end
129
+
130
+ def contains?(value)
131
+ @set.include?(value)
132
+ end
133
+
134
+ def get_all
135
+ @set.to_a
136
+ end
137
+ end
138
+
139
+ class InMemory::IdentitySetRepository
140
+ include Hold::IdentitySetRepository
141
+
142
+ def initialize(allocates_ids=false)
143
+ @by_id = {}
144
+ @id_seq = 0 if allocates_ids
145
+ end
146
+
147
+ def allocates_ids?
148
+ !!@id_seq
149
+ end
150
+
151
+ def store(object)
152
+ id = object.id
153
+ object.send(:id=, id = @id_seq += 1) if @id_seq && !id
154
+ raise MissingIdentity unless id
155
+ @by_id[id] = object
156
+ end
157
+
158
+ def delete(object)
159
+ id = object.id or raise MissingIdentity
160
+ delete_id(id)
161
+ end
162
+
163
+ def contains?(object)
164
+ id = object.id or raise MissingIdentity
165
+ @by_id.include?(id)
166
+ end
167
+
168
+ def get_all
169
+ @by_id.values
170
+ end
171
+
172
+ def get_by_id(id)
173
+ value = @by_id[id] and value.dup
174
+ end
175
+
176
+ def delete_id(id)
177
+ @by_id.delete(id)
178
+ end
179
+
180
+ def contains_id?(id)
181
+ @by_id.include?(id)
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,441 @@
1
+ require 'thin_models/lazy_array'
2
+
3
+ module Hold
4
+ class Error < ::RuntimeError; end
5
+ class UnsupportedOperation < Error; end
6
+ class EmptyConflict < Error; end
7
+ class IdentityConflict < Error; end
8
+ class MissingIdentity < Error; end
9
+
10
+ # A set of interfaces for persistence based around an object model.
11
+ #
12
+ # We're expected to use various implementations of these interfaces,
13
+ # including in-memory persistence, serialized persistence in a cache,
14
+ # persistence via mapping to a relational database, and combined database /
15
+ # cache lookup.
16
+ #
17
+ # They should also be quite easy to wrap in a restful resource layer, since
18
+ # the resource structure may often correspond closely to an object model
19
+ # persistence interface.
20
+
21
+ # The most fundamental persistence interface. Just offers a storage slot
22
+ # which stores a single instance, supporting get/set
23
+ module Cell
24
+ def get
25
+ raise UnsupportedOperation
26
+ end
27
+
28
+ def set(value)
29
+ raise UnsupportedOperation
30
+ end
31
+
32
+ # Cells may optionally be 'emptyable?', that is, admit a special state of
33
+ # 'empty' which is different to the state of storing an instance.
34
+ #
35
+ # empty and nil are distinct states.
36
+ #
37
+ # empty: undefined / uninitialized / unknown / not persisted / key not
38
+ # present in hash / missing nil: null / known to be nil / persisted
39
+ # explicitly as being nil / key present in hash with value of nil
40
+ #
41
+ # Annoying as this may seem this is useful in a bunch of contexts with the
42
+ # data models we're constrained to be using. Eg "row exists but value of
43
+ # column is NULL" vs "row doesn't exist" in SQL, or "property missing" vs
44
+ # "property present and equal to null" for JSON objects
45
+
46
+ def empty?
47
+ false
48
+ end
49
+
50
+ def clear
51
+ raise UnsupportedOperation
52
+ end
53
+
54
+ def set_if_empty(value)
55
+ raise EmptyConflict unless empty?
56
+ set(value)
57
+ end
58
+
59
+ def set_unless_empty(value)
60
+ raise EmptyConflict if empty?
61
+ set(value)
62
+ end
63
+
64
+ def get_unless_empty
65
+ raise EmptyConflict if empty?
66
+ get
67
+ end
68
+ alias :get! :get_unless_empty
69
+
70
+ # Can override to indicate if you only support getting/setting a particular
71
+ # class or classes:
72
+ def can_get_class?(klass); true; end
73
+ def can_set_class?(klass); true; end
74
+ end
75
+
76
+ # Interface extending Cell which offers some array-specific persistence
77
+ # methods for use only with Arrays.
78
+ # Default implementations are in terms of get, but it's expected that you'd
79
+ # override with more efficient implementations.
80
+ module ArrayCell
81
+ include Cell
82
+
83
+ def get_slice(start, length)
84
+ value = get() and value[start, length]
85
+ end
86
+
87
+ def get_length
88
+ value = get() and value.length
89
+ end
90
+
91
+ # returns an instance of ThinModels::LazyArray which lazily computes slices
92
+ # and length based on the get_length and get_slice methods you define.
93
+ def get_lazy_array
94
+ LazyArray.new(self)
95
+ end
96
+
97
+ def can_get_class?(klass); klass == Array; end
98
+ def can_set_class?(klass); klass <= Array; end
99
+
100
+ # Can override to indicate if you only support getting/setting arrays with
101
+ # items of a particular class or classes:
102
+ def can_get_item_class?(klass); true; end
103
+ def can_set_item_class?(klass); true; end
104
+
105
+ class LazyArray < ThinModels::LazyArray::Memoized
106
+ def initialize(array_cell)
107
+ @array_cell = array_cell
108
+ end
109
+
110
+ def _each(&b)
111
+ @array_cell.get.each(&b)
112
+ end
113
+
114
+ def slice_from_start_and_length(start, length)
115
+ @array_cell.get_slice(start, length)
116
+ end
117
+
118
+ def _length
119
+ @array_cell.get_length
120
+ end
121
+ end
122
+ end
123
+
124
+ # Interface extending Cell which offers some object-property-specific
125
+ # persistence methods for use only with Structs/Objects.
126
+ # Default implementations are in terms of get and set, but it's expected that
127
+ # you'd override with more efficient implementations.
128
+ module ObjectCell
129
+ include Cell
130
+
131
+ # default implementation gets the entire object in order to get the
132
+ # property in question. you might want to override with something more
133
+ # efficient
134
+ def get_property(property_name)
135
+ value = get() and value[property_name]
136
+ end
137
+
138
+ # default implementation gets the entire object and replaces it with a
139
+ # version with the property in question changed.
140
+ # you might want to override with something more efficient.
141
+ def set_property(property_name, value)
142
+ object = get()
143
+ object[property_name] = value
144
+ set(object)
145
+ end
146
+
147
+ def clear_property(property_name)
148
+ value = get()
149
+ value.delete(property_name)
150
+ set(value)
151
+ end
152
+
153
+ def has_property?(property_name)
154
+ !get_property(property_name).nil?
155
+ end
156
+
157
+ def get_properties(*properties)
158
+ properties.map {|p| get_property(p)}
159
+ end
160
+
161
+ # May return a Cell which allows get / set / potentially other operations
162
+ # on a particular property of this object in the context of its parent
163
+ # object.
164
+ #
165
+ # Be careful about the semantics if exposing property cells which allow
166
+ # partial write operations (like set_property) on the property value in the
167
+ # context of the parent object. If you do this it should only update the
168
+ # property value in that context, not in all contexts.
169
+ #
170
+ # By analogy to normal ruby hashes, it should mean this:
171
+ # a[:foo] = a[:foo].merge(:bar => 3)
172
+ # rather than this:
173
+ # a[:foo][:bar] = 3
174
+ # which would have an effect visible to any other object holding a
175
+ # reference to a[:foo].
176
+ #
177
+ # If you want the latter, you probably want to be updating a[:foo] in some
178
+ # hold cell which is canonical for the identity of that object.
179
+ #
180
+ # If you don't want the former, don't return a PropertyCell which allows
181
+ # partial updates. For simplicity's sake this is the stance taken by the
182
+ # default PropertyCell implementation.
183
+ def property_cell(property_name)
184
+ PropertyCell.new(self, property_name)
185
+ end
186
+
187
+ # An implementation of the basic Cell interface designed to wrap a property
188
+ # of an ObjectCell as a Cell itself.
189
+ class PropertyCell
190
+ include Cell
191
+
192
+ def initialize(object_cell, property_name)
193
+ @object_cell = object_cell
194
+ @property_name = property_name
195
+ end
196
+
197
+ def get
198
+ @object_cell.get_property(@property_name)
199
+ end
200
+
201
+ def set(value)
202
+ @object_cell.set_property(@property_name, value)
203
+ end
204
+
205
+ def empty?
206
+ !@object_cell.has_property?(@property_name)
207
+ end
208
+
209
+ def clear
210
+ @object_cell.clear_property(@property_name)
211
+ end
212
+ end
213
+
214
+ # These are here for you to use if you want to use them for Array
215
+ # properties gotten via ObjectCells, although the default implementation of
216
+ # property_cell doesn't do this
217
+ class ArrayPropertyCell < PropertyCell
218
+ include ArrayCell
219
+ end
220
+
221
+ class ObjectPropertyCell < PropertyCell
222
+ include ObjectCell
223
+ end
224
+ end
225
+
226
+
227
+ # Persists values in a key/value store
228
+ module HashRepository
229
+ def set_with_key(key, value)
230
+ raise UnsupportedOperation
231
+ end
232
+
233
+ def get_with_key(key)
234
+ raise UnsupportedOperation
235
+ end
236
+
237
+ # Gets multiple entities at a time by a list of keys.
238
+ # May override with an efficient multi-get implementation.
239
+ def get_many_with_keys(keys)
240
+ keys.map {|key| get_with_key(key)}
241
+ end
242
+
243
+ def clear_key(key)
244
+ raise UnsupportedOperation
245
+ end
246
+
247
+ def has_key?(key)
248
+ raise UnsupportedOperation
249
+ end
250
+ alias_method :key?, :has_key?
251
+
252
+ def key_cell(key)
253
+ KeyCell.new(self, key)
254
+ end
255
+
256
+ # Can override to indicate if you only support getting/setting values of a
257
+ # particular class or classes:
258
+ def can_get_class?(klass); true; end
259
+ def can_set_class?(klass); true; end
260
+
261
+ class KeyCell
262
+ include Cell
263
+
264
+ def initialize(hash_repository, key)
265
+ @hash_repository, @key = hash_repository, key
266
+ end
267
+
268
+ def get
269
+ @hash_repository.get_with_key(@key)
270
+ end
271
+
272
+ def set(value)
273
+ @hash_repository.set_with_key(@key, value)
274
+ end
275
+
276
+ def clear
277
+ @hash_repository.clear_key(@key)
278
+ end
279
+
280
+ def empty?
281
+ @hash_repository.has_key?(@key)
282
+ end
283
+
284
+ def can_get_class?(klass); @hash_repository.can_get_class?(klass); end
285
+ def can_set_class?(klass); @hash_repository.can_set_class?(klass); end
286
+ end
287
+ end
288
+
289
+ module SetRepository
290
+ # Store the object in the persisted set. If the object is already in the
291
+ # set, it may stay there untouched (in the case where the object's identity
292
+ # is based on its entire contents), or get replaced by the newer version
293
+ # (where the object's identity is only based on, say, some identity
294
+ # property), but will never be duplicated (since this is a set)
295
+ def store(object)
296
+ raise UnsupportedOperation
297
+ end
298
+
299
+ # like store, but should raise IdentityConflict if the object (or one equal
300
+ # to it) already exists in the set
301
+ def store_new(object)
302
+ raise IdentityConflict if contains?(object)
303
+ store(object)
304
+ end
305
+
306
+ # Removes the object with this identity from the persisted set
307
+ def delete(object)
308
+ raise UnsupportedOperation
309
+ end
310
+
311
+ # Is this object in the persisted set?
312
+ def contains?(object)
313
+ raise UnsupportedOperation
314
+ end
315
+
316
+ # Returns an array of all persisted items in the set
317
+ def get_all
318
+ raise UnsupportedOperation
319
+ end
320
+
321
+ def can_get_class?(klass); true; end
322
+ def can_set_class?(klass); true; end
323
+ end
324
+
325
+ # A special kind of SetRepository which stores Objects whose identities are
326
+ # determined by an identity property, and supports indexed lookup by their
327
+ # id.
328
+ #
329
+ # May allocate the IDs itself, or not.
330
+ #
331
+ # Exposes a somewhat more familiar CRUD-style persistence interface as a
332
+ # result.
333
+ #
334
+ # Comes with default implementations for most of the extra interface
335
+ module IdentitySetRepository
336
+ include SetRepository
337
+
338
+ # Either the repository allocates IDs, and you don't (in which case any
339
+ # entity with an ID may be assumed to be already persisted in the repo), or
340
+ # the repository doesn't allocate IDs (in which case you must always supply
341
+ # one when persisting a new object).
342
+ #
343
+ # If you allocates_ids?, you should deal with an object without an identity
344
+ # as an argument to store and store_new, and you should set the id property
345
+ # on it before returning it.
346
+ #
347
+ # If you don't, you may raise MissingIdentity if passed an object without
348
+ # one.
349
+ def allocates_ids?
350
+ false
351
+ end
352
+
353
+ # Looks up a persisted object by the value of its identity property
354
+ def get_by_id(id)
355
+ raise UnsupportedOperation
356
+ end
357
+
358
+ # deletes the object with the given identity where it exists in the repo
359
+ def delete_id(id)
360
+ delete(get_by_id(id))
361
+ end
362
+
363
+ # Loads a fresh instance of the given object by its id
364
+ # Returns nil where the object is no longer present in the repository
365
+ def reload(object)
366
+ id = object.id or raise MissingIdentity
367
+ get_by_id(id)
368
+ end
369
+
370
+ # Like reload, but updates the given instance in-place with the updated
371
+ # data.
372
+ # Returns nil where the object is no longer present in the repository
373
+ def load(object)
374
+ raise UnsupportedOperation unless object.respond_to?(:merge!)
375
+ updated = reload(object) or return
376
+ object.merge!(updated)
377
+ object
378
+ end
379
+
380
+ # Applies an in-place update to the object, where it exists in the
381
+ # repository
382
+ def update(entity, update_entity)
383
+ raise UnsupportedOperation unless entity.respond_to?(:merge!)
384
+ load(entity) or return
385
+ entity.merge!(update_entity)
386
+ store(entity)
387
+ end
388
+
389
+ # Applies an in-place update to the object with the given identity, where
390
+ # it exists in the repository
391
+ def update_by_id(id, update_entity)
392
+ entity = get_by_id(id) or return
393
+ raise UnsupportedOperation unless entity.respond_to?(:merge!)
394
+ entity.merge!(update_entity)
395
+ store(entity)
396
+ end
397
+
398
+ def contains_id?(id)
399
+ !!get_by_id(id)
400
+ end
401
+
402
+
403
+ def get_many_by_ids(ids)
404
+ ids.map {|id| get_by_id(id)}
405
+ end
406
+
407
+ def id_cell(id)
408
+ IdCell.new(self, id)
409
+ end
410
+
411
+ def cell(object)
412
+ id = object.id or raise MissingIdentity
413
+ id_cell(id)
414
+ end
415
+
416
+ class IdCell
417
+ include ObjectCell
418
+
419
+ def initialize(id_set_repo, id)
420
+ @id_set_repo = id_set_repo
421
+ @id = id
422
+ end
423
+
424
+ def get
425
+ @id_set_repo.get_by_id(@id)
426
+ end
427
+
428
+ def set(value)
429
+ @id_set_repo.update_by_id(@id, value)
430
+ end
431
+
432
+ def empty?
433
+ !@id_set_repo.contains?(@id)
434
+ end
435
+
436
+ def clear
437
+ @id_set_repo.delete_id(@id)
438
+ end
439
+ end
440
+ end
441
+ end