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)
  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
  @read_lock  = Mutex.new
  @write_lock = Mutex.new
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:



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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
# File 'lib/rffdb/document.rb', line 239

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
            storage.index(self.class, key).delete(@data[key.to_s], id) if indexed_column?(key)
            @data[key.to_s] = args.last
            storage.index(self.class, key).put(args.last, id) if indexed_column?(key)
          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:



196
197
198
199
200
201
# File 'lib/rffdb/document.rb', line 196

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?



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/rffdb/document.rb', line 108

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:



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

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



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

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:



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/rffdb/document.rb', line 136

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)


223
224
225
226
# File 'lib/rffdb/document.rb', line 223

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)



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

def self.load(id)
  new(id)
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



159
160
161
162
# File 'lib/rffdb/document.rb', line 159

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



171
172
173
174
# File 'lib/rffdb/document.rb', line 171

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

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

Query for Documents based on an attribute



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

def self.where(attribute, value, comparison_method = '==')
  if comparison_method.to_s == '==' && indexed_column?(attribute)
    DocumentCollection.new(
      storage.index(self, attribute).get(value).collect { |did| load(did) },
      self
    )
  else
    all.where(attribute, value, comparison_method)
  end
end

Instance Method Details

#<=>(other) ⇒ Object

Compare two documents



217
218
219
# File 'lib/rffdb/document.rb', line 217

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)


228
229
230
# File 'lib/rffdb/document.rb', line 228

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.



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

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
# 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
end

#respond_to?(method) ⇒ Boolean

Returns:

  • (Boolean)


286
287
288
289
290
291
292
293
294
# File 'lib/rffdb/document.rb', line 286

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



166
167
168
# File 'lib/rffdb/document.rb', line 166

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



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

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