Class: Volt::Model

Direct Known Subclasses

ActiveVoltInstance, BaseRootModel, User

Defined Under Namespace

Modules: Permissions

Constant Summary collapse

INVALID_FIELD_NAMES =
{
  attributes: true,
  parent: true,
  path: true,
  options: true,
  persistor: true
}

Constants included from FieldHelpers

FieldHelpers::FIELD_CASTS, FieldHelpers::NUMERIC_CAST

Constants included from Volt::Models::Helpers::Base

Volt::Models::Helpers::Base::ID_CHARS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Volt::Models::Helpers::ChangeHelpers

included, #run_changed

Methods included from ReactiveAccessors

#__reactive_dependency_get, included

Methods included from Associations

included

Methods included from Permissions

#action_allowed?, #allow, #allow_and_deny_fields, #deny, #filtered_attributes, included, #owner?

Methods included from Volt::Models::Helpers::ListenerTracker

#listener_added, #listener_removed

Methods included from Modes

included

Methods included from ClassEventable

included

Methods included from Volt::Models::Helpers::Dirty

#attribute_will_change!, #changed?, #changed_attributes, #changes, #clear_tracked_changes!, #revert_changes!, #was

Methods included from FieldHelpers

included

Methods included from Buffer

#buffer, #buffer?, included, #promise_for_errors, #save!, #save_to, #save_to=, #saved?, #saved_state

Methods included from Validations

#clear_server_errors, #error_in_changed_attributes?, #errors, included, #mark_all_fields!, #mark_field!, #marked_errors, #marked_fields, #server_errors, #validate, #validate!

Methods included from Volt::Models::Helpers::Model

#saved?, #saved_state

Methods included from StateManager

#change_state_to

Methods included from ModelHashBehaviour

#clear, #delete, #each, #each_pair, #each_with_object, #empty?, #key?, #keys, #nil?, #size, #to_h

Methods included from Volt::Models::Helpers::Base

#deep_unwrap, #event_added, #event_removed, #generate_id, included, #root, #self_attributes, #setup_persistor, #store

Methods included from ModelWrapper

#wrap_value, #wrap_values

Methods included from LifecycleCallbacks

included, #run_callbacks, #stop_chain

Constructor Details

#initialize(attributes = {}, options = {}, initial_state = nil) ⇒ Model

Returns a new instance of Model.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/volt/models/model.rb', line 64

def initialize(attributes = {}, options = {}, initial_state = nil)
  # Start off with empty attributes
  @attributes = {}

  # The listener event counter keeps track of how many computations are listening on this model
  @listener_event_counter = EventCounter.new(
    -> { parent.try(:persistor).try(:listener_added) },
    -> { parent.try(:persistor).try(:listener_removed) }
  )

  # The root dependency is used to track if anything is using the data from this
  # model.  That information is relayed to the ArrayModel so it knows when it can
  # stop subscribing.
  # @root_dep    = Dependency.new(@listener_event_counter.method(:add), @listener_event_counter.method(:remove))
  @root_dep    = Dependency.new(-> { retain }, -> { release })

  @deps        = HashDependency.new
  @size_dep    = Dependency.new
  self.options = options

  @new = (initial_state != :loaded)

  assign_attributes(attributes, true)

  # The persistor is usually responsible for setting up the loaded_state, if
  # there is no persistor, we set it to loaded
  if @persistor
    @persistor.loaded(initial_state)
  else
    change_state_to(:loaded_state, initial_state || :loaded, false)
  end

  # Trigger the new event, pass in :new
  trigger!(:new, :new)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/volt/models/model.rb', line 182

def method_missing(method_name, *args, &block)
  if method_name[0] == '_'
    # Remove underscore
    method_name = method_name[1..-1]
    if method_name[-1] == '='
      # Assigning an attribute without the =
      set(method_name[0..-2], args[0], &block)
    else
      # If the method has an ! on the end, then we assign an empty
      # collection if no result exists already.
      expand = (method_name[-1] == '!')
      method_name = method_name[0..-2] if expand

      get(method_name, expand)
    end
  else
    # Call on parent
    super
  end
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



