Class: AbstractRecord

Inherits:
Object
  • Object
show all
Defined in:
lib/yodel/models/core/record/abstract_record.rb

Overview

Record objects must implement these methods: fields perform_save perform_destroy perform_reload

Direct Known Subclasses

EmbeddedRecord, MongoRecord

Constant Summary collapse

CALLBACKS =

Callbacks & Validation


%w{save create update destroy validation}
ORDERS =
%w{before after}
FIELD_CALLBACKS =

Field callbacks

%w{save create update destroy}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(values = {}, new_record = true) ⇒ AbstractRecord

Returns a new instance of AbstractRecord.



10
11
12
13
14
15
16
17
# File 'lib/yodel/models/core/record/abstract_record.rb', line 10

def initialize(values={}, new_record=true)
  @values   = default_values.merge(values.stringify_keys) # FIXME: don't merge here; default || values
  @typecast = {} # typecast versions of original document values
  @changed  = {} # typecast versions of changed values
  @stash    = {} # values of unknown fields set by from_json
  @errors   = Errors.new
  @new      = new_record
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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

Raises:

  • (NoMethodError)


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/yodel/models/core/record/abstract_record.rb', line 199

def method_missing(name, *args, &block)
  # Catch a "fun" ruby 1.9 implemention detail. Calls to flatten blindly call
  # to_ary on items in an array rather than checking it they really support
  # the method with respond_to? Catch, and raise the expected exception.
  raise NoMethodError if name == :to_ary    
  field_name = name.to_s

  if field_name.end_with?('_changed?')
    changed?(field_name[0...-9])
  elsif field_name.end_with?('=')
    set(field_name[0...-1], args.first)
  elsif field_name.end_with?('?')
    present?(field_name[0...-1])
  elsif field_name.end_with?('_was')
    field_was(field_name[0...-4])
  else
    get(field_name)
  end
end

Instance Attribute Details

#changedObject (readonly)

Returns the value of attribute changed.



8
9
10
# File 'lib/yodel/models/core/record/abstract_record.rb', line 8

def changed
  @changed
end

#errorsObject (readonly)

Returns the value of attribute errors.



8
9
10
# File 'lib/yodel/models/core/record/abstract_record.rb', line 8

def errors
  @errors
end

#stashObject (readonly)

Returns the value of attribute stash.



8
9
10
# File 'lib/yodel/models/core/record/abstract_record.rb', line 8

def stash
  @stash
end

#typecastObject (readonly)

Returns the value of attribute typecast.



8
9
10
# File 'lib/yodel/models/core/record/abstract_record.rb', line 8

def typecast
  @typecast
end

#valuesObject (readonly)

Returns the value of attribute values.



8
9
10
# File 'lib/yodel/models/core/record/abstract_record.rb', line 8

def values
  @values
end

Class Method Details

.inherited(child) ⇒ Object



337
338
339
340
341
342
343
344
345
# File 'lib/yodel/models/core/record/abstract_record.rb', line 337

def self.inherited(child)
  super(child)
  CALLBACKS.each do |callback|
    ORDERS.each do |order|
      callbacks = instance_variable_get("@_#{order}_#{callback}_callbacks")
      child.instance_variable_set("@_#{order}_#{callback}_callbacks", callbacks)
    end
  end
end

Instance Method Details

#changed!(name) ⇒ Object



177
178
179
180
181
# File 'lib/yodel/models/core/record/abstract_record.rb', line 177

def changed!(name)
  ensure_field_is_valid(name)
  return if @changed.key?(name)
  @changed[name] = get(name).dup
end

#changed?(name) ⇒ Boolean

Returns:

  • (Boolean)


172
173
174
175
# File 'lib/yodel/models/core/record/abstract_record.rb', line 172

def changed?(name)
  ensure_field_is_valid(name)
  @changed.key?(name)
end

#clear_key(name) ⇒ Object



129
130
131
132
# File 'lib/yodel/models/core/record/abstract_record.rb', line 129

def clear_key(name)
  @changed.delete(name)
  @typecast.delete(name)
end

#default_valuesObject



50
51
52
53
54
55
56
57
# File 'lib/yodel/models/core/record/abstract_record.rb', line 50

def default_values
  fields.each_with_object({}) do |(name, field), defaults|
    default_value = field.default
    unless default_value.nil? && field.strip_nil?
      defaults[name] = default_value
    end
  end
end

#destroyObject



266
267
268
269
270
271
272
273
# File 'lib/yodel/models/core/record/abstract_record.rb', line 266

def destroy
  return if new? || destroyed?
  succeeded = false
  run_destroy_callbacks do
    succeeded = perform_destroy
  end
  @destroyed = succeeded
end

#destroyed?Boolean

Returns:

  • (Boolean)


