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

                             

                                                   
                  

                           

                                           

         

                                                                
         
 



                                            





                                     
         

       
                                                     


                                                
                                                                                
                  

                                                      
                                              



                                                                      
                                                                        


            







                                         
                          
                           

       





                                                                                  

       

                                                  

       
                                   
                                    
                                                                         


                                       
                                                 

       



                                                   
                       




                                                                              

       
                    


                               



                    



                       



                      





                                                                 




                                                           
 









                                                               
                                                                                                 




                                                                            
             
                                     







                                                        


                                          
                               


                                                 
         
 
                                  
                                                                                 
         
 
                                      
                                      
         
 



                                              
 


                                        
         
 



                                          
 
                           
                                                                             
           
         
 



                                               
 


                             
         
 

                                      
                                              
           
 


                        
 


                                                     
 



                                                                                             
                                                                

         




                                                 
           
 




                         
 


                                      
 





                              
 



                                 


                                    
         
 
                                                                                      

     
# frozen_string_literal: true

require "active_support/core_ext/object/duplicable"

module ActiveModel
  class Attribute # :nodoc:
    class << self
      def from_database(name, value, type)
        FromDatabase.new(name, value, type)
      end

      def from_user(name, value, type, original_attribute = nil)
        FromUser.new(name, value, type, original_attribute)
      end

      def with_cast_value(name, value, type)
        WithCastValue.new(name, value, type)
      end

      def null(name)
        Null.new(name)
      end

      def uninitialized(name, type)
        Uninitialized.new(name, type)
      end
    end

    attr_reader :name, :value_before_type_cast, :type

    # This method should not be called directly.
    # Use #from_database or #from_user
    def initialize(name, value_before_type_cast, type, original_attribute = nil)
      @name = name
      @value_before_type_cast = value_before_type_cast
      @type = type
      @original_attribute = original_attribute
    end

    def value
      # `defined?` is cheaper than `||=` when we get back falsy values
      @value = type_cast(value_before_type_cast) unless defined?(@value)
      @value
    end

    def original_value
      if assigned?
        original_attribute.original_value
      else
        type_cast(value_before_type_cast)
      end
    end

    def value_for_database
      type.serialize(value)
    end

    def changed?
      changed_from_assignment? || changed_in_place?
    end

    def changed_in_place?
      has_been_read? && type.changed_in_place?(original_value_for_database, value)
    end

    def forgetting_assignment
      with_value_from_database(value_for_database)
    end

    def with_value_from_user(value)
      type.assert_valid_value(value)
      self.class.from_user(name, value, type, original_attribute || self)
    end

    def with_value_from_database(value)
      self.class.from_database(name, value, type)
    end

    def with_cast_value(value)
      self.class.with_cast_value(name, value, type)
    end

    def with_type(type)
      if changed_in_place?
        with_value_from_user(value).with_type(type)
      else
        self.class.new(name, value_before_type_cast, type, original_attribute)
      end
    end

    def type_cast(*)
      raise NotImplementedError
    end

    def initialized?
      true
    end

    def came_from_user?
      false
    end

    def has_been_read?
      defined?(@value)
    end

    def ==(other)
      self.class == other.class &&
        name == other.name &&
        value_before_type_cast == other.value_before_type_cast &&
        type == other.type
    end
    alias eql? ==

    def hash
      [self.class, name, value_before_type_cast, type].hash
    end

    def init_with(coder)
      @name = coder["name"]
      @value_before_type_cast = coder["value_before_type_cast"]
      @type = coder["type"]
      @original_attribute = coder["original_attribute"]
      @value = coder["value"] if coder.map.key?("value")
    end

    def encode_with(coder)
      coder["name"] = name
      coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
      coder["type"] = type if type
      coder["original_attribute"] = original_attribute if original_attribute
      coder["value"] = value if defined?(@value)
    end

    protected
      def original_value_for_database
        if assigned?
          original_attribute.original_value_for_database
        else
          _original_value_for_database
        end
      end

    private
      attr_reader :original_attribute
      alias :assigned? :original_attribute

      def initialize_dup(other)
        if defined?(@value) && @value.duplicable?
          @value = @value.dup
        end
      end

      def changed_from_assignment?
        assigned? && type.changed?(original_value, value, value_before_type_cast)
      end

      def _original_value_for_database
        type.serialize(original_value)
      end

      class FromDatabase < Attribute # :nodoc:
        def type_cast(value)
          type.deserialize(value)
        end

        def _original_value_for_database
          value_before_type_cast
        end
      end

      class FromUser < Attribute # :nodoc:
        def type_cast(value)
          type.cast(value)
        end

        def came_from_user?
          !type.value_constructed_by_mass_assignment?(value_before_type_cast)
        end
      end

      class WithCastValue < Attribute # :nodoc:
        def type_cast(value)
          value
        end

        def changed_in_place?
          false
        end
      end

      class Null < Attribute # :nodoc:
        def initialize(name)
          super(name, nil, Type.default_value)
        end

        def type_cast(*)
          nil
        end

        def with_type(type)
          self.class.with_cast_value(name, nil, type)
        end

        def with_value_from_database(value)
          raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
        end
        alias_method :with_value_from_user, :with_value_from_database
        alias_method :with_cast_value, :with_value_from_database
      end

      class Uninitialized < Attribute # :nodoc:
        UNINITIALIZED_ORIGINAL_VALUE = Object.new

        def initialize(name, type)
          super(name, nil, type)
        end

        def value
          if block_given?
            yield name
          end
        end

        def original_value
          UNINITIALIZED_ORIGINAL_VALUE
        end

        def value_for_database
        end

        def initialized?
          false
        end

        def forgetting_assignment
          dup
        end

        def with_type(type)
          self.class.new(name, type)
        end
      end

      private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
  end
end