Class: RelaxDB::Document

Inherits:
Object
  • Object
show all
Includes:
Validators
Defined in:
lib/relaxdb/document.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(set_via_writers = {}, hash = nil) ⇒ Document

Returns a new instance of Document.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/relaxdb/document.rb', line 119

def initialize(set_via_writers={}, hash=nil)
  hash, set_via_writers = set_via_writers, true if set_via_writers.is_a?(Hash)
  
  # The default _id will be overwritten if loaded from CouchDB
  self._id = UuidGenerator.uuid 
  
  @errors = Errors.new
  @validation_skip_list = []

  # Set default properties if this object has not known CouchDB
  unless hash["_rev"]
    properties.each do |prop|
     if methods.include?("set_default_#{prop}")
       send("set_default_#{prop}")
     end
    end
  end
  
  # Maybe use the presence of _rev in hash to determine this rather than 
  # exposing the implementation detail to clients that choose to override?
  set_via_writers ? set_attributes(hash) : set_raw_attributes(hash)
end

Instance Attribute Details

#errorsObject

Used to store validation messages



8
9
10
# File 'lib/relaxdb/document.rb', line 8

def errors
  @errors
end

#validation_skip_listObject

Attribute symbols added to this list won’t be validated on save



11
12
13
# File 'lib/relaxdb/document.rb', line 11

def validation_skip_list
  @validation_skip_list
end

Class Method Details

.add_derived_prop(prop, deriver) ⇒ Object

See derived_properties_spec.rb for usage



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

def self.add_derived_prop(prop, deriver)
    source, writer = deriver[0], deriver[1]
    @derived_prop_writers ||= {}
    @derived_prop_writers[source] ||= {}
    @derived_prop_writers[source][prop] = writer
end

.after_save(callback) ⇒ Object



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

def self.after_save(callback)
  after_save_callbacks << callback
end

.after_save_callbacksObject



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

def self.after_save_callbacks
  @after_save_callbacks ||= []
end

.allObject



469
470
471
# File 'lib/relaxdb/document.rb', line 469

def self.all
  @all_delegator ||= AllDelegator.new(self.name)
end

.all_relationshipsObject



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

def self.all_relationships
  belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
end

.before_save(callback) ⇒ Object

Callbacks - define these in a module and mix’em’in ?



500
501
502
# File 'lib/relaxdb/document.rb', line 500

def self.before_save(callback)
  before_save_callbacks << callback
end

.before_save_callbacksObject



504
505
506
# File 'lib/relaxdb/document.rb', line 504

def self.before_save_callbacks
  @before_save ||= []
end

.belongs_to(relationship, opts = {}) ⇒ Object Also known as: references



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/relaxdb/document.rb', line 426

def self.belongs_to(relationship, opts={})
  @belongs_to_rels ||= {}
  @belongs_to_rels[relationship] = opts

  define_method(relationship) do
    create_or_get_proxy(BelongsToProxy, relationship).target
  end
  
  define_method("#{relationship}=") do |new_target|
    create_or_get_proxy(BelongsToProxy, relationship).target = new_target
    write_derived_props(relationship)
  end
  
  # Allows all writers to be invoked from the hash passed to initialize 
  define_method("#{relationship}_id=") do |id|
    instance_variable_set("@#{relationship}_id".to_sym, id)
    write_derived_props(relationship)
    id
  end

  # Allows belongs_to relationships to be used by the paginator
  define_method("#{relationship}_id") do
    instance_variable_get("@#{relationship}_id")
  end
  
  create_validator(relationship, opts[:validator]) if opts[:validator]
  
  # Untested below
  create_validation_msg(relationship, opts[:validation_msg]) if opts[:validation_msg]
end

.belongs_to_relsObject



461
462
463
# File 'lib/relaxdb/document.rb', line 461

def self.belongs_to_rels
  @belongs_to_rels ||= {}
end

.create_validation_msg(att, validation_msg) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/relaxdb/document.rb', line 68

def self.create_validation_msg(att, validation_msg)
  if validation_msg.is_a?(Proc)        
    validation_msg.arity == 1 ?
      define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val) } :
      define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val, self) } 
  else  
    define_method("#{att}_validation_msg") { validation_msg } 
  end
end

.create_validator(att, v) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/relaxdb/document.rb', line 55

def self.create_validator(att, v)
  method_name = "validate_#{att}"
  if v.is_a? Proc
    v.arity == 1 ?
      define_method(method_name) { |att_val| v.call(att_val) } :
      define_method(method_name) { |att_val| v.call(att_val, self) }
  elsif instance_methods.include? "validator_#{v}"
    define_method(method_name) { |att_val| send("validator_#{v}", att_val, self) }
  else
    define_method(method_name) { |att_val| send(v, att_val) }
  end          