227
228
229
# File 'lib/yodel/models/core/record/abstract_record.rb', line 227

def destroyed?
  !!@destroyed
end

#eql?(other) ⇒ Boolean Also known as: ==


Equality


Returns:

  • (Boolean)


23
24
25
26
# File 'lib/yodel/models/core/record/abstract_record.rb', line 23

def eql?(other)
  other.respond_to?(:id) && other.id == self.id &&
  other.is_a?(AbstractRecord)
end

#errors?Boolean

Returns:

  • (Boolean)


361
362
363
# File 'lib/yodel/models/core/record/abstract_record.rb', line 361

def errors?
  !@errors.blank?
end

#field(name) ⇒ Object



42
43
44
# File 'lib/yodel/models/core/record/abstract_record.rb', line 42

def field(name)
  fields[name]
end

#field?(name) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/yodel/models/core/record/abstract_record.rb', line 46

def field?(name)
  fields.key?(name)
end

#field_was(name) ⇒ Object



183
184
185
186
187
188
189
190
# File 'lib/yodel/models/core/record/abstract_record.rb', line 183

def field_was(name)
  ensure_field_is_valid(name)
  if @typecast.key?(name)
    @typecast[name]
  else
    typecast_value(name)
  end
end

#fieldsObject


Modelling




38
39
40
# File 'lib/yodel/models/core/record/abstract_record.rb', line 38

def fields
  {}
end

#from_json(values) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/yodel/models/core/record/abstract_record.rb', line 97

def from_json(values)
  values.each do |name, value|
    if field?(name)
      current_field = field(name)
      raise MassAssignment, "Cannot mass assign #{field}" if current_field.protected?
    else
      @stash[name] = value
      next
    end
    
    # action hashes allow operations on fields such as append, increment
    if value.is_a?(Hash) && value.key?('_action')
      current_field.json_action(value.delete('_action'), value.delete('_value'), self)
    else
      catch :ignore_value do
        processed_value = current_field.from_json(value, self)
        set(name, processed_value)
      end
    end
  end
  
  save
end

#get(name) ⇒ Object



134
135
136
137
138
139
# File 'lib/yodel/models/core/record/abstract_record.rb', line 134

def get(name)
  ensure_field_is_valid(name)
  return @changed[name] if @changed.key?(name)
  return @typecast[name] if @typecast.key?(name)
  typecast_value(name)
end

#get_meta(name) ⇒ Object



146
147
148
# File 'lib/yodel/models/core/record/abstract_record.rb', line 146

def get_meta(name)
  @values[name]
end

#get_raw(name) ⇒ Object



141
142
143
144
# File 'lib/yodel/models/core/record/abstract_record.rb', line 141

def get_raw(name)
  ensure_field_is_valid(name)
  @values[name]
end

#hashObject



30
31
32
# File 'lib/yodel/models/core/record/abstract_record.rb', line 30

def hash
  id.hash
end

#idObject


Accessors




125
126
127
# File 'lib/yodel/models/core/record/abstract_record.rb', line 125

def id
  object_id
end

#increment!(name, value = 1) ⇒ Object



192
193
194
195
196
197
# File 'lib/yodel/models/core/record/abstract_record.rb', line 192

def increment!(name, value=1)
  ensure_field_is_valid(name)
  current = get(name)
  set(name, current + value)
  save_without_validation
end

#inspectObject



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/yodel/models/core/record/abstract_record.rb', line 69

def inspect
  values = inspect_hash.collect do |name, value|
    if value.is_a?(Array) || value.is_a?(ChangeSensitiveArray)
      value = "[#{value.collect {|element| inspect_value(element)}.join(', ')}]"
    elsif value.is_a?(Hash) || value.is_a?(ChangeSensitiveHash)
      value = "{#{value.to_hash.collect {|key, value| "#{key.to_s}: #{inspect_value(value)}"}.join(', ')}}"
    else
      value = inspect_value(value)
    end
    "#{name}: #{value}"
  end
  "#<#{self.class.name} #{values.join(', ')}>"
end

#inspect_hashObject


Representations




63
64
65
66
67
# File 'lib/yodel/models/core/record/abstract_record.rb', line 63

def inspect_hash
  fields.each_with_object({}) do |(name, field), hash|
    hash[name] = get(name)
  end
end

#inspect_value(value) ⇒ Object



83
84
85
# File 'lib/yodel/models/core/record/abstract_record.rb', line 83

def inspect_value(value)
  value.respond_to?(:to_str) && !value.is_a?(String) ? value.to_str : value.inspect.to_s
end

#new?Boolean


Persistence


Returns:

  • (Boolean)


223
224
225
# File 'lib/yodel/models/core/record/abstract_record.rb', line 223

