Class: StrokeDB::Document

Inherits:
Object show all
Includes:
Validations::InstanceMethods
Defined in:
lib/strokedb/document.rb,
lib/strokedb/document/delete.rb,
lib/strokedb/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: MetaModulesCollector, 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!



187
188
189
190
191
192
193
194
195
196
# File 'lib/strokedb/document.rb', line 187

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:



586
587
588
589
590
591
592
593
594
595
596
597
598
599
# File 'lib/strokedb/document.rb', line 586

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:



75
76
77
# File 'lib/strokedb/document.rb', line 75

def callbacks
  @callbacks
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



163
164
165
# File 'lib/strokedb/document.rb', line 163

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.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/strokedb/document.rb', line 370

def self.find(*args)
  store = nil
  if (txns = Thread.current[:strokedb_transactions]) && !txns.nil? && !txns.empty?
    store = txns.last
  else
    if args.empty? || args.first.is_a?(String) || args.first.is_a?(Hash) || args.first.nil?
      store = StrokeDB.default_store
    else
      store = args.shift
    end
  end
  raise NoDefaultStoreError.new unless store
  query = args.first
  case query
  when UUID_RE
    store.find(query)
  when Hash
    store.search(query)
  else
    raise ArgumentError, "use UUID or query to find document(s)"
  end
end

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

Creates a document from a serialized representation



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/strokedb/document.rb', line 342

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

  collect_meta_modules(store, raw_slots['meta']).each do |meta_module|
    unless doc.is_a? meta_module
      doc.extend(meta_module)
    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



498
499
500
501
502
# File 'lib/strokedb/document.rb', line 498

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:



556
557
558
559
560
561
562
563
564
565
566
# File 'lib/strokedb/document.rb', line 556

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



205
206
207
208
# File 'lib/strokedb/document.rb', line 205

def [](slotname)
  slotname = slotname.document.uuid if (slotname.is_a?(Meta) && slotname.is_a?(Module)) || (slotname == Meta)
  @slots[slotname.to_s].value rescue nil
end

#[]=(slotname, value) ⇒ Object

Set slot value by its name:

document[:slot_1] = "some value"


215
216
217
218
219
220
221
222
223
# File 'lib/strokedb/document.rb', line 215

def []=(slotname, value)
  slotname = slotname.document.uuid  if (slotname.is_a?(Meta) && slotname.is_a?(Module)) || (slotname == Meta)
  slotname = slotname.to_s

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

  value
end

#__reference__Object

:nodoc:



552
553
554
# File 'lib/strokedb/document.rb', line 552

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

#add_callback(cbk) ⇒ Object

:nodoc:



601
602
603
604
605
606
607
608
609
610
611
612
# File 'lib/strokedb/document.rb', line 601

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/strokedb/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: {}>


267
268
269
# File 'lib/strokedb/document.rb', line 267

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

#eql?(doc) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


568
569
570
# File 'lib/strokedb/document.rb', line 568

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)


231
232
233
234
235
236
237
# File 'lib/strokedb/document.rb', line 231

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



573
574
575
# File 'lib/strokedb/document.rb', line 573

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)


411
412
413
414
# File 'lib/strokedb/document.rb', line 411

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

#make_immutable!Object



577
578
579
580
# File 'lib/strokedb/document.rb', line 577

def make_immutable!
  extend ImmutableDocument
  self
end

#marshal_dumpObject

:nodoc:



85
86
87
# File 'lib/strokedb/document.rb', line 85

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

#marshal_load(content) ⇒ Object

:nodoc:



89
90
91
92
93
94
# File 'lib/strokedb/document.rb', line 89

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



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'lib/strokedb/document.rb', line 472

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



512
513
514
# File 'lib/strokedb/document.rb', line 512

def metas
  @metas ||= Metas.new(self)
end

#mutable?Boolean

Returns:

  • (Boolean)


582
583
584
# File 'lib/strokedb/document.rb', line 582

def mutable?
  true
end