end

.derived_prop_writersObject



86
87
88
# File 'lib/relaxdb/document.rb', line 86

def self.derived_prop_writers
  @derived_prop_writers ||= {}
end

.has_many(relationship, opts = {}) ⇒ Object



387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/relaxdb/document.rb', line 387

def self.has_many(relationship, opts={})
  @has_many_rels ||= []
  @has_many_rels << relationship
  
  define_method(relationship) do
    create_or_get_proxy(HasManyProxy, relationship, opts)
  end
  
  define_method("#{relationship}=") do |children|
    create_or_get_proxy(HasManyProxy, relationship, opts).children = children
    write_derived_props(relationship)
    children
  end      
end

.has_many_relsObject



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

def self.has_many_rels
  # Don't force clients to check its instantiated
  @has_many_rels ||= []
end

.has_one(relationship) ⇒ Object



407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/relaxdb/document.rb', line 407

def self.has_one(relationship)
  @has_one_rels ||= []
  @has_one_rels << relationship
  
  define_method(relationship) do      
    create_or_get_proxy(HasOneProxy, relationship).target
  end
  
  define_method("#{relationship}=") do |new_target|
    create_or_get_proxy(HasOneProxy, relationship).target = new_target
    write_derived_props(relationship)
    new_target
  end
end

.has_one_relsObject



422
423
424
# File 'lib/relaxdb/document.rb', line 422

def self.has_one_rels
  @has_one_rels ||= []      
end

.paginate_by(page_params, *view_keys) {|paginate_params| ... } ⇒ Object

Yields:

  • (paginate_params)


529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/relaxdb/document.rb', line 529

def self.paginate_by(page_params, *view_keys)
  paginate_params = PaginateParams.new
  yield paginate_params
  raise paginate_params.error_msg if paginate_params.invalid? 
  
  paginator = Paginator.new(paginate_params, page_params)
              
  design_doc_name = self.name
  view = SortedByView.new(design_doc_name, *view_keys)
  query = Query.new(design_doc_name, view.view_name)
  query.merge(paginate_params)
  
  docs = view.query(query)
  docs.reverse! if paginate_params.order_inverted?
  
  paginator.add_next_and_prev(docs, design_doc_name, view.view_name, view_keys)
  
  docs
end

.propertiesObject



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

def self.properties
  # Ensure that classes that don't define their own properties still function as CouchDB objects
  @properties ||= [:_id, :_rev]
end

.property(prop, opts = {}) ⇒ Object

Define properties and property methods



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/relaxdb/document.rb', line 15

def self.property(prop, opts={})
  # Class instance varibles are not inherited, so the default properties must be explicitly listed 
  # Perhaps a better solution exists. Revise. I think extlib contains a solution for this...
  @properties ||= [:_id, :_rev]
  @properties << prop

  define_method(prop) do
    instance_variable_get("@#{prop}".to_sym)        
  end

  define_method("#{prop}=") do |val|
    instance_variable_set("@#{prop}".to_sym, val)
  end
  
  if opts[:default]
    define_method("set_default_#{prop}") do
      default = opts[:default]
      default = default.is_a?(Proc) ? default.call : default
      instance_variable_set("@#{prop}".to_sym, default)
    end
  end
  
  if opts[:validator]
    create_validator(prop, opts[:validator]) 
  end
  
  if opts[:validation_msg]
    create_validation_msg(prop, opts[:validation_msg])
  end
  
  if opts[:derived]
    add_derived_prop(prop, opts[:derived])
  end
end

.references_many(relationship, opts = {}) ⇒ Object

If you’re using this method, read the specs and make sure you understand how it can be used and how it shouldn’t be used



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/relaxdb/document.rb', line 362

def self.references_many(relationship, opts={})
  # Treat the representation as a standard property 
  properties << relationship
  
  # Keep track of the relationship so peers can be disassociated on destroy
  @references_many_rels ||= []
  @references_many_rels << relationship
 
  define_method(relationship) do
    array_sym = "@#{relationship}".to_sym
    instance_variable_set(array_sym, []) unless instance_variable_defined? array_sym

    create_or_get_proxy(RelaxDB::ReferencesManyProxy, relationship, opts)
  end

  define_method("#{relationship}=") do |val|
    # Sharp edge - do not invoke this method
    instance_variable_set("@#{relationship}".to_sym, val)
  end           
end

.references_many_relsObject



383
384
385
# File 'lib/relaxdb/document.rb', line 383

def self.references_many_rels
  @references_many_rels ||= []
end

Instance Method Details

#==(other) ⇒ Object

Returns true if CouchDB considers other to be the same as self



356
357
358
# File 'lib/relaxdb/document.rb', line 356

def ==(other)
  other && _id == other._id
end

