Class: RubyFFDB::Document

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/rffdb/document.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(existing_id = false, lazy = true) ⇒ Document

Returns a new instance of Document.

Raises:



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/rffdb/document.rb', line 7

def initialize(existing_id = false, lazy = true)
  @read_lock  = Mutex.new
  @write_lock = Mutex.new
  if existing_id
    @id  = existing_id
    fail Exceptions::NoSuchDocument unless File.exist?(file_path)
    if lazy
      @lazy = true
    else
      reload(true)
      @lazy = false
    end
    @saved = true
  else
    @id = storage.next_id(self.class)
    @data = {}
    # relative to database root
    @saved = false
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

TODO:

refactor and comment better

Uses the defined schema to setup getter and setter methods. Runs validations, format checking, and type checking on setting methods.

Raises:



258
259
260
261
262
263
264
265
266
267
268
269
270
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
# File 'lib/rffdb/document.rb', line 258

def method_missing(method, *args, &block)
  setter = method.to_s.match(/(.*)=$/) ? true : false
  key = setter ? $1.to_sym : method.to_s.to_sym

  if structure.key?(key) && setter
    if args.last.is_a?(structure[key][:class]) &&
       (
         structure[key][:format].nil? ||
         args.last.to_s.match(structure[key][:format])
       )
      valid = true
      if structure[key][:unique] == true
        fail Exceptions::NotUnique unless test_uniqueness(key, args.last)
      end
      structure[key][:validations].each do |validation|
        valid = send(validation.to_sym, args.last)
        fail Exceptions::FailedValidation unless valid
      end
      # here is where the lazy-loading happens
      refresh if @read_lock.synchronize { @lazy } &&
                 @read_lock.synchronize { committed? }
      @read_lock.synchronize do
        @write_lock.synchronize do
          if valid
            @data[key.to_s] = args.last
          end
        end
      end
      commit if indexed_column?(key) # indexed columns always cause commits
    else
      fail Exceptions::InvalidInput
    end
    @saved = false
  elsif structure.key?(key)
    # here is where the lazy-loading happens
    refresh if @read_lock.synchronize { @lazy } &&
               @read_lock.synchronize { committed? }
    @read_lock.synchronize do
      @data[key.to_s]
    end
  else
    super
  end
end

Instance Attribute Details

#idObject (readonly)

Returns the value of attribute id.



4
5
6
# File 'lib/rffdb/document.rb', line 4

def id
  @id
end

Class Method Details

.allDocumentCollection

Return all available instances of this type

Returns:



213
214
215
216
217
218
# File 'lib/rffdb/document.rb', line 213

def self.all
  DocumentCollection.new(
    storage.all(self).collect { |doc_id| load(doc_id) },
    self
  )
end

.attribute(name, options = {}) ⇒ Object

This DSL method is used to define the schema for a document. It sets up all data access for the class, and allows specifying strict checks on that schema during its use, such as validations, class types, regexp formatting, etc.

