module ActiveSnapshot module SnapshotsConcern extend ActiveSupport::Concern included do ### We do NOT mark these as dependent: :destroy, the developer must manually destroy the snapshots or individual snapshot items has_many :snapshots, as: :item, class_name: 'ActiveSnapshot::Snapshot' has_many :snapshot_items, as: :item, class_name: 'ActiveSnapshot::SnapshotItem' end def create_snapshot!(identifier, user: nil, metadata: nil) snapshot = snapshots.create!({ identifier: identifier, user_id: (user.id if user), user_type: (user.class.name if user), metadata: (metadata || {}), }) snapshot_items = [] snapshot_items << snapshot.build_snapshot_item(self) snapshot_children = self.children_to_snapshot if snapshot_children snapshot_children.each do |child_group_name, h| h[:records].each do |child_item| snapshot_items << snapshot.build_snapshot_item(child_item, child_group_name: child_group_name) end end end SnapshotItem.import(snapshot_items, validate: true) snapshot end class_methods do def has_snapshot_children(&block) if !block_given? && !defined?(@snapshot_children_proc) raise ArgumentError.new("Invalid `has_snapshot_children` requires block to be defined") elsif block_given? @snapshot_children_proc = block else @snapshot_children_proc end end end def children_to_snapshot snapshot_children_proc = self.class.has_snapshot_children if !snapshot_children_proc raise ArgumentError.new("`has_snapshot_children` must be defined on your class") else records = self.instance_exec(&snapshot_children_proc) if records.is_a?(Hash) records = records.with_indifferent_access else raise ArgumentError.new("Invalid `has_snapshot_children` definition. Must return a Hash") end snapshot_children = {}.with_indifferent_access records.each do |assoc_name, opts| snapshot_children[assoc_name] = {} if opts.is_a?(ActiveRecord::Relation) || opts.is_a?(Array) snapshot_children[assoc_name][:records] = opts elsif opts.is_a?(Hash) opts = opts.with_indifferent_access records = opts[:records] || opts[:record] if records if records.respond_to?(:to_a) records = records.to_a else records = [records] end snapshot_children[assoc_name][:records] = records else raise ArgumentError.new("Invalid `has_snapshot_children` definition. Must define a :records key for each child association.") end delete_method = opts[:delete_method] if delete_method.present? && delete_method.to_s != "default" if delete_method.respond_to?(:call) snapshot_children[assoc_name][:delete_method] = delete_method else raise ArgumentError.new("Invalid `has_snapshot_children` definition. Invalid :delete_method argument. Must be a Lambda / Proc") end end else raise ArgumentError.new("Invalid `has_snapshot_children` definition. Invalid :records argument. Must be a Hash or Array") end end return snapshot_children end end end end