#after_saveObject



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

def after_save
  self.class.after_save_callbacks.each do |callback|
    callback.is_a?(Proc) ? callback.call(self) : send(callback)
  end
end

#all_childrenObject



273
274
275
276
277
# File 'lib/relaxdb/document.rb', line 273

def all_children      
  ho = self.class.has_one_rels.map { |r| send(r) }
  hm = self.class.has_many_rels.inject([]) { |m,r| m += send(r).children }
  ho + hm
end

#before_saveObject



508
509
510
511
512
513
# File 'lib/relaxdb/document.rb', line 508

def before_save
  self.class.before_save_callbacks.each do |callback|
    resp = callback.is_a?(Proc) ? callback.call(self) : send(callback)
    return false unless resp
  end
end

#create_or_get_proxy(klass, relationship, opts = nil) ⇒ Object



345
346
347
348
349
350
351
352
353
# File 'lib/relaxdb/document.rb', line 345

def create_or_get_proxy(klass, relationship, opts=nil)
  proxy_sym = "@proxy_#{relationship}".to_sym
  proxy = instance_variable_get(proxy_sym)
  unless proxy
    proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
    instance_variable_set(proxy_sym, proxy)
  end
  proxy     
end

#destroy!Object

destroy! nullifies all relationships with peers and children before deleting itself in CouchDB The nullification and deletion are not performed in a transaction

TODO: Current implemention may be inappropriate - causing CouchDB to try to JSON encode undefined. Ensure nil is serialized? See has_many_spec#should nullify its child relationships



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/relaxdb/document.rb', line 479

def destroy!
  self.class.references_many_rels.each do |rel|
    send(rel).clear
  end
  
  self.class.has_many_rels.each do |rel|
    send(rel).clear
  end
  
  self.class.has_one_rels.each do |rel|
    send("#{rel}=".to_sym, nil)
  end
  
  # Implicitly prevent the object from being resaved by failing to update its revision
  RelaxDB.db.delete("#{_id}?rev=#{_rev}")
  self
end

#inspectObject Also known as: to_s



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/relaxdb/document.rb', line 188

def inspect
  s = "#<#{self.class}:#{self.object_id}"
  properties.each do |prop|
    prop_val = instance_variable_get("@#{prop}".to_sym)
    s << ", #{prop}: #{prop_val.inspect}" if prop_val
  end
  self.class.belongs_to_rels.each do |relationship, opts|
    id = instance_variable_get("@#{relationship}_id".to_sym)
    s << ", #{relationship}_id: #{id}" if id
  end
  s << ", errors: #{errors.inspect}" unless errors.empty?
  s << ">"
end

#new_document?Boolean Also known as: new_record?, unsaved?

Returns:

  • (Boolean)


327
328
329
# File 'lib/relaxdb/document.rb', line 327

def new_document?
  @_rev.nil?
end

#on_update_conflictObject



239
240
241
242
# File 'lib/relaxdb/document.rb', line 239

def on_update_conflict
  # override with any behaviour you want to happen when
  # CouchDB returns DocumentConflict on an attempt to save
end

#post_saveObject



251
252
253
# File 'lib/relaxdb/document.rb', line 251

def post_save
  after_save
end

#pre_saveObject



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

def pre_save
  return false unless validates?
  return false unless before_save            
  set_created_at if new_document?
  true 
end

#propertiesObject



110
111
112
# File 'lib/relaxdb/document.rb', line 110

def properties
  self.class.properties
end

#saveObject

Not yet sure of final implemention for hooks - may lean more towards DM than AR



219
220
221
222
223
224
225
226
# File 'lib/relaxdb/document.rb', line 219

def save
  if pre_save && save_to_couch
    after_save
    self
  else
    false
  end
end

#save!Object



255
256
257
258
259
260
261
262
263
# File 'lib/relaxdb/document.rb', line 255

def save!
  if save
    self
  elsif update_conflict?
    raise UpdateConflict, self
  else
    raise ValidationFailure, self.errors.to_json
  end
end

#save_allObject



265
266
267
# File 'lib/relaxdb/document.rb', line 265

def save_all
  RelaxDB.bulk_save(self, *all_children)
end

#save_all!Object



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

def save_all!
  RelaxDB.bulk_save!(self, *all_children)
end

#save_to_couchObject



228
229
230
231
232
233
234
235
236
237
# File 'lib/relaxdb/document.rb', line 228

def save_to_couch
  begin
    resp = RelaxDB.db.put(_id, to_json)
    self._rev = JSON.parse(resp.body)["rev"]
  rescue HTTP_412
    on_update_conflict
    @update_conflict = true
    return false
  end      
end

#set_attributes(data) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/relaxdb/document.rb', line 142