def new?
  !!@new
end

#prepare_reload_paramsObject



299
300
301
# File 'lib/yodel/models/core/record/abstract_record.rb', line 299

def prepare_reload_params
  {id: id}
end

#present?(name) ⇒ Boolean

Returns:

  • (Boolean)


166
167
168
169
170
# File 'lib/yodel/models/core/record/abstract_record.rb', line 166

def present?(name)
  # FIXME: this doesn't work for many/one store: false
  ensure_field_is_valid(name)
  !get(name).blank?
end

#reloadObject



290
291
292
293
294
295
296
297
# File 'lib/yodel/models/core/record/abstract_record.rb', line 290

def reload
  return if new? || destroyed?
  reload_params = prepare_reload_params
  
  # remove all instance variables and re-initialise
  instance_variables.each {|var| remove_instance_variable(var)}
  perform_reload(reload_params)
end

#saveObject



231
232
233
# File 'lib/yodel/models/core/record/abstract_record.rb', line 231

def save
  valid? ? save_without_validation : false
end

#save_without_validationObject

Raises:



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
263
264
# File 'lib/yodel/models/core/record/abstract_record.rb', line 235

def save_without_validation
  raise DestroyedRecord if destroyed?
  callback = "run_#{new? ? 'create' : 'update'}_callbacks"
  succeeded = false
  
  run_save_callbacks do
    send(callback) do
      
      # untypecast all changed values to construct an up to date values hash
      changed.each do |name, value|
        changed_field = field(name)
        untypecast_value = changed_field.untypecast(value, self)
        if untypecast_value.nil? && changed_field.strip_nil?
          values.delete(name)
        else
          values[name] = untypecast_value
        end
        typecast[name] = value
      end
      succeeded = perform_save
    end
  end
  
  if succeeded
    @new = false
    @changed.clear
    @stash.clear
  end
  succeeded
end

#search_termsObject


Search




389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/yodel/models/core/record/abstract_record.rb', line 389

def search_terms
  search_terms = Set.new
  
  fields.each do |name, field|
    # TODO: we should cache somewhere which types do and do not contain the search_terms_set
    # method; this can also be used to automatically populate the searchable option on fields
    next unless field.searchable? && field.respond_to?(:search_terms_set)
    search_terms.merge(field.search_terms_set(self).collect(&:downcase))
  end
  
  search_terms.to_a
end

#set(name, value) ⇒ Object



150
151
152
153
# File 'lib/yodel/models/core/record/abstract_record.rb', line 150

def set(name, value)
  ensure_field_is_valid(name)
  @changed[name] = value
end

#set_meta(name, value) ⇒ Object



162
163
164
# File 'lib/yodel/models/core/record/abstract_record.rb', line 162

def set_meta(name, value)
  @values[name] = value
end

#set_raw(name, value) ⇒ Object



155
156
157
158
159
160
# File 'lib/yodel/models/core/record/abstract_record.rb', line 155

def set_raw(name, value)
  ensure_field_is_valid(name)
  @values[name] = value
  @changed.delete(name)
  @typecast.delete(name)
end

#to_json(*a) ⇒ Object



93
94
95
# File 'lib/yodel/models/core/record/abstract_record.rb', line 93

def to_json(*a)
  @values.to_json(*a)
end

#to_strObject Also known as: to_s



87
88
89
# File 'lib/yodel/models/core/record/abstract_record.rb', line 87

def to_str
  "#<#{self.class.name}: #{id}>"
end

#trigger_field_callback(order, action) ⇒ Object



378
379
380
381
382
383
# File 'lib/yodel/models/core/record/abstract_record.rb', line 378

def trigger_field_callback(order, action)
  method = "#{order}_#{action}"
  fields.each do |name, field|
    field.send(method, self) if field.respond_to?(method)
  end
end

#update(values, do_save = true) ⇒ Object

Raises:



275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/yodel/models/core/record/abstract_record.rb', line 275

def update(values, do_save=true)
  raise DestroyedRecord if destroyed?
  return if values.empty?
  values.stringify_keys!
  values.each do |name, value|
    ensure_field_is_valid(name)
    if field(name).protected?
      raise MassAssignment, "Cannot mass assign #{field}"
    else
      set(name, value)
    end
  end
  save if do_save
end

#valid?Boolean

Returns:

  • (Boolean)


347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/yodel/models/core/record/abstract_record.rb', line 347

def valid?
  # validate all fields for new records; we know saved records should be
  # valid so we can limit testing to the set of changed fields only
  run_validation_callbacks do
    @errors.clear
    unless new?
      @changed.each {|name, value| field(name).validate(self, @errors)}
    else
      fields.each {|name, field| field.validate(self, @errors)}
    end
  end
  @errors.empty?
end