54
55
56
# File 'lib/volt/models/model.rb', line 54

def attributes
  @attributes
end

#optionsObject

Returns the value of attribute options.



54
55
56
# File 'lib/volt/models/model.rb', line 54

def options
  @options
end

#parentObject (readonly)

Returns the value of attribute parent.



54
55
56
# File 'lib/volt/models/model.rb', line 54

def parent
  @parent
end

#pathObject (readonly)

Returns the value of attribute path.



54
55
56
# File 'lib/volt/models/model.rb', line 54

def path
  @path
end

#persistorObject (readonly)

Returns the value of attribute persistor.



54
55
56
# File 'lib/volt/models/model.rb', line 54

def persistor
  @persistor
end

Instance Method Details

#!Object

Pass through needed



178
179
180
# File 'lib/volt/models/model.rb', line 178

def !
  !attributes
end

#==(val) ⇒ Object

Pass the comparison through



167
168
169
170
171
172
173
174
175
# File 'lib/volt/models/model.rb', line 167

def ==(val)
  if val.is_a?(Model)
    # Use normal comparison for a model
    super
  else
    # Compare to attributes otherwise
    attributes == val
  end
end

#_idObject



121
122
123
# File 'lib/volt/models/model.rb', line 121

def _id
  get(:id)
end

#assign_attributes(attrs, initial_setup = false, skip_changes = false) ⇒ Object Also known as: attributes=

Assign multiple attributes as a hash, directly.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/volt/models/model.rb', line 140

def assign_attributes(attrs, initial_setup = false, skip_changes = false)
  attrs = wrap_values(attrs)

  if attrs
    # When doing a mass-assign, we don't validate or save until the end.
    if initial_setup || skip_changes
      Model.no_change_tracking do
        assign_all_attributes(attrs, skip_changes)
      end
    else
      assign_all_attributes(attrs)
    end
  else
    # Assign to empty
    @attributes = {}
  end

  # Trigger and change all
  @deps.changed_all!
  @deps = HashDependency.new

  run_initial_setup(initial_setup)
end

#destroyObject



335
336
337
338
339
340
341
342
343
344
# File 'lib/volt/models/model.rb', line 335

def destroy
  if parent
    result = parent.delete(self)

    # Wrap result in a promise if it isn't one
    return result#.then
  else
    fail 'Model does not have a parent and cannot be deleted.'
  end
end

#get(attr_name, expand = false) ⇒ Object

When reading an attribute, we need to handle reading on: 1) a nil model, which returns a wrapped error 2) reading directly from attributes 3) trying to read a key that doesn’t exist.



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
# File 'lib/volt/models/model.rb', line 239

def get(attr_name, expand = false)
  # Reading an attribute, we may get back a nil model.
  attr_name = attr_name.to_sym

  check_valid_field_name(attr_name)

  # Track that something is listening
  @root_dep.depend

  # Track dependency
  @deps.depend(attr_name)

  # See if the value is in attributes
  if @attributes && @attributes.key?(attr_name)
    return @attributes[attr_name]
  else
    # If we're expanding, or the get is for a collection, in which
    # case we always expand.
    plural_attr = attr_name.plural?
    if expand || plural_attr
      new_value = read_new_model(attr_name)

      # A value was generated, store it
      if new_value
        # Assign directly.  Since this is the first time
        # we're loading, we can just assign.
        #
        # Don't track changes if we're setting a collection
        Volt.run_in_mode_if(plural_attr, :no_change_tracking) do
          set(attr_name, new_value)
        end
      end

      return new_value
    else
      return nil
    end
  end
end

#idObject



113
114
115
# File 'lib/volt/models/model.rb', line 113

def id
  get(:id)
end

#id=(val) ⇒ Object



117
118
119
# File 'lib/volt/models/model.rb', line 117

def id=(val)
  set(:id, val)
end

#inspectObject



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/volt/models/model.rb', line 312

