Class: StrokeDB::Document

Inherits:
Object show all
Includes:
Validations::InstanceMethods
Defined in:
lib/document/document.rb,
lib/document/delete.rb,
lib/document/versions.rb

Overview

Document is one of the core classes. It is being used to represent database document.

Database document is an entity that:

  • is uniquely identified with UUID

  • has a number of slots, where each slot is a key-value pair (whereas pair could be a JSON object)

Here is a simplistic example of document:

1e3d02cc-0769-4bd8-9113-e033b246b013:

name: "My Document"
language: "English"
authors: ["Yurii Rashkovskii","Oleg Andreev"]

Defined Under Namespace

Classes: Metas, Versions

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Validations::InstanceMethods

#errors, #valid?

Constructor Details

#initialize(*args, &block) ⇒ Document

Instantiates new document

Here are few ways to call it:

Document.new(:slot_1 => slot_1_value, :slot_2 => slot_2_value)

This way new document with slots slot_1 and slot_2 will be initialized in the default store.

Document.new(store,:slot_1 => slot_1_value, :slot_2 => slot_2_value)

This way new document with slots slot_1 and slot_2 will be initialized in the given store.

Document.new({:slot_1 => slot_1_value, :slot_2 => slot_2_value},uuid)

where uuid is a string with UUID. WARNING: this way of initializing Document should not be used unless you know what are you doing!



152
153
154
155
156
157
158
159
160
161
# File 'lib/document/document.rb', line 152

def initialize(*args, &block)
  @initialization_block = block

  if args.first.is_a?(Hash) || args.empty?
    raise NoDefaultStoreError unless StrokeDB.default_store
    do_initialize(StrokeDB.default_store, *args)
  else
    do_initialize(*args)
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args) ⇒ Object

:nodoc:

Raises:



527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'lib/document/document.rb', line 527

def method_missing(sym, *args) #:nodoc:
  sym = sym.to_s
  
  return send(:[]=, sym.chomp('='), *args) if sym.ends_with? '='
  return self[sym]                         if slotnames.include? sym
  return !!send(sym.chomp('?'), *args)     if sym.ends_with? '?'
        
  raise SlotNotFoundError.new(sym) if (callbacks['when_slot_not_found'] || []).empty?

  r = execute_callbacks(:when_slot_not_found, sym)
  raise r if r.is_a? SlotNotFoundError # TODO: spec this behavior
 
  r
end

Instance Attribute Details

#callbacksObject (readonly)

:nodoc:



67
68
69
# File 'lib/document/document.rb', line 67

def callbacks
  @callbacks
end

#storeObject (readonly)

:nodoc:



67
68
69
# File 'lib/document/document.rb', line 67

def store
  @store
end

Class Method Details

.create!(*args, &block) ⇒ Object

Instantiates new document with given arguments (which are the same as in Document#new), and saves it right away



128
129
130
# File 'lib/document/document.rb', line 128

def self.create!(*args, &block)
  new(*args, &block).save!
end

.find(*args) ⇒ Object

Find document(s) by:

a) UUID

Document.find(uuid)

b) search query

Document.find(:slot => "value")

If first argument is Store, that particular store will be used; otherwise default store will be assumed.



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/document/document.rb', line 331

def self.find(*args)
  store = nil
  if args.empty? || args.first.is_a?(String) || args.first.is_a?(Hash)
    store = StrokeDB.default_store
  else
    store = args.shift
  end
  raise NoDefaultStoreError.new unless store
  query = args.first
  case query
  when UUID_RE
    store.find(query)
  when Hash
    store.search(query)
  else
    raise TypeError
  end
end

.from_raw(store, raw_slots, opts = {}) ⇒ Object

Creates a document from a serialized representation



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/document/document.rb', line 301

def self.from_raw(store, raw_slots, opts = {}) #:nodoc:
  doc = new(store, raw_slots, true)

  collect_meta_modules(store, raw_slots['meta']).each do |meta_module|
    unless doc.is_a? meta_module
      doc.extend(meta_module)

      meta_module.send!(:setup_callbacks, doc) rescue nil
    end
  end

  unless opts[:skip_callbacks]
    doc.send! :execute_callbacks, :on_initialization
    doc.send! :execute_callbacks, :on_load
  end
  doc
end

Instance Method Details

#+(document) ⇒ Object

Instantiate a composite document



443
444
445
446
447
# File 'lib/document/document.rb', line 443

def +(document)
  original, target = [to_raw, document.to_raw].map{ |raw| raw.except(*%w(uuid version previous_version)) }

  Document.new(@store, original.merge(target).merge(:uuid => Util.random_uuid), true)
end

#==(doc) ⇒ Object

:nodoc:



497
498
499
500
501
502
503
504
505
506
507
# File 'lib/document/document.rb', line 497

