class ViewModel::ActiveRecord

def self.model_previously_new?(model)

supports activerecord >= 5.0. This is an approximation.
Rails 6.1 introduced "previously_new_record?", but this library still
def self.model_previously_new?(model)
  if (id_changes = model.saved_change_to_id)
    old_id, _new_id = id_changes
    return true if old_id.nil?
  end
  false
end

def _association_data(association_name)

internal
def _association_data(association_name)
  association_data = self._members[association_name.to_s]
  raise ArgumentError.new("Invalid association '#{association_name}'") unless association_data.is_a?(AssociationData)
  association_data
end

def _list_member?

def _list_member?
  _list_attribute_name.present?
end

def _read_association(association_name)

def _read_association(association_name)
  association_data = self.class._association_data(association_name)
  associated = model.public_send(association_data.direct_reflection.name)
  return nil if associated.nil?
  case
  when association_data.through?
    # associated here are join-table models; we need to get the far side out
    join_models = associated
    if association_data.ordered?
      attr = association_data.direct_viewmodel._list_attribute_name
      join_models = join_models.sort_by { |j| j[attr] }
    end
    join_models.map do |through_model|
      model = through_model.public_send(association_data.indirect_reflection.name)
      association_data.viewmodel_class_for_model!(model.class).new(model)
    end
  when association_data.collection?
    associated_viewmodels = associated.map do |x|
      associated_viewmodel_class = association_data.viewmodel_class_for_model!(x.class)
      associated_viewmodel_class.new(x)
    end
    # If any associated type is a list member, they must all be
    if association_data.ordered?
      associated_viewmodels.sort_by!(&:_list_attribute)
    end
    associated_viewmodels
  else
    associated_viewmodel_class = association_data.viewmodel_class_for_model!(associated.class)
    associated_viewmodel_class.new(associated)
  end
end

def _read_association_touched(association_name, touched_ids:)

not being returned, order is also ignored.
large collections that must not be returned fully. Since the collection is
Intended to be used by replace_associated style methods which may touch very

inefficient.
interface is complex due to the data requirements, and the implementation is
Helper to return entities that were part of the last deserialization. The
def _read_association_touched(association_name, touched_ids:)
  association_data = self.class._association_data(association_name)
  associated = model.public_send(association_data.direct_reflection.name)
  return nil if associated.nil?
  case
  when association_data.through?
    # associated here are join-table models; we need to get the far side out
    associated.map do |through_model|
      model = through_model.public_send(association_data.indirect_reflection.name)
      next unless self.class.model_previously_new?(through_model) || touched_ids.include?(model.id)
      association_data.viewmodel_class_for_model!(model.class).new(model)
    end.reject(&:nil?)
  when association_data.collection?
    associated.map do |model|
      next unless self.class.model_previously_new?(model) || touched_ids.include?(model.id)
      association_data.viewmodel_class_for_model!(model.class).new(model)
    end.reject(&:nil?)
  else
    # singleton always touched by definition
    model = associated
    association_data.viewmodel_class_for_model!(model.class).new(model)
  end
end

def _serialize_association(association_name, json, serialize_context:)

def _serialize_association(association_name, json, serialize_context:)
  associated = self.public_send(association_name)
  association_data = self.class._association_data(association_name)
  json.set! association_name do
    case
    when associated.nil?
      json.null!
    when association_data.referenced?
      if association_data.collection?
        json.array!(associated) do |target|
          self.class.serialize_as_reference(target, json, serialize_context: serialize_context)
        end
      else
        self.class.serialize_as_reference(associated, json, serialize_context: serialize_context)
      end
    else
      self.class.serialize(associated, json, serialize_context: serialize_context)
    end
  end
end

def acts_as_list(attr = :position)

`acts_as_manual_list` collection.
Specifies that the model backing this viewmodel is a member of an
def acts_as_list(attr = :position)
  @_list_attribute_name = attr
  @generated_accessor_module.module_eval do
    define_method('_list_attribute') do
      model.public_send(attr)
    end
    define_method('_list_attribute=') do |x|
      model.public_send(:"#{attr}=", x)
    end
  end
end

