aboutsummaryrefslogblamecommitdiffstats
path: root/activemodel/lib/active_model/attribute_mutation_tracker.rb
blob: d8cd48a53b4c6e8182e00395647f092cb20720f7 (plain) (tree)
1
2
3
4
5
6
7
8
9

                             
                                                         
                                                   
 
                  
                                          

                                 
                                                        
                              
                                      

       



                                                           


                                                                                    
                                                       



           

                                                                                    
                                                  
                                            



           

                                      
                                                           



                    
                                               


                                                                         


                                                                          


                                    
                                             


                                
                                                                         



                                      
                                          


                               
                                 

       
           
                                              
 


                       




































































                                                                                             
     
 
                                     

                     
                               


        
                      


        
               


        


                                      
                    


           
                               


           
                                    


           
                                 
       
     
   
# frozen_string_literal: true

require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/object/duplicable"

module ActiveModel
  class AttributeMutationTracker # :nodoc:
    OPTION_NOT_GIVEN = Object.new

    def initialize(attributes, forced_changes = Set.new)
      @attributes = attributes
      @forced_changes = forced_changes
    end

    def changed_attribute_names
      attr_names.select { |attr_name| changed?(attr_name) }
    end

    def changed_values
      attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
        if changed?(attr_name)
          result[attr_name] = original_value(attr_name)
        end
      end
    end

    def changes
      attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
        if change = change_to_attribute(attr_name)
          result.merge!(attr_name => change)
        end
      end
    end

    def change_to_attribute(attr_name)
      if changed?(attr_name)
        [original_value(attr_name), fetch_value(attr_name)]
      end
    end

    def any_changes?
      attr_names.any? { |attr| changed?(attr) }
    end

    def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
      attribute_changed?(attr_name) &&
        (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
        (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
    end

    def changed_in_place?(attr_name)
      attributes[attr_name].changed_in_place?
    end

    def forget_change(attr_name)
      attributes[attr_name] = attributes[attr_name].forgetting_assignment
      forced_changes.delete(attr_name)
    end

    def original_value(attr_name)
      attributes[attr_name].original_value
    end

    def force_change(attr_name)
      forced_changes << attr_name
    end

    private
      attr_reader :attributes, :forced_changes

      def attr_names
        attributes.keys
      end

      def attribute_changed?(attr_name)
        forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
      end

      def fetch_value(attr_name)
        attributes.fetch_value(attr_name)
      end
  end

  class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
    def initialize(attributes, forced_changes = {})
      super
      @finalized_changes = nil
    end

    def changed_in_place?(attr_name)
      false
    end

    def change_to_attribute(attr_name)
      if finalized_changes&.include?(attr_name)
        finalized_changes[attr_name].dup
      else
        super
      end
    end

    def forget_change(attr_name)
      forced_changes.delete(attr_name)
    end

    def original_value(attr_name)
      if changed?(attr_name)
        forced_changes[attr_name]
      else
        fetch_value(attr_name)
      end
    end

    def force_change(attr_name)
      forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
    end

    def finalize_changes
      @finalized_changes = changes
    end

    private
      attr_reader :finalized_changes

      def attr_names
        forced_changes.keys
      end

      def attribute_changed?(attr_name)
        forced_changes.include?(attr_name)
      end

      def fetch_value(attr_name)
        attributes.send(:_read_attribute, attr_name)
      end

      def clone_value(attr_name)
        value = fetch_value(attr_name)
        value.duplicable? ? value.clone : value
      rescue TypeError, NoMethodError
        value
      end
  end

  class NullMutationTracker # :nodoc:
    include Singleton

    def changed_attribute_names
      []
    end

    def changed_values
      {}
    end

    def changes
      {}
    end

    def change_to_attribute(attr_name)
    end

    def any_changes?
      false
    end

    def changed?(attr_name, **)
      false
    end

    def changed_in_place?(attr_name)
      false
    end

    def original_value(attr_name)
    end
  end
end