lib/view_model/active_record/cloner.rb
# frozen_string_literal: true # Simple visitor for cloning models through the tree structure defined by # ViewModel::ActiveRecord. Owned associations will be followed and cloned, while # non-owned referenced associations will be copied directly as references. # Attributes (including association foreign keys not covered by ViewModel # `association`s) will be copied from the original. # # To customize, subclasses may define methods `visit_x_view(node, new_model)` # for each type they wish to affect. These callbacks may update attributes of # the new model, and additionally can call `ignore!` or # `ignore_association!(name)` to prune the current model or the target of the # named association from the cloned tree. class ViewModel::ActiveRecord::Cloner def clone(node) reset_state! new_model = node.model.dup pre_visit(node, new_model) return nil if ignored? if node.class.name class_name = node.class.name.underscore.gsub('/', '__') visit = :"visit_#{class_name}" end_visit = :"end_visit_#{class_name}" end if visit && respond_to?(visit, true) self.send(visit, node, new_model) return nil if ignored? end # visit the underlying viewmodel for each association, ignoring any # customization ignored_associations = @ignored_associations node.class._members.each do |name, association_data| next unless association_data.is_a?(ViewModel::ActiveRecord::AssociationData) reflection = association_data.direct_reflection if ignored_associations.include?(name) new_associated = association_data.collection? ? [] : nil else # Load the record associated with the old model associated = node.model.public_send(reflection.name) if associated.nil? new_associated = nil elsif !association_data.owned? && !association_data.through? # simply attach the associated target to the new model new_associated = associated else # Otherwise descend into the child, and attach the result build_vm = ->(model) do vm_class = if association_data.through? # descend into the synthetic join table viewmodel association_data.direct_viewmodel else association_data.viewmodel_class_for_model!(model.class) end vm_class.new(model) end new_associated = if ViewModel::Utils.array_like?(associated) associated.map { |m| clone(build_vm.(m)) }.compact else clone(build_vm.(associated)) end end end new_association = new_model.association(reflection.name) new_association.writer(new_associated) end if end_visit && respond_to?(end_visit, true) self.send(end_visit, node, new_model) end post_visit(node, new_model) new_model end def pre_visit(node, new_model); end def post_visit(node, new_model); end private def reset_state! @ignored = false @ignored_associations = Set.new end def ignore! @ignored = true end def ignore_association!(name) @ignored_associations.add(name.to_s) end def ignored? @ignored end end