def inspect
  Computation.run_without_tracking do
    str = "#<#{self.class}"
    # str += ":#{object_id}"

    # First, select all of the non-ArrayModel values
    attrs = attributes.reject {|key, val| val.is_a?(ArrayModel) }.to_h

    # Show the :id first, then sort the rest of the attributes
    id = attrs.delete(:id)
    id = id[0..3] + '..' + id[-4..-1] if id

    attrs = attrs.sort
    attrs.insert(0, [:id, id]) if id

    str += attrs.map do |key, value|
      " #{key}: #{value.inspect}"
    end.join(',')
    str += '>'
    str
  end
end

#new?Boolean

Return true if the model hasn’t been saved yet

Returns:



126
127
128
# File 'lib/volt/models/model.rb', line 126

def new?
  @new
end

#new_array_model(attributes, options) ⇒ Object



304
305
306
307
308
309
310
# File 'lib/volt/models/model.rb', line 304

def new_array_model(attributes, options)
  # Start with an empty query
  options         = options.dup
  options[:query] = []

  Volt::ArrayModel.class_at_path(options[:path]).new(attributes, options)
end

#new_model(attributes = {}, new_options = {}, initial_state = nil) ⇒ Object



298
299
300
301
302
# File 'lib/volt/models/model.rb', line 298

def new_model(attributes = {}, new_options = {}, initial_state = nil)
  new_options = new_options.merge(persistor: @persistor)

  Volt::Model.class_at_path(new_options[:path]).new(attributes, new_options, initial_state)
end

#read_new_model(method_name) ⇒ Object

Get a new model, make it easy to override



284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/volt/models/model.rb', line 284

def read_new_model(method_name)
  if @persistor && @persistor.respond_to?(:read_new_model)
    return @persistor.read_new_model(method_name)
  else
    opts = @options.merge(parent: self, path: path + [method_name])

    if method_name.plural?
      return new_array_model([], opts)
    else
      return new_model({}, opts)
    end
  end
end

#releaseObject



104
105
106
# File 'lib/volt/models/model.rb', line 104

def release
  @listener_event_counter.remove
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:



279
280
281
# File 'lib/volt/models/model.rb', line 279

def respond_to_missing?(method_name, include_private = false)
  method_name.to_s.start_with?('_') || super
end

#retainObject



100
101
102
# File 'lib/volt/models/model.rb', line 100

def retain
  @listener_event_counter.add
end

#set(attribute_name, value, &block) ⇒ Object

Do the assignment to a model and trigger a changed event



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/volt/models/model.rb', line 204

def set(attribute_name, value, &block)
  # Assign, without the =
  attribute_name = attribute_name.to_sym

  check_valid_field_name(attribute_name)

  old_value = @attributes[attribute_name]
  new_value = wrap_value(value, [attribute_name])

  if old_value != new_value
    # Track the old value, skip if we are in no_validate
    attribute_will_change!(attribute_name, old_value) unless Volt.in_mode?(:no_change_tracking)

    # Assign the new value
    @attributes[attribute_name] = new_value

    @deps.changed!(attribute_name)

    @size_dep.changed! if old_value.nil? || new_value.nil?

    # TODO: Can we make this so it doesn't need to be handled for non store collections
    # (maybe move it to persistor, though thats weird since buffers don't have a persistor)
    clear_server_errors(attribute_name) if @server_errors.present?

    # Save the changes
    run_changed(attribute_name) unless Volt.in_mode?(:no_change_tracking)
  end

  new_value
end

#state_for(*args) ⇒ Object



108
109
110
111
# File 'lib/volt/models/model.rb', line 108

def state_for(*args)
  @root_dep.depend
  super
end

#to_jsonObject



353
354
355
# File 'lib/volt/models/model.rb', line 353

def to_json
  to_h.to_json
end

#update(attrs) ⇒ Object

Update tries to update the model and returns



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/volt/models/model.rb', line 358

def update(attrs)
  old_attrs = @attributes.dup
  Model.no_change_tracking do
    assign_all_attributes(attrs, false)

    validate!.then do |errs|
      if errs && errs.present?
        # Revert wholesale
        @attributes = old_attrs
        Promise.new.resolve(errs)
      else
        # Persist
        persist_changes(nil)
      end
    end
  end
end