def association(association_name,

(only applies to when +through+ is set).
- +through_order_attr+ the through model is ordered by the given attribute

ActiveRecord +has_many:through:+.
- +through+ names an ActiveRecord association that will be used like an

External associations may only be made to root viewmodels.
must be independently manipulated using `AssociationManipulation`.
associations are not included in (de)serializations of the parent, and
- +external+ indicates an association external to the view. Externalized

association
- +viewmodel+, +viewmodels+ specifies the viewmodel(s) to use for the

- +as+ sets the name of the association in the viewmodel

indirect reference, while a child viewmodel type will be directly nested.
An association to a root viewmodel type will be serialized with an

be inferred from the model name, or may be explicitly specified.
will be recursively (de)serialized by its own viewmodel type, which will
Adds an association from the model to this viewmodel. The associated model
def association(association_name,
                as: nil,
                viewmodel: nil,
                viewmodels: nil,
                external: false,
                read_only: false,
                through: nil,
                through_order_attr: nil)
  vm_association_name = (as || association_name).to_s
  if through
    direct_association_name   = through
    indirect_association_name = association_name
  else
    direct_association_name   = association_name
    indirect_association_name = nil
  end
  target_viewmodels = Array.wrap(viewmodel || viewmodels)
  association_data = AssociationData.new(
    owner:                     self,
    association_name:          vm_association_name,
    direct_association_name:   direct_association_name,
    indirect_association_name: indirect_association_name,
    target_viewmodels:         target_viewmodels,
    external:                  external,
    read_only:                 read_only,
    through_order_attr:        through_order_attr)
  _members[vm_association_name] = association_data
  @generated_accessor_module.module_eval do
    define_method vm_association_name do
      _read_association(vm_association_name)
    end
    define_method :"serialize_#{vm_association_name}" do |json, serialize_context: self.class.new_serialize_context|
      _serialize_association(vm_association_name, json, serialize_context: serialize_context)
    end
  end
end

def association_changed!(association_name)

def association_changed!(association_name)
  association_name = association_name.to_s
  association_data = self.class._association_data(association_name)
  if association_data.read_only?
    raise ViewModel::DeserializationError::ReadOnlyAssociation.new(association_name, blame_reference)
  end
  unless @changed_associations.include?(association_name)
    @changed_associations << association_name
  end
end

def associations(*assocs, **args)

Specify multiple associations at once
def associations(*assocs, **args)
  assocs.each { |assoc| association(assoc, **args) }
end

def associations_changed?

def associations_changed?
  @changed_associations.present?
end

def cacheable!(**opts)

def cacheable!(**opts)
  include ViewModel::ActiveRecord::Cache::CacheableView
  create_viewmodel_cache!(**opts)
end

def changes

Additionally pass `changed_associations` while constructing changes.
def changes
  ViewModel::Changes.new(
    new:                         new_model?,
    changed_attributes:          changed_attributes,
    changed_associations:        changed_associations,
    changed_nested_children:     changed_nested_children?,
    changed_referenced_children: changed_referenced_children?,
  )
end

def clear_changes!

def clear_changes!
  super.tap do
    @changed_associations = []
  end
end

def context_for_child(member_name, context:)

def context_for_child(member_name, context:)
  # Synthetic viewmodels don't exist as far as the traversal context is
  # concerned: pass through the child context received from the parent
  return context if self.class.synthetic
  # associations to roots start a new tree
  member_data = self.class._members[member_name.to_s]
  if member_data.association? && member_data.referenced?
    return context.for_references
  end
  super
end

def deep_schema_version(include_referenced: true, include_external: true)

def deep_schema_version(include_referenced: true, include_external: true)
  (@deep_schema_version ||= {})[[include_referenced, include_external]] ||=
    begin
      vms = dependent_viewmodels(include_referenced: include_referenced, include_external: include_external)
      ViewModel.schema_versions(vms).freeze
    end
end

def dependent_viewmodels(seen = Set.new, include_referenced: true, include_external: true)

def dependent_viewmodels(seen = Set.new, include_referenced: true, include_external: true)
  return if seen.include?(self)
  seen << self
  _members.each_value do |data|
    next unless data.is_a?(AssociationData)
    next unless include_referenced || !data.referenced?
    next unless include_external   || !data.external?
    data.viewmodel_classes.each do |vm|
      vm.dependent_viewmodels(seen, include_referenced: include_referenced, include_external: include_external)
    end
  end
  seen
end

def deserialize_from_view(subtree_hash_or_hashes, references: {}, deserialize_context: new_deserialize_context)

def deserialize_from_view(subtree_hash_or_hashes, references: {}, deserialize_context: new_deserialize_context)
  model_class.transaction do
    ViewModel::Utils.wrap_one_or_many(subtree_hash_or_hashes) do |subtree_hashes|
      root_update_data, referenced_update_data = UpdateData.parse_hashes(subtree_hashes, references)
      _updated_viewmodels =
        UpdateContext
          .build!(root_update_data, referenced_update_data, root_type: self)
          .run!(deserialize_context: deserialize_context)
    end
  end
rescue ViewModel::DeserializationError => e
  if (new_error = customize_deserialization_error(e))
    raise new_error
  else
    raise
  end
end

def destroy!(deserialize_context: self.class.new_deserialize_context)

def destroy!(deserialize_context: self.class.new_deserialize_context)
  model_class.transaction do
    ViewModel::Callbacks.wrap_deserialize(self, deserialize_context: deserialize_context) do |hook_control|
      changes = ViewModel::Changes.new(deleted: true)
      deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, self, changes: changes)
      hook_control.record_changes(changes)
      model.destroy!
    end
  end