Parameters:

  • name (Symbol)

    the unique name of the attribute

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :class (Class) — default: Object

    the expected object class for this attribute

  • :format (Regexp)

    a regular expression for the required format of the attribute (for any :class that supports #.to_s)

  • :validate (Array, Symbol)

    either a symbol or array of symbols referencing the instance method(s) to use to validate this attribute

  • :unique (Boolean)

    should this attribute be unique?



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/rffdb/document.rb', line 125

def self.attribute(name, options = {})
  @structure ||= {}
  @structure[name.to_sym] = {}
  # setup the schema
  @structure[name.to_sym][:class]   =
    options.key?(:class) ? options[:class] : Object
  @structure[name.to_sym][:format]  =
    options.key?(:format) ? options[:format] : nil
  @structure[name.to_sym][:validations] =
    options.key?(:validate) ? [*options[:validate]] : []
  @structure[name.to_sym][:unique] =
    options.key?(:unique) == true ? true : false
  @structure[name.to_sym][:index] =
    options.key?(:index) == true ? true : false
end

.cacheCacheProvider

Allow direct access to the cache instance of this document class

Returns:



207
208
209
# File 'lib/rffdb/document.rb', line 207

def self.cache
  storage.cache(self)
end

.cache_size(size) ⇒ Object

Sets the maximum number of entries the cache instance for this document will hold. Note: this clears the current contents of the cache.

Parameters:

  • size (Fixnum)

    the maximum size of this class’ cache instance



201
202
203
# File 'lib/rffdb/document.rb', line 201

def self.cache_size(size)
  storage.cache_size(self, size)
end

.engine(storage_engine, cache_opts = {}) ⇒ Object

This DSL method is used to setup the backend StorageEngine class and optionally the CacheProvider for this Document type.

Parameters:

  • storage_engine (Class)

    the StorageEngine child class to use

  • cache_opts (Hash) (defaults to: {})

    a customizable set of options

Options Hash (cache_opts):

  • :cache_provider (Class) — default: CacheProviders::LRUCache

    the CacheProvider child class for caching

  • :cache_size (Fixnum)

    the cache size, in terms of the number of objects stored

Raises:



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/rffdb/document.rb', line 153

def self.engine(storage_engine, cache_opts = {})
  unless storage_engine.instance_of?(Class) &&
         storage_engine.ancestors.include?(StorageEngine)
    fail Exceptions::InvalidEngine
  end
  @engine = storage_engine
  if cache_opts.key?(:cache_provider)
    # Make sure the cache provider specified is valid
    unless cache_opts[:cache_provider].instance_of?(Class) &&
           cache_opts[:cache_provider].ancestors.include?(CacheProvider)
      fail Exceptions::InvalidCacheProvider
    end

    @engine.cache_provider(self, cache_opts[:cache_provider])
  end

  @engine.cache_size(
    self, cache_opts[:cache_size]
  ) if cache_opts.key?(:cache_size)
end

.indexed_column?(column) ⇒ Boolean

Should this column be indexed?

Returns:

  • (Boolean)


242
243
244
245
# File 'lib/rffdb/document.rb', line 242

def self.indexed_column?(column)
  csym = column.to_sym
  structure.key?(csym) && structure[csym][:index] == true
end

.load(id) ⇒ Object

Currently an alias for #new, but used as a wrapper in case more work needs to be done before pulling a document from the storage engine (such as sanitizing input, etc)



97
98
99
# File 'lib/rffdb/document.rb', line 97

def self.load(id)
  new(id)
end

.reindexObject

Reindex all documents of this type. This can take a while on large DBs.



104
105
106
107
108
109
# File 'lib/rffdb/document.rb', line 104

def self.reindex
  storage.all(self).each do |doc_id| 
    new(doc_id, false).touch.commit
  end
  return true
end

.storageStorageEngine

Returns a reference to the storage engine singleton of this document class.

Returns:

  • (StorageEngine)

    a reference to the storage engine singleton of this document class



176
177
178
179
# File 'lib/rffdb/document.rb', line 176

def self.storage
  @engine ||= StorageEngines::YamlEngine
  @engine
end

.structureHash

Returns a copy of the schema information for this class.

Returns:

  • (Hash)

    a copy of the schema information for this class



188
189
190
191
# File 'lib/rffdb/document.rb', line 188

def self.structure
  @structure ||= {}
  @structure.dup
end

.where(attribute, value, comp_op = '==') ⇒ Object

Query for Documents based on an attribute



222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/rffdb/document.rb', line 222

def self.where(attribute, value, comp_op = '==')
  if indexed_column?(attribute)
    DocumentCollection.new(
      storage.index_lookup(self, attribute, value, comp_op).collect do |did|
        load(did)
      end,
      self
    )
  else
    all.where(attribute, value, comp_op)
  end
end

Instance Method Details

#<=>(other) ⇒ Object

Compare two documents



236
237
238
# File 'lib/rffdb/document.rb', line 236

def <=>(other)
  id <=> other.id
end

#commitBoolean Also known as: save

Commit the document to storage

Returns:

  • (Boolean)


36
37
38
39
40
41
42
43
44
45
# File 'lib/rffdb/document.rb', line 36

def commit
  @read_lock.synchronize do
    @write_lock.synchronize do
      unless @saved
        storage.store(self.class, @id, @data.dup)
      end
      @saved = true
    end
  end
end

#committed?Boolean

Has this documented been committed to storage?

Returns:

  • (Boolean)


51
52
53
# File 'lib/rffdb/document.rb', line 51

def committed?
  @saved
end

#file_pathString

The location of the flat-file

Returns:

  • (String)

    flat-file used to store this document (may not exist yet)



30
31
32
# File 'lib/rffdb/document.rb', line 30

def file_path
  storage.file_path(self.class, @id)
end

#indexed_column?(column) ⇒ Boolean

Returns:

  • (Boolean)


247
248
249
# File 'lib/rffdb/document.rb', line 247

def indexed_column?(column)
  self.class.send('indexed_column?'.to_sym, column)
end

#refreshObject

Overwrites the document’s data, either from disk or from cache. Useful for lazy-loading and not typically used directly. Since data might have been pulled from cache, this can lead to bizarre things if not used carefully and things rely on #committed? or @saved.



79
80
81
82
83
84
# File 'lib/rffdb/document.rb', line 79

def refresh
  @write_lock.synchronize do
    @data = storage.retrieve(self.class, @id)
    @saved = true
  end
end

#reload(force = false) ⇒ Object

Retrieve the stored data from disk, never using cache. Allows forcing to overwrite uncommitted changes.

Raises:



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/rffdb/document.rb', line 59

def reload(force = false)
  if committed? || force
    @read_lock.synchronize do
      @write_lock.synchronize do
        @data = storage.retrieve(self.class, @id, false)
      end
    end
  else
    fail Exceptions::PendingChanges
  end
  @read_lock.synchronize do
    @write_lock.synchronize { @saved = true }
  end
  return self
end

#respond_to?(method) ⇒ Boolean

Returns:

  • (Boolean)


303
304
305
306
307
308
309
310
311
# File 'lib/rffdb/document.rb', line 303

def respond_to?(method)
  key = method.to_s.match(/(.*)=$/) ? $1.to_sym : method.to_s.to_sym

  if structure.key?(key)
    true
  else
    super
  end
end

#storageStorageEngine

Returns a reference to the storage engine singleton of this document class.

Returns:

  • (StorageEngine)

    a reference to the storage engine singleton of this document class



183
184
185
# File 'lib/rffdb/document.rb', line 183

def storage
  self.class.send(:storage)
end

#structureHash

Returns a copy of the schema information for this class.

Returns:

  • (Hash)

    a copy of the schema information for this class



194
195
196
# File 'lib/rffdb/document.rb', line 194

def structure
  self.class.send(:structure)
end

#touchObject

Allow saving an already saved Document. Useful for reindexing, maybe more.



87
88
89
90
91
92
# File 'lib/rffdb/document.rb', line 87

def touch
  @read_lock.synchronize do
    @write_lock.synchronize { @saved = false }
  end
  return self
end