def ==(doc) #:nodoc:
  case doc
  when Document, DocumentReferenceValue
    doc = doc.load if doc.kind_of? DocumentReferenceValue
    
    # we make a quick UUID check here to skip two heavy to_raw calls
    doc.uuid == uuid && doc.to_raw == to_raw
  else
    false
  end
end

#[](slotname) ⇒ Object

Get slot value by its name:

document[:slot_1]

If slot was not found, it will return nil



170
171
172
# File 'lib/document/document.rb', line 170

def [](slotname)
  @slots[slotname.to_s].value rescue nil
end

#[]=(slotname, value) ⇒ Object

Set slot value by its name:

document[:slot_1] = "some value"


179
180
181
182
183
184
185
186
# File 'lib/document/document.rb', line 179

def []=(slotname, value)
  slotname = slotname.to_s

  (@slots[slotname] ||= Slot.new(self, slotname)).value = value
  update_version!(slotname)
  
  value
end

#__reference__Object

:nodoc:



493
494
495
# File 'lib/document/document.rb', line 493

def __reference__ #:nodoc:
  "@##{uuid}.#{version}"
end

#add_callback(cbk) ⇒ Object

:nodoc:



542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/document/document.rb', line 542

def add_callback(cbk) #:nodoc:
  name, uid = cbk.name, cbk.uid

  callbacks[name] ||= []

  # if uid is specified, previous callback with the same uid is deleted
  if uid && old_cb = callbacks[name].find{ |cb| cb.uid == uid }
    callbacks[name].delete old_cb
  end

  callbacks[name] << cbk
end

#delete!Object



20
21
22
23
24
25
# File 'lib/document/delete.rb', line 20

def delete!
  raise DocumentDeletionError, "can't delete non-head document" unless head?
  metas << DeletedDocument
  save!
  make_immutable!
end

#diff(from) ⇒ Object

Creates Diff document from from document to this document

document.diff(original_document) #=> #<StrokeDB::Diff added_slots: {"b"=>2}, from: #<Doc a: 1>, removed_slots: {"a"=>1}, to: #<Doc b: 2>, updated_slots: {}>


230
231
232
# File 'lib/document/document.rb', line 230

def diff(from)
  Diff.new(store, :from => from, :to => self)
end

#eql?(doc) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


509
510
511
# File 'lib/document/document.rb', line 509

def eql?(doc) #:nodoc:
  self == doc
end

#has_slot?(slotname) ⇒ Boolean

Checks slot presence. Unlike Document#slotnames it allows you to find even ‘virtual slots’ that could be computed runtime by associations or when_slot_found callbacks

document.has_slot?(:slotname)

Returns:

  • (Boolean)


194
195
196
197
198
199
200
# File 'lib/document/document.rb', line 194

def has_slot?(slotname)
  v = send(slotname)

  (v.nil? && slotnames.include?(slotname.to_s)) ? true : !!v
rescue SlotNotFoundError
  false
end

#hashObject

documents are hashed by their UUID



514
515
516
# File 'lib/document/document.rb', line 514

def hash #:nodoc:
  uuid.hash
end

#head?Boolean

Returns true if this document is a latest version of document being saved to a respective store

Returns:

  • (Boolean)


368
369
370
371
# File 'lib/document/document.rb', line 368

def head?
  return false if new? || is_a?(VersionedDocument)
  store.head_version(uuid) == version
end

#make_immutable!Object



518
519
520
521
# File 'lib/document/document.rb', line 518

def make_immutable!
  extend ImmutableDocument
  self
end

#marshal_dumpObject

:nodoc:



69
70
71
# File 'lib/document/document.rb', line 69

def marshal_dump #:nodoc:
  (@new ? '1' : '0') + (@saved ? '1' : '0') + to_raw.to_json
end

#marshal_load(content) ⇒ Object

:nodoc:



73
74
75
76
77
78
# File 'lib/document/document.rb', line 73

def marshal_load(content) #:nodoc:
  @callbacks = {}
  initialize_raw_slots(JSON.parse(content[2,content.length]))
  @saved = content[1,1] == '1'
  @new = content[0,1] == '1'
end

#metaObject

Returns document’s metadocument (if any). In case if document has more than one metadocument, it will combine all metadocuments into one ‘virtual’ metadocument



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/document/document.rb', line 417

def meta
  unless (m = self[:meta]).kind_of? Array
    # simple case
    return m || Document.new(@store)
  end
  
  return m.first if m.size == 1

  mm = m.clone
  collected_meta = mm.shift.clone

  names = collected_meta[:name].split(',') rescue []
  
  mm.each do |next_meta|
    next_meta = next_meta.clone
    collected_meta += next_meta
    names << next_meta.name if next_meta[:name]
  end

  collected_meta.name = names.uniq.join(',')
  collected_meta.make_immutable!
end

#metasObject

Should be used to add metadocuments on the fly:

document.metas << Buyer
document.metas << Buyer.document