end

def eager_includes(include_referenced: true, vm_path: [])

after two layers of eager loading.
serializing/deserializing this view. Cycles in the schema will be broken
Constructs a preload specification of the required models for
def eager_includes(include_referenced: true, vm_path: [])
  association_specs = {}
  return nil if vm_path.count(self) > 2
  child_path = vm_path + [self]
  _members.each do |assoc_name, association_data|
    next unless association_data.is_a?(AssociationData)
    next if association_data.external?
    case
    when association_data.through?
      viewmodel = association_data.direct_viewmodel
      children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
    when !include_referenced && association_data.referenced?
      children = nil # Load up to the root viewmodel, but no further
    when association_data.polymorphic?
      children_by_klass = {}
      association_data.viewmodel_classes.each do |vm_class|
        klass = vm_class.model_class.name
        children_by_klass[klass] = vm_class.eager_includes(include_referenced: include_referenced, vm_path: child_path)
      end
      children = DeepPreloader::PolymorphicSpec.new(children_by_klass)
    else
      viewmodel = association_data.viewmodel_class
      children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path)
    end
    association_specs[association_data.direct_reflection.name.to_s] = children
  end
  DeepPreloader::Spec.new(association_specs)
end

def find(id_or_ids, scope: nil, lock: nil, eager_include: true)

# Load instances of the viewmodel by id(s)
def find(id_or_ids, scope: nil, lock: nil, eager_include: true)
  find_scope = self.model_class.all
  find_scope = find_scope.order(:id).lock(lock) if lock
  find_scope = find_scope.merge(scope) if scope
  ViewModel::Utils.wrap_one_or_many(id_or_ids) do |ids|
    models = find_scope.where(id: ids).to_a
    if models.size < ids.size
      missing_ids = ids - models.map(&:id)
      if missing_ids.present?
        raise ViewModel::DeserializationError::NotFound.new(
                missing_ids.map { |id| ViewModel::Reference.new(self, id) })
      end
    end
    vms = models.map { |m| self.new(m) }
    ViewModel.preload_for_serialization(vms, lock: lock) if eager_include
    vms
  end
end

def initialize(*)

def initialize(*)
  super
  model_is_new! if model.new_record?
  @changed_associations = []
end

def load(scope: nil, eager_include: true, lock: nil)

# TODO: is this too much of a encapsulation violation?
# Load instances of the viewmodel by scope
def load(scope: nil, eager_include: true, lock: nil)
  load_scope = self.model_class.all
  load_scope = load_scope.lock(lock) if lock
  load_scope = load_scope.merge(scope) if scope
  vms = load_scope.map { |model| self.new(model) }
  ViewModel.preload_for_serialization(vms, lock: lock) if eager_include
  vms
end

def serialize_members(json, serialize_context: self.class.new_serialize_context)

def serialize_members(json, serialize_context: self.class.new_serialize_context)
  self.class._members.each do |member_name, member_data|
    next if member_data.association? && member_data.external?
    member_context =
      case member_data
      when AssociationData
        self.context_for_child(member_name, context: serialize_context)
      else
        serialize_context
      end
    self.public_send("serialize_#{member_name}", json, serialize_context: member_context)
  end
end