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.
- checksums.yaml +7 -0
- data/README.md +573 -0
- data/lib/hold.rb +14 -0
- data/lib/hold/file/hash_repository.rb +59 -0
- data/lib/hold/in_memory.rb +184 -0
- data/lib/hold/interfaces.rb +441 -0
- data/lib/hold/sequel.rb +41 -0
- data/lib/hold/sequel/dataset_lazy_array.rb +33 -0
- data/lib/hold/sequel/identity_set_repository.rb +565 -0
- data/lib/hold/sequel/polymorphic_repository.rb +121 -0
- data/lib/hold/sequel/property_mapper.rb +138 -0
- data/lib/hold/sequel/property_mapper/array.rb +62 -0
- data/lib/hold/sequel/property_mapper/column.rb +42 -0
- data/lib/hold/sequel/property_mapper/created_at.rb +14 -0
- data/lib/hold/sequel/property_mapper/custom_query.rb +34 -0
- data/lib/hold/sequel/property_mapper/custom_query_single_value.rb +36 -0
- data/lib/hold/sequel/property_mapper/foreign_key.rb +96 -0
- data/lib/hold/sequel/property_mapper/hash.rb +60 -0
- data/lib/hold/sequel/property_mapper/identity.rb +41 -0
- data/lib/hold/sequel/property_mapper/many_to_many.rb +158 -0
- data/lib/hold/sequel/property_mapper/one_to_many.rb +199 -0
- data/lib/hold/sequel/property_mapper/transformed_column.rb +38 -0
- data/lib/hold/sequel/property_mapper/updated_at.rb +17 -0
- data/lib/hold/sequel/query.rb +92 -0
- data/lib/hold/sequel/query_array_cell.rb +21 -0
- data/lib/hold/sequel/repository_observer.rb +28 -0
- data/lib/hold/sequel/with_polymorphic_type_column.rb +117 -0
- data/lib/hold/serialized.rb +104 -0
- data/lib/hold/serialized/json_serializer.rb +12 -0
- data/lib/hold/version.rb +3 -0
- metadata +199 -0
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
|