#new?Boolean

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

Returns:

  • (Boolean)


403
404
405
# File 'lib/strokedb/document.rb', line 403

def new?
  !!@new
end

#pretty_printObject Also known as: to_s, inspect

:nodoc:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/strokedb/document.rb', line 271

def pretty_print #:nodoc:
  slots = to_raw.except('meta')

  s = is_a?(ImmutableDocument) ? "#<^" : "#<"

  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[0,4]}..., "
      else
        if k.match(/^#{UUID_RE}$/)
          s << "[#{store.find(k).name}]: #{self[k].inspect}, " rescue s << "#{k}: #{self[k].inspect}, "
        else
          s << "#{k}: #{self[k].inspect}, "
        end
      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)



537
538
539
# File 'lib/strokedb/document.rb', line 537

def previous_version
  self[:previous_version]
end

#raw_uuidObject

:nodoc:



530
531
532
# File 'lib/strokedb/document.rb', line 530

def raw_uuid #:nodoc:
  @raw_uuid ||= uuid.to_raw_uuid
end

#reloadObject

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



396
397
398
# File 'lib/strokedb/document.rb', line 396

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

#remove_slot!(slotname) ⇒ Object

Removes slot

document.remove_slot!(:slotname)


244
245
246
247
248
249
250
251
# File 'lib/strokedb/document.rb', line 244

def remove_slot!(slotname)
  slotname = slotname.to_s

  @slots.delete slotname
  update_version! slotname

  nil
end

#reverse_update_slots(hash) ⇒ Object

Updates nil/false slots with a specified hash and returns itself. Already set slots are not modified (||= is used). Acts like hash1.reverse_merge(hash2) (hash2.merge(hash1)).



456
457
458
459
460
461
# File 'lib/strokedb/document.rb', line 456

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

#reverse_update_slots!(hash) ⇒ Object

Same as reverse_update_slots, but also saves the document.



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

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

#save!(perform_validation = true) ⇒ Object

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



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

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"]


258
259
260
# File 'lib/strokedb/document.rb', line 258

def slotnames
  @slots.keys
end

#storeObject



77
78
79
80
81
82
83
# File 'lib/strokedb/document.rb', line 77

def store
  if (txns = Thread.current[:strokedb_transactions]) && !txns.nil? && !txns.empty?
    txns.last
  else
    @store
  end
end

#to_jsonObject

Returns string with Document’s JSON representation



311
312
313
# File 'lib/strokedb/document.rb', line 311

def to_json
  to_raw.to_json
end

#to_optimized_rawObject

:nodoc:



335
336
337
# File 'lib/strokedb/document.rb', line 335

def to_optimized_raw #:nodoc:
  __reference__
end

#to_rawObject

Primary serialization



325
326
327
328
329
330
331
332
333
# File 'lib/strokedb/document.rb', line 325

def to_raw #:nodoc:
  raw_slots = {}

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

  raw_slots.to_raw
end

#to_xml(opts = {}) ⇒ Object

Returns string with Document’s XML representation



318
319
320
# File 'lib/strokedb/document.rb', line 318

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

#update_slots(hash) ⇒ Object

Updates slots with a specified hash and returns itself.



439
440
441
442
443
444
# File 'lib/strokedb/document.rb', line 439

def update_slots(hash)
  hash.each do |k, v|
    send("#{k}=", v) unless self[k] == v
  end
  self
end

#update_slots!(hash) ⇒ Object

Same as update_slots, but also saves the document.



447
448
449
# File 'lib/strokedb/document.rb', line 447

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

#uuidObject

Return document’s uuid



526
527
528
# File 'lib/strokedb/document.rb', line 526

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

#versionObject

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



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

def version
  self[:version]
end

#version=(v) ⇒ Object

:nodoc:



541
542
543
# File 'lib/strokedb/document.rb', line 541

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

#versionsObject

Returns an instance of Document::Versions



548
549
550
# File 'lib/strokedb/document.rb', line 548

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