Please not that it accept both meta modules and their documents, there is no difference



457
458
459
# File 'lib/document/document.rb', line 457

def metas
  Metas.new(self)
end

#mutable?Boolean

Returns:

  • (Boolean)


523
524
525
# File 'lib/document/document.rb', line 523

def mutable?
  true
end

#new?Boolean

Returns true if this is a document that has never been saved.

Returns:

  • (Boolean)


360
361
362
# File 'lib/document/document.rb', line 360

def new?
  !!@new
end

#pretty_printObject Also known as: to_s, inspect

:nodoc:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/document/document.rb', line 234

def pretty_print #:nodoc:
  slots = to_raw.except('meta')
  
  s = is_a?(ImmutableDocument) ? "#<(imm)" : "#<"
  
  Util.catch_circular_reference(self) do
    if self[:meta] && name = meta[:name]
      s << "#{name} "
    else
      s << "Doc "
    end

    slots.keys.sort.each do |k|
      if %w(version previous_version).member?(k) && v = self[k]
        s << "#{k}: #{v.gsub(/^(0)+/,'')[0,4]}..., "
      else
        s << "#{k}: #{self[k].inspect}, "
      end
    end

    s.chomp!(', ')
    s.chomp!(' ')
    s << ">"
  end
  
  s
rescue Util::CircularReferenceCondition
  "#(#{(self[:meta] ? "#{meta}" : "Doc")} #{('@#'+uuid)[0,5]}...)"
end

#previous_versionObject

Returns document’s previous version (which is stored in previous_version slot)



478
479
480
# File 'lib/document/document.rb', line 478

def previous_version
  self[:previous_version]
end

#reloadObject

Reloads head of the same document from store. All unsaved changes will be lost!



353
354
355
# File 'lib/document/document.rb', line 353

def reload
  new? ? self : store.find(uuid)
end

#remove_slot!(slotname) ⇒ Object

Removes slot

document.remove_slot!(:slotname)


207
208
209
210
211
212
213
214
# File 'lib/document/document.rb', line 207

def remove_slot!(slotname)
  slotname = slotname.to_s
  
  @slots.delete slotname
  update_version! slotname

  nil
end

#save!(perform_validation = true) ⇒ Object

Saves the document. If validations do not pass, InvalidDocumentError exception is raised.



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/document/document.rb', line 377

def save!(perform_validation = true)
  execute_callbacks :before_save

  if perform_validation
    raise InvalidDocumentError.new(self) unless valid?
  end

  execute_callbacks :after_validation

  store.save!(self)
  @new = false
  @saved = true
 
  execute_callbacks :after_save
  
  self
end

#slotnamesObject

Returns an Array of explicitely defined slots

document.slotnames #=> ["version","name","language","authors"]


221
222
223
# File 'lib/document/document.rb', line 221

def slotnames
  @slots.keys
end

#to_jsonObject

Returns string with Document’s JSON representation



270
271
272
# File 'lib/document/document.rb', line 270

def to_json
  to_raw.to_json
end

#to_optimized_rawObject

:nodoc:



294
295
296
# File 'lib/document/document.rb', line 294

def to_optimized_raw #:nodoc:
  __reference__
end

#to_rawObject

Primary serialization



284
285
286
287
288
289
290
291
292
# File 'lib/document/document.rb', line 284

def to_raw #:nodoc:
  raw_slots = {}

  @slots.each_pair do |k,v|
    raw_slots[k.to_s] = v.to_raw
  end
  
  raw_slots
end

#to_xml(opts = {}) ⇒ Object

Returns string with Document’s XML representation



277
278
279
# File 'lib/document/document.rb', line 277

def to_xml(opts = {})
  to_raw.to_xml({ :root => 'document', :dasherize => true }.merge(opts))
end

#update_slots(hash) ⇒ Object

Updates slots with specified hash and returns itself.



398
399
400
401
402
403
404
# File 'lib/document/document.rb', line 398

def update_slots(hash)
  hash.each do |k, v|
    self[k] = v
  end
  
  self
end

#update_slots!(hash) ⇒ Object

Same as update_slots, but also saves the document.



409
410
411
# File 'lib/document/document.rb', line 409

def update_slots!(hash)
  update_slots(hash).save!
end

#uuidObject

Return document’s uuid



471
472
473
# File 'lib/document/document.rb', line 471

def uuid
  @uuid ||= self[:uuid]
end

#versionObject

Returns document’s version (which is stored in version slot)



464
465
466
# File 'lib/document/document.rb', line 464

def version
  self[:version]
end

#version=(v) ⇒ Object

:nodoc:



482
483
484
# File 'lib/document/document.rb', line 482

def version=(v) #:nodoc:
  self[:version] = v
end

#versionsObject

Returns an instance of Document::Versions



489
490
491
# File 'lib/document/document.rb', line 489

def versions
  @versions ||= Versions.new(self)
end