aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/properties.rb
blob: 48ee42aaca963035150e1b9c3dc0c1bb1ed2d888 (plain) (tree)
1
2
3
4
5
6
7
8
                   
                             

                                 
                             
 
               
                                                                                   


                                     




                                                                                         










                                                                                             















































                                                                                                 
                                                 
                        
                                            
                                                                         
                                                                                                                                   



                                                                                    
                                                                                           








                                                                                  
                                            



             
                                                   







                                                                                 
         
 
                                              

                           

                              
                                  

                              
         


       
module ActiveRecord
  module Properties # :nodoc:
    extend ActiveSupport::Concern

    Type = ActiveRecord::Type

    included do
      class_attribute :user_provided_columns, instance_accessor: false # :internal:
      self.user_provided_columns = {}
    end

    module ClassMethods
      # Defines or overrides a property on this model. This allows customization of
      # Active Record's type casting behavior, as well as adding support for user defined
      # types.
      #
      # +name+ The name of the methods to define attribute methods for, and the column which
      # this will persist to.
      #
      # +cast_type+ A type object that contains information about how to type cast the value.
      # See the examples section for more information.
      #
      # ==== Options
      # The options hash accepts the following options:
      #
      # +default+ is the default value that the column should use on a new record.
      #
      # ==== Examples
      #
      # The type detected by Active Record can be overriden.
      #
      #   # db/schema.rb
      #   create_table :store_listings, force: true do |t|
      #     t.decimal :price_in_cents
      #   end
      #
      #   # app/models/store_listing.rb
      #   class StoreListing < ActiveRecord::Base
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '10.1')
      #
      #   # before
      #   store_listing.price_in_cents # => BigDecimal.new(10.1)
      #
      #   class StoreListing < ActiveRecord::Base
      #     property :price_in_cents, Type::Integer.new
      #   end
      #
      #   # after
      #   store_listing.price_in_cents # => 10
      #
      # Users may also define their own custom types, as long as they respond to the methods
      # defined on the value type. The `type_cast` method on your type object will be called
      # with values both from the database, and from your controllers. See
      # `ActiveRecord::Properties::Type::Value` for the expected API. It is recommended that your
      # type objects inherit from an existing type, or the base value type.
      #
      #   class MoneyType < ActiveRecord::Type::Integer
      #     def type_cast(value)
      #       if value.include?('$')
      #         price_in_dollars = value.gsub(/\$/, '').to_f
      #         price_in_dollars * 100
      #       else
      #         value.to_i
      #       end
      #     end
      #   end
      #
      #   class StoreListing < ActiveRecord::Base
      #     property :price_in_cents, MoneyType.new
      #   end
      #
      #   store_listing = StoreListing.new(price_in_cents: '$10.00')
      #   store_listing.price_in_cents # => 1000
      def property(name, cast_type, options = {})
        name = name.to_s
        clear_caches_calculated_from_columns
        # Assign a new hash to ensure that subclasses do not share a hash
        self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type))
      end

      # Returns an array of column objects for the table associated with this class.
      def columns
        @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
      end

      # Returns a hash of column objects for the table associated with this class.
      def columns_hash
        @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
      end

      def reset_column_information # :nodoc:
        super
        clear_caches_calculated_from_columns
      end

      private

      def add_user_provided_columns(schema_columns)
        existing_columns = schema_columns.map do |column|
          user_provided_columns[column.name] || column
        end

        existing_column_names = existing_columns.map(&:name)
        new_columns = user_provided_columns.except(*existing_column_names).values

        existing_columns + new_columns
      end

      def clear_caches_calculated_from_columns
        @columns = nil
        @columns_hash = nil
        @column_types = nil
        @column_defaults = nil
        @raw_column_defaults = nil
        @column_names = nil
        @content_columns = nil
      end
    end
  end
end