class Volt::Model

Constants

INVALID_FIELD_NAMES

Attributes

attributes[R]
options[R]
parent[R]
path[R]
persistor[R]

Public Class Methods

new(attributes = {}, options = {}, initial_state = nil) click to toggle source
# 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

Private Class Methods

inherited(subclass) click to toggle source
# File lib/volt/models/model.rb, line 433
def self.inherited(subclass)
  if defined?(RootModels)
    RootModels.add_model_class(subclass)
  end
end
process_class_name(name) click to toggle source
# File lib/volt/models/model.rb, line 439
def self.process_class_name(name)
  name.singularize
end

Public Instance Methods

!() click to toggle source

Pass through needed

# File lib/volt/models/model.rb, line 178
def !
  !attributes
end
==(val) click to toggle source

Pass the comparison through

Calls superclass method
# 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
_id() click to toggle source
# File lib/volt/models/model.rb, line 121
def _id
  get(:id)
end
assign_attributes(attrs, initial_setup = false, skip_changes = false) click to toggle source

Assign multiple attributes as a hash, directly.

# 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
Also aliased as: attributes=
attributes=(attrs, initial_setup = false, skip_changes = false)
Alias for: assign_attributes
destroy() click to toggle source
# 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) click to toggle source

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.

# 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
id() click to toggle source
# File lib/volt/models/model.rb, line 113
def id
  get(:id)
end
id=(val) click to toggle source
# File lib/volt/models/model.rb, line 117
def id=(val)
  set(:id, val)
end
inspect() click to toggle source
# 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
method_missing(method_name, *args, &block) click to toggle source
# 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
new?() click to toggle source

Return true if the model hasn't been saved yet

# File lib/volt/models/model.rb, line 126
def new?
  @new
end
new_array_model(attributes, options) click to toggle source
# 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) click to toggle source
# 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
options=(options) click to toggle source

Update the options

# File lib/volt/models/model.rb, line 131
def options=(options)
  @options     = options
  @parent      = options[:parent]
  @path        = options[:path] || []
  @class_paths = options[:class_paths]
  @persistor   = setup_persistor(options[:persistor])
end
read_new_model(method_name) click to toggle source

Get a new model, make it easy to override

# 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
release() click to toggle source
# File lib/volt/models/model.rb, line 104
def release
  @listener_event_counter.remove
end
respond_to_missing?(method_name, include_private = false) click to toggle source
Calls superclass method
# 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
retain() click to toggle source
# File lib/volt/models/model.rb, line 100
def retain
  @listener_event_counter.add
end
set(attribute_name, value, &block) click to toggle source

Do the assignment to a model and trigger a changed event

# 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) click to toggle source
Calls superclass method Volt::StateManager#state_for
# File lib/volt/models/model.rb, line 108
def state_for(*args)
  @root_dep.depend
  super
end
to_json() click to toggle source
# File lib/volt/models/model.rb, line 353
def to_json
  to_h.to_json
end
update(attrs) click to toggle source

Update tries to update the model and returns

# 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

Private Instance Methods

assign_all_attributes(attrs, track_changes = false) click to toggle source

Used internally from other methods that assign all attributes

# File lib/volt/models/model.rb, line 409
def assign_all_attributes(attrs, track_changes = false)
  # Assign each attribute using setters
  attrs.each_pair do |key, value|
    key = key.to_sym

    # Track the change, since assign_all_attributes runs with no_change_tracking
    old_val = @attributes[key]
    attribute_will_change!(key, old_val) if track_changes && old_val != value

    if self.respond_to?(:"#{key}=")
      # If a method without an underscore is defined, call that.
      send(:"#{key}=", value)
    else
      # Otherwise, use the _ version
      set(key, value)
    end
  end

  # Make an id if there isn't one yet
  if @attributes[:id].nil? && persistor.try(:auto_generate_id)
    @attributes[:id] = generate_id
  end
end
check_valid_field_name(name) click to toggle source

Volt provides a few access methods to get more data about the model, we want to prevent these from being assigned or accessed through underscore methods.

# File lib/volt/models/model.rb, line 402
def check_valid_field_name(name)
  if INVALID_FIELD_NAMES[name]
    fail InvalidFieldName, "`#{name}` is reserved and can not be used as a field"
  end
end
run_initial_setup(initial_setup) click to toggle source
# File lib/volt/models/model.rb, line 377
def run_initial_setup(initial_setup)
  # Save the changes
  if initial_setup
    # Run initial validation
    if Volt.in_mode?(:no_validate)
      # No validate, resolve self
      Promise.new.resolve(self)
    else
      return validate!.then do |errs|
        if errs && errs.size > 0
          Promise.new.reject(errs)
        else
          Promise.new.resolve(self)
        end
      end
    end
  else
    return run_changed
  end
end