def set_attributes(data)
  data.each do |key, val|
    # Only set instance variables on creation - object references are resolved on demand

    # If the variable name ends in _at, _on or _date try to convert it to a Time
    if [/_at$/, /_on$/, /_date$/].inject(nil) { |i, r| i ||= (key =~ r) }
        val = Time.parse(val).utc rescue val
    end
    
    # Ignore param keys that don't have a corresponding writer
    # This allows us to comfortably accept a hash containing superflous data 
    # such as a params hash in a controller 
    send("#{key}=".to_sym, val) if methods.include? "#{key}="
  end
end

#set_created_atObject



338
339
340
341
342
343
# File 'lib/relaxdb/document.rb', line 338

def set_created_at
  if methods.include? "created_at"
    # Don't override it if it's already been set
    @created_at = Time.now if @created_at.nil?
  end
end

#set_raw_attributes(data) ⇒ Object

Set the raw attribute values rather than setting via the associated writers The associated writers may invoke validation functions or set derived values Such behaviour is unwanted when loading from CouchDB and potentially under other circumstances



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/relaxdb/document.rb', line 162

def set_raw_attributes(data)
  data.each do |key, val|
    if [/_at$/, /_on$/, /_date$/].inject(nil) { |i, r| i ||= (key =~ r) }
        val = Time.parse(val).utc rescue val
    end
    
    if methods.include? "#{key}="
      key = key.to_sym
      if properties.include? key
        instance_variable_set("@#{key}".to_sym, val)
      elsif self.class.has_one_rels.include? key
        create_or_get_proxy(HasOneProxy, key).target = val
      else 
        # belongs_to
        if key.to_s =~ /_id$/                                            
          instance_variable_set("@#{key}".to_sym, val)                   
        else                                                             
          create_or_get_proxy(BelongsToProxy, key).target = val          
        end                                                              
      end                                                                
    end
            
  end
  
end

#to_jsonObject



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

def to_json
  data = {}
  self.class.belongs_to_rels.each do |relationship, opts|
    id = instance_variable_get("@#{relationship}_id".to_sym)
    data["#{relationship}_id"] = id if id
  end
  properties.each do |prop|
    prop_val = instance_variable_get("@#{prop}".to_sym)
    data["#{prop}"] = prop_val if prop_val
  end
  data["class"] = self.class.name
  data.to_json      
end

#to_paramObject Also known as: id



333
334
335
# File 'lib/relaxdb/document.rb', line 333

def to_param
  self._id
end

#update_conflict?Boolean

Returns:

  • (Boolean)


279
280
281
# File 'lib/relaxdb/document.rb', line 279

def update_conflict?
  @update_conflict
end

#validate_att(att_name, att_val) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/relaxdb/document.rb', line 304

def validate_att(att_name, att_val)
  begin
    success = send("validate_#{att_name}", att_val)
  rescue => e
    RelaxDB.logger.warn "Validating #{att_name} with #{att_val} raised #{e}"
    succes = false
  end

  unless success
    if methods.include? "#{att_name}_validation_msg"
      begin
        @errors[att_name] = send("#{att_name}_validation_msg", att_val)
      rescue => e
        RelaxDB.logger.warn "Validation_msg for #{att_name} with #{att_val} raised #{e}"
        @errors[att_name] = "validation_msg_exception:invalid:#{att_val}"
      end
    else
      @errors[att_name] = "invalid:#{att_val}"
    end
  end
  success
end

#validates?Boolean

Returns:

  • (Boolean)


283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/relaxdb/document.rb', line 283

def validates?
  props = properties - validation_skip_list
  prop_vals = props.map { |prop| instance_variable_get("@#{prop}") }
  
  rels = self.class.belongs_to_rels.keys - validation_skip_list
  rel_vals = rels.map { |rel| instance_variable_get("@#{rel}_id") }
  
  att_names = props + rels
  att_vals =  prop_vals + rel_vals
  
  total_success = true      
  att_names.each_index do |i|
    att_name, att_val = att_names[i], att_vals[i]
    if methods.include? "validate_#{att_name}"
      total_success &= validate_att(att_name, att_val)
    end
  end
        
  total_success
end

#write_derived_props(source) ⇒ Object

The rationale for rescuing the send below is that the lambda for a derived property shouldn’t need to concern itself with checking the validity of the underlying property. Nor, IMO, should clients be exposed to the possibility of a writer raising an exception.



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/relaxdb/document.rb', line 96

def write_derived_props(source)
  writers = self.class.derived_prop_writers[source]
  if writers 
    writers.each do |prop, writer|
      current_val = send(prop)
      begin
        send("#{prop}=", writer.call(current_val, self)) 
      rescue => e
        RelaxDB.logger.warn "Deriving #{prop} from #{source} raised #{e}"
      end
    end
  end
end