strokedb 0.0.2
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/CONTRIBUTORS +7 -0
- data/CREDITS +13 -0
- data/README +44 -0
- data/bin/sdbc +2 -0
- data/lib/config/config.rb +161 -0
- data/lib/data_structures/inverted_list.rb +297 -0
- data/lib/data_structures/point_query.rb +24 -0
- data/lib/data_structures/skiplist.rb +302 -0
- data/lib/document/associations.rb +107 -0
- data/lib/document/callback.rb +11 -0
- data/lib/document/coercions.rb +57 -0
- data/lib/document/delete.rb +28 -0
- data/lib/document/document.rb +684 -0
- data/lib/document/meta.rb +261 -0
- data/lib/document/slot.rb +199 -0
- data/lib/document/util.rb +27 -0
- data/lib/document/validations.rb +704 -0
- data/lib/document/versions.rb +106 -0
- data/lib/document/virtualize.rb +82 -0
- data/lib/init.rb +57 -0
- data/lib/stores/chainable_storage.rb +57 -0
- data/lib/stores/inverted_list_index/inverted_list_file_storage.rb +56 -0
- data/lib/stores/inverted_list_index/inverted_list_index.rb +49 -0
- data/lib/stores/remote_store.rb +172 -0
- data/lib/stores/skiplist_store/chunk.rb +119 -0
- data/lib/stores/skiplist_store/chunk_storage.rb +21 -0
- data/lib/stores/skiplist_store/file_chunk_storage.rb +44 -0
- data/lib/stores/skiplist_store/memory_chunk_storage.rb +37 -0
- data/lib/stores/skiplist_store/skiplist_store.rb +217 -0
- data/lib/stores/store.rb +5 -0
- data/lib/sync/chain_sync.rb +38 -0
- data/lib/sync/diff.rb +126 -0
- data/lib/sync/lamport_timestamp.rb +81 -0
- data/lib/sync/store_sync.rb +79 -0
- data/lib/sync/stroke_diff/array.rb +102 -0
- data/lib/sync/stroke_diff/default.rb +21 -0
- data/lib/sync/stroke_diff/hash.rb +186 -0
- data/lib/sync/stroke_diff/string.rb +116 -0
- data/lib/sync/stroke_diff/stroke_diff.rb +9 -0
- data/lib/util/blankslate.rb +42 -0
- data/lib/util/ext/blank.rb +50 -0
- data/lib/util/ext/enumerable.rb +36 -0
- data/lib/util/ext/fixnum.rb +16 -0
- data/lib/util/ext/hash.rb +22 -0
- data/lib/util/ext/object.rb +8 -0
- data/lib/util/ext/string.rb +35 -0
- data/lib/util/inflect.rb +217 -0
- data/lib/util/java_util.rb +9 -0
- data/lib/util/lazy_array.rb +54 -0
- data/lib/util/lazy_mapping_array.rb +64 -0
- data/lib/util/lazy_mapping_hash.rb +46 -0
- data/lib/util/serialization.rb +29 -0
- data/lib/util/trigger_partition.rb +136 -0
- data/lib/util/util.rb +38 -0
- data/lib/util/xml.rb +6 -0
- data/lib/view/view.rb +55 -0
- data/script/console +70 -0
- data/strokedb.rb +75 -0
- metadata +148 -0
@@ -0,0 +1,684 @@
|
|
1
|
+
module StrokeDB
|
2
|
+
# Slots which contain references to another documents are matched
|
3
|
+
# with these regexps.
|
4
|
+
DOCREF = /^@##{UUID_RE}$/
|
5
|
+
VERSIONREF = /^@##{UUID_RE}\.#{VERSION_RE}$/
|
6
|
+
|
7
|
+
#
|
8
|
+
# Raised on unexisting document access.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# document.slot_that_does_not_exist_ever
|
13
|
+
#
|
14
|
+
class SlotNotFoundError < StandardError
|
15
|
+
attr_reader :slotname
|
16
|
+
|
17
|
+
def initialize(slotname)
|
18
|
+
@slotname = slotname
|
19
|
+
end
|
20
|
+
|
21
|
+
def message
|
22
|
+
"Can't find slot #{@slotname}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"#<#{self.class.name}: #{message}>"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Raised when Document#save! is called on an invalid document
|
32
|
+
# (for which doc.valid? returns false)
|
33
|
+
#
|
34
|
+
class InvalidDocumentError < StandardError #:nodoc:
|
35
|
+
attr_reader :document
|
36
|
+
|
37
|
+
def initialize(document)
|
38
|
+
@document = document
|
39
|
+
end
|
40
|
+
|
41
|
+
def message
|
42
|
+
"Validation failed: #{@document.errors.messages.join(", ")}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect
|
46
|
+
"#<#{self.class.name}: #{message}>"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Document is one of the core classes. It is being used to represent database document.
|
51
|
+
#
|
52
|
+
# Database document is an entity that:
|
53
|
+
#
|
54
|
+
# * is uniquely identified with UUID
|
55
|
+
# * has a number of slots, where each slot is a key-value pair (whereas pair could be a JSON object)
|
56
|
+
#
|
57
|
+
# Here is a simplistic example of document:
|
58
|
+
#
|
59
|
+
# <tt>1e3d02cc-0769-4bd8-9113-e033b246b013:</tt>
|
60
|
+
# name: "My Document"
|
61
|
+
# language: "English"
|
62
|
+
# authors: ["Yurii Rashkovskii","Oleg Andreev"]
|
63
|
+
#
|
64
|
+
class Document
|
65
|
+
include Validations::InstanceMethods
|
66
|
+
|
67
|
+
attr_reader :store, :callbacks #:nodoc:
|
68
|
+
|
69
|
+
def marshal_dump #:nodoc:
|
70
|
+
(@new ? '1' : '0') + (@saved ? '1' : '0') + to_raw.to_json
|
71
|
+
end
|
72
|
+
|
73
|
+
def marshal_load(content) #:nodoc:
|
74
|
+
@callbacks = {}
|
75
|
+
initialize_raw_slots(JSON.parse(content[2,content.length]))
|
76
|
+
@saved = content[1,1] == '1'
|
77
|
+
@new = content[0,1] == '1'
|
78
|
+
end
|
79
|
+
|
80
|
+
# Collection of meta documents
|
81
|
+
class Metas < Array #:nodoc:
|
82
|
+
def initialize(document)
|
83
|
+
@document = document
|
84
|
+
_meta = document[:meta]
|
85
|
+
concat [_meta].flatten.compact.map{|v| v.is_a?(DocumentReferenceValue) ? v.load : v}
|
86
|
+
end
|
87
|
+
|
88
|
+
def <<(meta)
|
89
|
+
add_meta(meta, :call_initialization_callbacks => true)
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_meta(meta, opts = {})
|
93
|
+
opts = opts.stringify_keys
|
94
|
+
_module = nil
|
95
|
+
|
96
|
+
# meta can be specified both as a meta document and as a module
|
97
|
+
case meta
|
98
|
+
when Document
|
99
|
+
push meta
|
100
|
+
_module = StrokeDB::Document.collect_meta_modules(@document.store, meta).first
|
101
|
+
when Meta
|
102
|
+
push meta.document(@document.store)
|
103
|
+
_module = meta
|
104
|
+
else
|
105
|
+
raise ArgumentError, "Meta should be either document or meta module"
|
106
|
+
end
|
107
|
+
|
108
|
+
# register meta in the document
|
109
|
+
@document[:meta] = self
|
110
|
+
|
111
|
+
if _module
|
112
|
+
@document.extend(_module)
|
113
|
+
|
114
|
+
_module.send!(:setup_callbacks, @document) rescue nil
|
115
|
+
|
116
|
+
if opts['call_initialization_callbacks']
|
117
|
+
@document.send!(:execute_callbacks_for, _module, :on_initialization)
|
118
|
+
@document.send!(:execute_callbacks_for, _module, :on_new_document) if @document.new?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Instantiates new document with given arguments (which are the same as in Document#new),
|
126
|
+
# and saves it right away
|
127
|
+
#
|
128
|
+
def self.create!(*args, &block)
|
129
|
+
new(*args, &block).save!
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Instantiates new document
|
134
|
+
#
|
135
|
+
# Here are few ways to call it:
|
136
|
+
#
|
137
|
+
# Document.new(:slot_1 => slot_1_value, :slot_2 => slot_2_value)
|
138
|
+
#
|
139
|
+
# This way new document with slots <tt>slot_1</tt> and <tt>slot_2</tt> will be initialized in the
|
140
|
+
# default store.
|
141
|
+
#
|
142
|
+
# Document.new(store,:slot_1 => slot_1_value, :slot_2 => slot_2_value)
|
143
|
+
#
|
144
|
+
# This way new document with slots <tt>slot_1</tt> and <tt>slot_2</tt> will be initialized in the
|
145
|
+
# given <tt>store</tt>.
|
146
|
+
#
|
147
|
+
# Document.new({:slot_1 => slot_1_value, :slot_2 => slot_2_value},uuid)
|
148
|
+
#
|
149
|
+
# where <tt>uuid</tt> is a string with UUID. *WARNING*: this way of initializing Document should not
|
150
|
+
# be used unless you know what are you doing!
|
151
|
+
#
|
152
|
+
def initialize(*args, &block)
|
153
|
+
@initialization_block = block
|
154
|
+
|
155
|
+
if args.first.is_a?(Hash) || args.empty?
|
156
|
+
raise NoDefaultStoreError unless StrokeDB.default_store
|
157
|
+
do_initialize(StrokeDB.default_store, *args)
|
158
|
+
else
|
159
|
+
do_initialize(*args)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
# Get slot value by its name:
|
165
|
+
#
|
166
|
+
# document[:slot_1]
|
167
|
+
#
|
168
|
+
# If slot was not found, it will return <tt>nil</tt>
|
169
|
+
#
|
170
|
+
def [](slotname)
|
171
|
+
@slots[slotname.to_s].value rescue nil
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# Set slot value by its name:
|
176
|
+
#
|
177
|
+
# document[:slot_1] = "some value"
|
178
|
+
#
|
179
|
+
def []=(slotname, value)
|
180
|
+
slotname = slotname.to_s
|
181
|
+
|
182
|
+
(@slots[slotname] ||= Slot.new(self, slotname)).value = value
|
183
|
+
update_version!(slotname)
|
184
|
+
|
185
|
+
value
|
186
|
+
end
|
187
|
+
|
188
|
+
#
|
189
|
+
# Checks slot presence. Unlike Document#slotnames it allows you to find even 'virtual slots' that could be
|
190
|
+
# computed runtime by associations or <tt>when_slot_found</tt> callbacks
|
191
|
+
#
|
192
|
+
# document.has_slot?(:slotname)
|
193
|
+
#
|
194
|
+
def has_slot?(slotname)
|
195
|
+
v = send(slotname)
|
196
|
+
|
197
|
+
(v.nil? && slotnames.include?(slotname.to_s)) ? true : !!v
|
198
|
+
rescue SlotNotFoundError
|
199
|
+
false
|
200
|
+
end
|
201
|
+
|
202
|
+
#
|
203
|
+
# Removes slot
|
204
|
+
#
|
205
|
+
# document.remove_slot!(:slotname)
|
206
|
+
#
|
207
|
+
def remove_slot!(slotname)
|
208
|
+
slotname = slotname.to_s
|
209
|
+
|
210
|
+
@slots.delete slotname
|
211
|
+
update_version! slotname
|
212
|
+
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
|
216
|
+
#
|
217
|
+
# Returns an <tt>Array</tt> of explicitely defined slots
|
218
|
+
#
|
219
|
+
# document.slotnames #=> ["version","name","language","authors"]
|
220
|
+
#
|
221
|
+
def slotnames
|
222
|
+
@slots.keys
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Creates Diff document from <tt>from</tt> document to this document
|
227
|
+
#
|
228
|
+
# document.diff(original_document) #=> #<StrokeDB::Diff added_slots: {"b"=>2}, from: #<Doc a: 1>, removed_slots: {"a"=>1}, to: #<Doc b: 2>, updated_slots: {}>
|
229
|
+
#
|
230
|
+
def diff(from)
|
231
|
+
Diff.new(store, :from => from, :to => self)
|
232
|
+
end
|
233
|
+
|
234
|
+
def pretty_print #:nodoc:
|
235
|
+
slots = to_raw.except('meta')
|
236
|
+
|
237
|
+
s = is_a?(ImmutableDocument) ? "#<(imm)" : "#<"
|
238
|
+
|
239
|
+
Util.catch_circular_reference(self) do
|
240
|
+
if self[:meta] && name = meta[:name]
|
241
|
+
s << "#{name} "
|
242
|
+
else
|
243
|
+
s << "Doc "
|
244
|
+
end
|
245
|
+
|
246
|
+
slots.keys.sort.each do |k|
|
247
|
+
if %w(version previous_version).member?(k) && v = self[k]
|
248
|
+
s << "#{k}: #{v.gsub(/^(0)+/,'')[0,4]}..., "
|
249
|
+
else
|
250
|
+
s << "#{k}: #{self[k].inspect}, "
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
s.chomp!(', ')
|
255
|
+
s.chomp!(' ')
|
256
|
+
s << ">"
|
257
|
+
end
|
258
|
+
|
259
|
+
s
|
260
|
+
rescue Util::CircularReferenceCondition
|
261
|
+
"#(#{(self[:meta] ? "#{meta}" : "Doc")} #{('@#'+uuid)[0,5]}...)"
|
262
|
+
end
|
263
|
+
|
264
|
+
alias :to_s :pretty_print
|
265
|
+
alias :inspect :pretty_print
|
266
|
+
|
267
|
+
#
|
268
|
+
# Returns string with Document's JSON representation
|
269
|
+
#
|
270
|
+
def to_json
|
271
|
+
to_raw.to_json
|
272
|
+
end
|
273
|
+
|
274
|
+
#
|
275
|
+
# Returns string with Document's XML representation
|
276
|
+
#
|
277
|
+
def to_xml(opts = {})
|
278
|
+
to_raw.to_xml({ :root => 'document', :dasherize => true }.merge(opts))
|
279
|
+
end
|
280
|
+
|
281
|
+
#
|
282
|
+
# Primary serialization
|
283
|
+
#
|
284
|
+
def to_raw #:nodoc:
|
285
|
+
raw_slots = {}
|
286
|
+
|
287
|
+
@slots.each_pair do |k,v|
|
288
|
+
raw_slots[k.to_s] = v.to_raw
|
289
|
+
end
|
290
|
+
|
291
|
+
raw_slots
|
292
|
+
end
|
293
|
+
|
294
|
+
def to_optimized_raw #:nodoc:
|
295
|
+
__reference__
|
296
|
+
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# Creates a document from a serialized representation
|
300
|
+
#
|
301
|
+
def self.from_raw(store, raw_slots, opts = {}) #:nodoc:
|
302
|
+
doc = new(store, raw_slots, true)
|
303
|
+
|
304
|
+
collect_meta_modules(store, raw_slots['meta']).each do |meta_module|
|
305
|
+
unless doc.is_a? meta_module
|
306
|
+
doc.extend(meta_module)
|
307
|
+
|
308
|
+
meta_module.send!(:setup_callbacks, doc) rescue nil
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
unless opts[:skip_callbacks]
|
313
|
+
doc.send! :execute_callbacks, :on_initialization
|
314
|
+
doc.send! :execute_callbacks, :on_load
|
315
|
+
end
|
316
|
+
doc
|
317
|
+
end
|
318
|
+
|
319
|
+
#
|
320
|
+
# Find document(s) by:
|
321
|
+
#
|
322
|
+
# a) UUID
|
323
|
+
#
|
324
|
+
# Document.find(uuid)
|
325
|
+
#
|
326
|
+
# b) search query
|
327
|
+
#
|
328
|
+
# Document.find(:slot => "value")
|
329
|
+
#
|
330
|
+
# If first argument is Store, that particular store will be used; otherwise default store will be assumed.
|
331
|
+
def self.find(*args)
|
332
|
+
store = nil
|
333
|
+
if args.empty? || args.first.is_a?(String) || args.first.is_a?(Hash)
|
334
|
+
store = StrokeDB.default_store
|
335
|
+
else
|
336
|
+
store = args.shift
|
337
|
+
end
|
338
|
+
raise NoDefaultStoreError.new unless store
|
339
|
+
query = args.first
|
340
|
+
case query
|
341
|
+
when UUID_RE
|
342
|
+
store.find(query)
|
343
|
+
when Hash
|
344
|
+
store.search(query)
|
345
|
+
else
|
346
|
+
raise TypeError
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
#
|
351
|
+
# Reloads head of the same document from store. All unsaved changes will be lost!
|
352
|
+
#
|
353
|
+
def reload
|
354
|
+
new? ? self : store.find(uuid)
|
355
|
+
end
|
356
|
+
|
357
|
+
#
|
358
|
+
# Returns <tt>true</tt> if this is a document that has never been saved.
|
359
|
+
#
|
360
|
+
def new?
|
361
|
+
!!@new
|
362
|
+
end
|
363
|
+
|
364
|
+
#
|
365
|
+
# Returns <tt>true</tt> if this document is a latest version of document being saved to a respective
|
366
|
+
# store
|
367
|
+
#
|
368
|
+
def head?
|
369
|
+
return false if new? || is_a?(VersionedDocument)
|
370
|
+
store.head_version(uuid) == version
|
371
|
+
end
|
372
|
+
|
373
|
+
#
|
374
|
+
# Saves the document. If validations do not pass, InvalidDocumentError
|
375
|
+
# exception is raised.
|
376
|
+
#
|
377
|
+
def save!(perform_validation = true)
|
378
|
+
execute_callbacks :before_save
|
379
|
+
|
380
|
+
if perform_validation
|
381
|
+
raise InvalidDocumentError.new(self) unless valid?
|
382
|
+
end
|
383
|
+
|
384
|
+
execute_callbacks :after_validation
|
385
|
+
|
386
|
+
store.save!(self)
|
387
|
+
@new = false
|
388
|
+
@saved = true
|
389
|
+
|
390
|
+
execute_callbacks :after_save
|
391
|
+
|
392
|
+
self
|
393
|
+
end
|
394
|
+
|
395
|
+
#
|
396
|
+
# Updates slots with specified <tt>hash</tt> and returns itself.
|
397
|
+
#
|
398
|
+
def update_slots(hash)
|
399
|
+
hash.each do |k, v|
|
400
|
+
self[k] = v
|
401
|
+
end
|
402
|
+
|
403
|
+
self
|
404
|
+
end
|
405
|
+
|
406
|
+
#
|
407
|
+
# Same as update_slots, but also saves the document.
|
408
|
+
#
|
409
|
+
def update_slots!(hash)
|
410
|
+
update_slots(hash).save!
|
411
|
+
end
|
412
|
+
|
413
|
+
#
|
414
|
+
# Returns document's metadocument (if any). In case if document has more than one metadocument,
|
415
|
+
# it will combine all metadocuments into one 'virtual' metadocument
|
416
|
+
#
|
417
|
+
def meta
|
418
|
+
unless (m = self[:meta]).kind_of? Array
|
419
|
+
# simple case
|
420
|
+
return m || Document.new(@store)
|
421
|
+
end
|
422
|
+
|
423
|
+
return m.first if m.size == 1
|
424
|
+
|
425
|
+
mm = m.clone
|
426
|
+
collected_meta = mm.shift.clone
|
427
|
+
|
428
|
+
names = collected_meta[:name].split(',') rescue []
|
429
|
+
|
430
|
+
mm.each do |next_meta|
|
431
|
+
next_meta = next_meta.clone
|
432
|
+
collected_meta += next_meta
|
433
|
+
names << next_meta.name if next_meta[:name]
|
434
|
+
end
|
435
|
+
|
436
|
+
collected_meta.name = names.uniq.join(',')
|
437
|
+
collected_meta.make_immutable!
|
438
|
+
end
|
439
|
+
|
440
|
+
#
|
441
|
+
# Instantiate a composite document
|
442
|
+
#
|
443
|
+
def +(document)
|
444
|
+
original, target = [to_raw, document.to_raw].map{ |raw| raw.except(*%w(uuid version previous_version)) }
|
445
|
+
|
446
|
+
Document.new(@store, original.merge(target).merge(:uuid => Util.random_uuid), true)
|
447
|
+
end
|
448
|
+
|
449
|
+
#
|
450
|
+
# Should be used to add metadocuments on the fly:
|
451
|
+
#
|
452
|
+
# document.metas << Buyer
|
453
|
+
# document.metas << Buyer.document
|
454
|
+
#
|
455
|
+
# Please not that it accept both meta modules and their documents, there is no difference
|
456
|
+
#
|
457
|
+
def metas
|
458
|
+
Metas.new(self)
|
459
|
+
end
|
460
|
+
|
461
|
+
#
|
462
|
+
# Returns document's version (which is stored in <tt>version</tt> slot)
|
463
|
+
#
|
464
|
+
def version
|
465
|
+
self[:version]
|
466
|
+
end
|
467
|
+
|
468
|
+
#
|
469
|
+
# Return document's uuid
|
470
|
+
#
|
471
|
+
def uuid
|
472
|
+
@uuid ||= self[:uuid]
|
473
|
+
end
|
474
|
+
|
475
|
+
#
|
476
|
+
# Returns document's previous version (which is stored in <tt>previous_version</tt> slot)
|
477
|
+
#
|
478
|
+
def previous_version
|
479
|
+
self[:previous_version]
|
480
|
+
end
|
481
|
+
|
482
|
+
def version=(v) #:nodoc:
|
483
|
+
self[:version] = v
|
484
|
+
end
|
485
|
+
|
486
|
+
#
|
487
|
+
# Returns an instance of Document::Versions
|
488
|
+
#
|
489
|
+
def versions
|
490
|
+
@versions ||= Versions.new(self)
|
491
|
+
end
|
492
|
+
|
493
|
+
def __reference__ #:nodoc:
|
494
|
+
"@##{uuid}.#{version}"
|
495
|
+
end
|
496
|
+
|
497
|
+
def ==(doc) #:nodoc:
|
498
|
+
case doc
|
499
|
+
when Document, DocumentReferenceValue
|
500
|
+
doc = doc.load if doc.kind_of? DocumentReferenceValue
|
501
|
+
|
502
|
+
# we make a quick UUID check here to skip two heavy to_raw calls
|
503
|
+
doc.uuid == uuid && doc.to_raw == to_raw
|
504
|
+
else
|
505
|
+
false
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def eql?(doc) #:nodoc:
|
510
|
+
self == doc
|
511
|
+
end
|
512
|
+
|
513
|
+
# documents are hashed by their UUID
|
514
|
+
def hash #:nodoc:
|
515
|
+
uuid.hash
|
516
|
+
end
|
517
|
+
|
518
|
+
def make_immutable!
|
519
|
+
extend ImmutableDocument
|
520
|
+
self
|
521
|
+
end
|
522
|
+
|
523
|
+
def mutable?
|
524
|
+
true
|
525
|
+
end
|
526
|
+
|
527
|
+
def method_missing(sym, *args) #:nodoc:
|
528
|
+
sym = sym.to_s
|
529
|
+
|
530
|
+
return send(:[]=, sym.chomp('='), *args) if sym.ends_with? '='
|
531
|
+
return self[sym] if slotnames.include? sym
|
532
|
+
return !!send(sym.chomp('?'), *args) if sym.ends_with? '?'
|
533
|
+
|
534
|
+
raise SlotNotFoundError.new(sym) if (callbacks['when_slot_not_found'] || []).empty?
|
535
|
+
|
536
|
+
r = execute_callbacks(:when_slot_not_found, sym)
|
537
|
+
raise r if r.is_a? SlotNotFoundError # TODO: spec this behavior
|
538
|
+
|
539
|
+
r
|
540
|
+
end
|
541
|
+
|
542
|
+
def add_callback(cbk) #:nodoc:
|
543
|
+
name, uid = cbk.name, cbk.uid
|
544
|
+
|
545
|
+
callbacks[name] ||= []
|
546
|
+
|
547
|
+
# if uid is specified, previous callback with the same uid is deleted
|
548
|
+
if uid && old_cb = callbacks[name].find{ |cb| cb.uid == uid }
|
549
|
+
callbacks[name].delete old_cb
|
550
|
+
end
|
551
|
+
|
552
|
+
callbacks[name] << cbk
|
553
|
+
end
|
554
|
+
|
555
|
+
protected
|
556
|
+
|
557
|
+
# value of the last called callback is returned when executing callbacks
|
558
|
+
# (or nil if none found)
|
559
|
+
def execute_callbacks(name, *args) #:nodoc:
|
560
|
+
(callbacks[name.to_s] || []).inject(nil) do |prev_value, callback|
|
561
|
+
callback.call(self, *args)
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
def execute_callbacks_for(origin, name, *args) #:nodoc:
|
566
|
+
(callbacks[name.to_s] || []).inject(nil) do |prev_value, callback|
|
567
|
+
callback.origin == origin ? callback.call(self, *args) : prev_value
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
# initialize the document. initialize_raw is true when
|
572
|
+
# document is initialized from a raw serialized form
|
573
|
+
def do_initialize(store, slots={}, initialize_raw = false) #:nodoc:
|
574
|
+
@callbacks = {}
|
575
|
+
@store = store
|
576
|
+
|
577
|
+
if initialize_raw
|
578
|
+
initialize_raw_slots slots
|
579
|
+
@saved = true
|
580
|
+
else
|
581
|
+
@new = true
|
582
|
+
initialize_slots slots
|
583
|
+
|
584
|
+
self[:uuid] = Util.random_uuid unless self[:uuid]
|
585
|
+
generate_new_version! unless self[:version]
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
# initialize slots for a new, just created document
|
590
|
+
def initialize_slots(slots) #:nodoc:
|
591
|
+
@slots = {}
|
592
|
+
slots = slots.stringify_keys
|
593
|
+
|
594
|
+
# there is a reason for meta slot is initialized separately —
|
595
|
+
# we need to setup coercions before initializing actual slots
|
596
|
+
if meta = slots['meta']
|
597
|
+
meta = [meta] unless meta.is_a?(Array)
|
598
|
+
meta.each {|m| metas.add_meta(m) }
|
599
|
+
end
|
600
|
+
|
601
|
+
slots.except('meta').each {|name,value| self[name] = value }
|
602
|
+
|
603
|
+
# now, when we have all slots initialized, we can run initialization callbacks
|
604
|
+
execute_callbacks :on_initialization
|
605
|
+
execute_callbacks :on_new_document
|
606
|
+
end
|
607
|
+
|
608
|
+
# initialize slots from a raw representation
|
609
|
+
def initialize_raw_slots(slots) #:nodoc:
|
610
|
+
@slots = {}
|
611
|
+
|
612
|
+
slots.each do |name,value|
|
613
|
+
s = Slot.new(self, name)
|
614
|
+
s.raw_value = value
|
615
|
+
|
616
|
+
@slots[name.to_s] = s
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
# returns an array of meta modules (as constants) for a given something
|
621
|
+
# (a document reference, a document itself, or an array of the former)
|
622
|
+
def self.collect_meta_modules(store, meta) #:nodoc:
|
623
|
+
meta_names = []
|
624
|
+
|
625
|
+
case meta
|
626
|
+
when VERSIONREF
|
627
|
+
if m = store.find($1, $2)
|
628
|
+
meta_names << m[:name]
|
629
|
+
end
|
630
|
+
when DOCREF
|
631
|
+
if m = store.find($1)
|
632
|
+
meta_names << m[:name]
|
633
|
+
end
|
634
|
+
when Array
|
635
|
+
meta_names = meta.map { |m| collect_meta_modules(store, m) }.flatten
|
636
|
+
when Document
|
637
|
+
meta_names << meta[:name]
|
638
|
+
end
|
639
|
+
|
640
|
+
meta_names.map { |m| m.is_a?(String) ? (m.constantize rescue nil) : m }.compact
|
641
|
+
end
|
642
|
+
|
643
|
+
def generate_new_version!
|
644
|
+
self.version = Util.random_uuid
|
645
|
+
end
|
646
|
+
|
647
|
+
def update_version!(slotname)
|
648
|
+
if @saved && slotname != 'version' && slotname != 'previous_version'
|
649
|
+
self[:previous_version] = version unless version.nil?
|
650
|
+
|
651
|
+
generate_new_version!
|
652
|
+
|
653
|
+
@saved = nil
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
#
|
659
|
+
# VersionedDocument is a module that is being added to all document's of specific version.
|
660
|
+
# It should not be accessed directly
|
661
|
+
#
|
662
|
+
module VersionedDocument
|
663
|
+
#
|
664
|
+
# Reloads the same version of the same document from store. All unsaved changes will be lost!
|
665
|
+
#
|
666
|
+
def reload
|
667
|
+
store.find(uuid, version)
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
#
|
672
|
+
# ImmutableDocument can't be saved
|
673
|
+
# It should not be used directly, use Document#make_immutable! instead
|
674
|
+
#
|
675
|
+
module ImmutableDocument
|
676
|
+
def mutable?
|
677
|
+
false
|
678
|
+
end
|
679
|
+
|
680
|
+
def save!
|
681
|
+
self
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|